GitHub - CodingCogs-OSS/Fast-Paseto

GitHub

A high-performance

PASETO

(Platform-Agnostic Security Tokens) library with a Rust core and Python bindings via PyO3.

Features

Blazing fast — Cryptographic operations implemented in Rust

Zero Python dependencies — Pure Rust extension module

Type-safe — Full type hints with .pyi stubs

PASETO v2, v3, v4 — All modern versions supported

PASERK support — Key serialization, wrapping, and password protection

PEM key loading — Import Ed25519 keys from standard PEM format

Performance

Time per operation, lower is better. Measured on Python 3.11 / Windows 11 (i7-13xxH) with a release build. Every library performs the same logical work: v4 tokens, the same claims dict, JSON serialization included.

Operationfast-paseto

pyseto

python-paseto

pypaseto

PyJWT

generate symmetric key0.11 µs1.03 µs0.67 µs0.47 µs0.18 µsgenerate keypair14.2 µs74.5 µs27.7 µs26.1 µs30.4 µsv4.local encode4.33 µs19.0 µs8.98 µs13.1 µs9.28 µsv4.local decode4.02 µs21.5 µs9.68 µs12.6 µs11.4 µsv4.public encode (sign)35.8 µs40.0 µs32.3 µs32.0 µs36.5 µsv4.public decode (verify)34.9 µs99.9 µs86.4 µs87.6 µs94.1 µsRelative to fast-paseto (higher means slower than fast-paseto):

Operationpysetopython-pasetopypasetoPyJWTgenerate symmetric key9.3x6.1x4.3x1.7xgenerate keypair5.2x1.9x1.8x2.1xv4.local encode4.4x2.1x3.0x2.1xv4.local decode5.3x2.4x3.1x2.8xv4.public encode (sign)1.1x0.9x0.9x1.0xv4.public decode (verify)2.9x2.5x2.5x2.7xReading the numbers:

Symmetric paths are where the Rust core pays off most: 4-5x faster than the other PASETO libraries on encode/decode, and roughly 9x on key generation versus pyseto.

Ed25519 signing is a wash. The libsodium-backed libraries edge ahead by roughly 10%, because that operation is dominated by the same underlying primitive everywhere. Ed25519 verification is ~2.5x faster in fast-paseto.

PyJWT is included as a reference point, not a like-for-like comparison. Its "local" rows are HS256, which is signed but not encrypted, so it is doing strictly less work than a PASETO local token yet still comes out slower.

Running the benchmarks

python profiling/benchmark.py # all libraries python profiling/benchmark.py --only pyseto pyjwt # a subset python profiling/benchmark.py --json out.json # keep the raw timingspython-paseto and pypaseto both ship a top-level paseto module, so they cannot be installed side by side. The benchmark works around this by measuring each library in its own throwaway environment via uv run --no-project --with <package>; nothing but fast-paseto needs to be present in your venv.

Both of those libraries also bind libsodium through pysodium, so it must be installed for their rows to appear:

sudo apt install libsodium23 # Debian/Ubuntu brew install libsodium # macOS# Windows: put libsodium.dll on PATH, or point LIBSODIUM_DIR at its folderLibraries that cannot be loaded are reported as unavailable with the reason, rather than silently dropped from the table.

Installation

pip install fast-pasetoQuick Start

Local Tokens (Symmetric Encryption)

importfast_paseto# Generate a random 32-byte symmetric keykey=fast_paseto.generate_symmetric_key() # Create an encrypted tokentoken=fast_paseto.encode( key=key, payload={"user_id": 123, "role": "admin"}, purpose="local", ) # => "v4.local...."# Decode and verify the tokendecoded=fast_paseto.decode(token, key, purpose="local") print(decoded.payload) # {"user_id": 123, "role": "admin"}Public Tokens (Asymmetric Signatures)

importfast_paseto# Generate an Ed25519 keypairsecret_key, public_key=fast_paseto.generate_keypair() # Create a signed token (not encrypted!)token=fast_paseto.encode( key=secret_key, payload={"user_id": 123, "permissions": ["read", "write"]}, purpose="public", ) # => "v4.public...."# Verify the signature and decodedecoded=fast_paseto.decode(token, public_key, purpose="public") print(decoded.payload) # {"user_id": 123, "permissions": ["read", "write"]}Using the Paseto Class

For applications that need consistent defaults across multiple tokens:

fromfast_pasetoimportPaseto, generate_symmetric_key# Create a configured instancepaseto=Paseto( default_exp=3600, # Tokens expire in 1 hourinclude_iat=True, # Auto-add issued-at timestampleeway=60, # Allow 60s clock skew on verification ) key=generate_symmetric_key() # Encode with automatic exp/iat claimstoken=paseto.encode(key, {"user_id": 123}) # Decode with leeway applieddecoded=paseto.decode(token, key) print(decoded["user_id"]) # 123Token Types

TypePurposeUse CaselocalSymmetric encryptionConfidential data between trusted partiespublicAsymmetric signaturesVerifiable claims (not encrypted!)Supported Versions

VersionLocal (Encryption)Public (Signatures)v4 (default)XChaCha20-Poly1305Ed25519v3AES-256-CTR + HMAC-SHA384ECDSA P-384v2XChaCha20-Poly1305Ed25519Key Management (PASERK)

Key Serialization

importfast_pasetokey=fast_paseto.generate_symmetric_key() # Serialize to PASERK formatpaserk=fast_paseto.to_paserk_local(key) # => "k4.local.AAAA..."# Deserialize backkey_type, key_bytes=fast_paseto.from_paserk(paserk)Key IDs

# Generate deterministic key identifierslid=fast_paseto.generate_lid(symmetric_key) # k4.lid.XXXX...sid=fast_paseto.generate_sid(secret_key) # k4.sid.XXXX...pid=fast_paseto.generate_pid(public_key) # k4.pid.XXXX...Key Wrapping

# Wrap a key with another keywrapping_key=fast_paseto.generate_symmetric_key() wrapped=fast_paseto.local_wrap(key, wrapping_key) # Unwraporiginal_key=fast_paseto.local_unwrap(wrapped, wrapping_key)Password-Protected Keys

# Encrypt a key with a password (uses Argon2id)encrypted=fast_paseto.local_pw_encrypt(key, "my-secure-password") # Decrypt with passworddecrypted_key=fast_paseto.local_pw_decrypt(encrypted, "my-secure-password")Loading PEM Keys

importfast_paseto# Load Ed25519 private key from PEMwithopen("private_key.pem") asf: secret_key=fast_paseto.ed25519_from_pem(f.read()) # Load Ed25519 public key from PEMwithopen("public_key.pem") asf: public_key=fast_paseto.ed25519_public_from_pem(f.read())Custom Serialization

importmsgpackimportfast_pasetoclassMsgPackSerializer: defdumps(self, obj): returnmsgpack.packb(obj) classMsgPackDeserializer: defloads(self, data): returnmsgpack.unpackb(data) token=fast_paseto.encode( key=key, payload={"data": [1, 2, 3]}, serializer=MsgPackSerializer(), ) decoded=fast_paseto.decode( token, key, deserializer=MsgPackDeserializer(), )Footers and Implicit Assertions

# Add a footer (included in token, not encrypted)token=fast_paseto.encode( key=key, payload={"user_id": 123}, footer={"kid": "key-001"}, ) # Add implicit assertion (not in token, must match on decode)token=fast_paseto.encode( key=key, payload={"user_id": 123}, implicit_assertion=b"context-data", ) decoded=fast_paseto.decode( token, key, implicit_assertion=b"context-data", # Must match! )Error Handling

fromfast_pasetoimport ( PasetoError, PasetoKeyError, PasetoCryptoError, PasetoExpiredError, PasetoNotYetValidError, ) try: decoded=fast_paseto.decode(token, key) exceptPasetoExpiredError: print("Token has expired") exceptPasetoKeyError: print("Invalid key") exceptPasetoCryptoError: print("Decryption/verification failed") exceptPasetoErrorase: print(f"PASETO error: {e}")Key Lengths

Key TypeLengthToken TypeSymmetric32 byteslocalEd25519 Secret64 bytespublic (signing)Ed25519 Public32 bytespublic (verification)Development

Prerequisites

Python 3.11+

Rust (2024 edition)

uv

for Python environment management

maturin

for building

Setup

uv venv .venv\Scripts\activate # Windows# source .venv/bin/activate # Linux/macOS maturin developRunning Tests

# Rust tests cargo test# Python tests (requires maturin develop first) pytest # All checks cargo fmt && cargo clippy && ruff format .&& ruff check .&& cargo test&& pytestLicense

MIT

Star History

Star History Chart