๐ pq-jwt
Post-Quantum JWT - A quantum-resistant JWT implementation using ML-DSA (Module-Lattice Digital Signature Algorithm) signatures.
๐ก๏ธ Future-proof your authentication - Protect your JWTs against quantum computer attacks with NIST-standardized post-quantum cryptography.
๐ Features
- โ Quantum-Resistant - Uses ML-DSA (FIPS 204) signatures that remain secure even against quantum attacks
- โ Multiple Security Levels - Choose from ML-DSA-44, ML-DSA-65, or ML-DSA-87 based on your needs
- โ Standards Compliant - JWT format following RFC 7519
- โ Flexible API - Simple functions and advanced Builder patterns
- โ Key Management - Built-in support for saving keys to files
- โ
Key Rotation - Support for
kid(Key ID) in JWT headers - โ Zero Dependencies Bloat - Minimal, focused dependencies
- โ Easy to Use - Simple, intuitive API
- โ Well Tested - Comprehensive test coverage with unit and integration tests
- โ Pure Rust - Memory-safe implementation with no unsafe code
๐ Feature Matrix
JWT Operations & Claims Support
Operations
- โ Sign
- โ Verify
- โ Key Generation
- โ Key Rotation (kid)
Standard Claims
- โ iss (issuer)
- โ exp (expiration)
- โ iat (issued at)
- โ sub (subject)
- โ aud (audience)
- โ nbf (not before)
- โ jti (JWT ID)
Claim Validation
- โ iss check
- โ exp check (always)
- โ iat check
- โ sub check
- โ aud check
- โ nbf check
- โ jti (REQUIRED, auto-generated UUID v7)
- โ typ check (always "JWT")
- โ Leeway support
Custom Claims
- โ Arbitrary JSON data
- โ Type-safe deserialization
Post-Quantum Algorithms
| Algorithm | NIST Level | Status | Use Case |
|---|---|---|---|
| ML-DSA-44 | Category 2 | โ Supported | IoT, constrained devices |
| ML-DSA-65 | Category 3 | โ Supported (Recommended) | General purpose applications |
| ML-DSA-87 | Category 5 | โ Supported | High-security requirements |
Note: This library does NOT support classical algorithms (HS256, RS256, ES256, PS256, EdDSA) as they are vulnerable to quantum attacks. For classical JWT algorithms, use other libraries like jsonwebtoken.
๐ฆ Installation
Add this to your Cargo.toml:
[]
= "0.1.0"
๐ Quick Start
use ;
use ;
๐ Usage Examples
Basic Authentication Token (Simple API)
use ;
use ;
// Generate long-term keypair (store securely!)
let = generate_keypair?;
// Create user session token
let now = now.duration_since.unwrap.as_secs;
let = sign?;
// Later: verify the token
let payload = verify?;
println!;
Advanced Authentication Token (Builder API with Custom Claims)
use Builder;
use MlDsaAlgo;
use ;
use json;
let = generate_keypair?;
let now = now.duration_since.unwrap.as_secs;
// Create signer with all standard claims and custom data
let signer = new
.algorithm
.private_key
.issuer
.expiration
.subject
.audience
.custom_claims
.build?;
let = signer.sign?;
// Verify
let payload = verify?;
println!;
Generate and Save Keys to File
use Builder;
use MlDsaAlgo;
// Generate and save to default location (keys/)
let = new
.algorithm
.save_to_file
.generate?;
// Or save to custom location
let = new
.algorithm
.save_to_file_at
.generate?;
// Files created:
// - ml_dsa_65_1704139200_private.key
// - ml_dsa_65_1704139200_public.key (derived from private key)
Load Keys from File
use ;
use MlDsaAlgo;
// Load from default location (keys/) - picks latest by timestamp
let = from
.file?;
// Load from custom location
let = from
.file_at?;
// Public key is automatically derived from private key
assert_eq!;
Load or Generate Keys (Automatic Fallback)
use ;
use MlDsaAlgo;
// Try to load existing key, generate if missing
let = load_or_generate
.file?;
match source
// Custom location
let = load_or_generate
.file_at?;
// Perfect for server initialization - always has a valid key!
Load Keys from String (Database/Environment)
use ;
use MlDsaAlgo;
// Load private key from database or environment
let private_key_from_db = var?;
// Derive public key from private key
let = from
.private_key_str?;
assert_eq!;
// Use the keys for signing/verification
Key Rotation with Key ID (kid)
The Key ID (kid) is automatically generated from the public key using SHA-256, ensuring consistent identification across key rotations.
use Builder as SignerBuilder;
use Builder as VerifierBuilder;
use ;
use ;
let now = now.duration_since.unwrap.as_secs;
// Generate keypair
let = generate_keypair?;
// Create signer (kid is auto-generated from public key)
let signer = new
.algorithm
.private_key
.issuer
.expiration
.build?;
let = signer.sign?;
// Verify (kid from JWT header can be used to identify which key to use)
let verifier = new
.public_key
.issuer
.build?;
let payload = verifier.verify?;
Reusable Signer and Verifier
use Builder as SignerBuilder;
use Builder as VerifierBuilder;
use MlDsaAlgo;
use ;
let now = now.duration_since.unwrap.as_secs;
// Create once, use many times
let signer = new
.algorithm
.private_key
.issuer
.expiration
.build?;
// Sign (no parameters needed - uses configured claims)
let = signer.sign?;
let = signer.sign?;
let = signer.sign?;
// Create reusable verifier
let verifier = new
.public_key
.issuer
.build?;
// Verify multiple tokens
for jwt in
API Authentication
use ;
use Builder;
use verifier;
use ;
use json;
let now = now.duration_since.unwrap.as_secs;
// Server initialization
let =
generate_keypair?;
// Issue API token with custom claims
let signer = new
.algorithm
.private_key
.issuer
.expiration // 24 hours
.subject
.custom_claims
.build?;
let = signer.sign?;
// Client sends: Authorization: Bearer <api_token>
// Server verifies:
match verify
Custom Payload with Type Safety
use Builder;
use ;
use ;
use json;
use ;
let now = now.duration_since.unwrap.as_secs;
let custom_data = CustomData ;
// Build JWT with standard claims + custom data
let signer = new
.algorithm
.private_key
.issuer
.expiration
.subject
.custom_claims
.build?;
let = signer.sign?;
// Later... verify and extract
let verified = verify?;
let payload: Value = from_str?;
let custom: CustomData = from_value?;
println!;
๐ Security Levels
Choose the right security level for your use case:
| Variant | NIST Level | Signature Size | Key Gen | Sign | Verify | Use Case |
|---|---|---|---|---|---|---|
| ML-DSA-44 | Category 2 | ~2.4 KB | ~200 ยตs | ~460 ยตs | ~140 ยตs | IoT devices, low-power systems |
| ML-DSA-65 | Category 3 | ~3.3 KB | ~350 ยตs | ~930 ยตs | ~220 ยตs | Recommended for most applications |
| ML-DSA-87 | Category 5 | ~4.6 KB | ~440 ยตs | ~550 ยตs | ~315 ยตs | High-security requirements, long-term secrets |
Security Level Comparison
- NIST Category 2 โ AES-128 security
- NIST Category 3 โ AES-192 security (Recommended)
- NIST Category 5 โ AES-256 security
Choosing an Algorithm
use MlDsaAlgo;
// For most web applications (recommended)
let algo = Dsa65;
// For IoT or bandwidth-constrained environments
let algo = Dsa44;
// For maximum security (government, financial)
let algo = Dsa87;
๐ฏ Performance
Benchmarked on Apple M1 Pro (release build):
ML-DSA-65 Performance:
โโ Key Generation: ~350 ยตs (2,857 ops/sec)
โโ Signing: ~930 ยตs (1,075 ops/sec)
โโ Verification: ~220 ยตs (4,545 ops/sec)
Token Size: ~4.5 KB (vs ~300 bytes for ECDSA)
Performance Tips
- Cache Keys: Generate keypairs once and reuse them
- Pre-verify Format: Check JWT structure before cryptographic verification
- Use ML-DSA-44: If bandwidth is critical and security level 2 is acceptable
- Batch Operations: Verify multiple tokens in parallel for better throughput
๐ Size Comparison
| Algorithm | Private Key | Public Key | Signature | Total JWT |
|---|---|---|---|---|
| ECDSA P-256 | 32 bytes | 64 bytes | 64 bytes | ~300 bytes |
| RSA-2048 | 1.2 KB | 270 bytes | 256 bytes | ~800 bytes |
| ML-DSA-44 | 2.5 KB | 1.3 KB | 2.4 KB | ~3.3 KB |
| ML-DSA-65 | 4 KB | 1.9 KB | 3.3 KB | ~4.5 KB |
| ML-DSA-87 | 4.9 KB | 2.6 KB | 4.6 KB | ~6.2 KB |
โ ๏ธ Trade-off: Post-quantum signatures are larger, but provide quantum resistance. The size increase is the price of security against quantum attacks.
๐ ๏ธ API Reference
Simple API (Convenience Functions)
generate_keypair(algo: MlDsaAlgo) -> Result<(String, String), String>
Generates a new keypair for the specified algorithm.
Returns: (private_key_hex, public_key_hex)
let = generate_keypair?;
sign(algo: MlDsaAlgo, iss: &str, exp: u64, private_key_hex: &str) -> Result<(String, String, String), String>
Signs JWT claims and returns a JWT with the public key and JWT ID.
Parameters:
algo- ML-DSA algorithm variantiss- Issuer (REQUIRED)exp- Expiration time as Unix timestamp in seconds (REQUIRED)private_key_hex- Hex-encoded private key
Returns: (jwt, public_key_hex, jti)
jwt- The signed JWT stringpublic_key_hex- Hex-encoded public key (for verification)jti- JWT ID (UUID v7 format) - useful for session management
Note: The iat (issued at) claim defaults to the current time. The jti is automatically generated as a UUID v7.
use ;
let now = now.duration_since.unwrap.as_secs;
let = sign?;
println!;
verify(jwt: &str, public_key_hex: &str, expected_issuer: &str) -> Result<String, String>
Verifies a JWT and returns the decoded payload.
Parameters:
jwt- The JWT string to verifypublic_key_hex- Hex-encoded public keyexpected_issuer- Expected issuer that must match the JWT'sissclaim
Returns: payload if valid, error otherwise
let payload = verify?;
Builder API (Advanced)
keygen::Builder
Generation Methods:
Builder::new()- Create builder for generation.algorithm(MlDsaAlgo)- Set the algorithm variant.save_to_file()- Save keys to default location (keys/).save_to_file_at(path)- Save keys to custom path.generate()- Generate keypair (and save if configured)- Returns:
(private_key_hex, public_key_hex)
Loading Methods:
Builder::from(algo)- Create builder for loading (error if missing)Builder::load_or_generate(algo)- Load or auto-generate if missing.file()- Load from default location (keys/), picks latest by timestamp.file_at(path)- Load from custom path, picks latest by timestamp.private_key_str(hex)- Load from hex string, derives public key- Returns:
(private_key_hex, public_key_hex, KeySource)
use ;
// Generate and save
let = new
.algorithm
.save_to_file_at
.generate?;
// Load from file (error if missing)
let = from
.file_at?;
// Load or generate (auto-fallback)
let = load_or_generate
.file_at?;
// Load from string
let = from
.private_key_str?;
signer::Builder
Configuration Methods:
.algorithm(MlDsaAlgo)- Set the algorithm variant (REQUIRED).private_key(&str)- Set the private key (REQUIRED)
Standard JWT Claims Methods:
.issuer(&str)- Setissclaim (REQUIRED).expiration(u64)- Setexpclaim as Unix timestamp (REQUIRED).subject(&str)- Setsubclaim (optional).audience(&str)- Setaudclaim (optional).issued_at(Option<u64>)- Setiatclaim, defaults to signing time if not set (optional).not_before(u64)- Setnbfclaim as Unix timestamp (optional).jwt_id(&str)- Override the auto-generatedjticlaim (UUID v7 by default).custom_claims(serde_json::Value)- Add custom claims (optional)
Build Method:
.build()- Build Signer instance, returnsResult<Signer, String>
Signer Methods:
.sign()- Sign the configured claims, returnsResult<(String, String, String), String>as(jwt, public_key, jti)
Notes:
- The Key ID (kid) is automatically generated from the public key using SHA-256
- The JWT ID (jti) is automatically generated as UUID v7 (time-ordered) if not explicitly set
- The
iat(issued at) defaults to the current signing time if not explicitly set - Claims are validated before signing (
exp > iat,nbf <= iat) - Custom claims that duplicate standard claim keys are ignored
use Builder;
use ;
use json;
let now = now.duration_since.unwrap.as_secs;
let signer = new
.algorithm
.private_key
.issuer
.expiration
.subject
.custom_claims
.build?;
let = signer.sign?;
verifier::Builder
Required Configuration:
.public_key(&str)- Set the public key (REQUIRED).issuer(&str)- Set expected issuer for validation (REQUIRED)
Optional Claim Validations:
.audience(&str)- Set expected audience for validation.subject(&str)- Set expected subject for validation.leeway(u64)- Set time leeway in seconds for clock skew (default: 0)
Build Method:
.build()- Build Verifier instance, returnsResult<Verifier, String>
Verifier Methods:
.verify(&str)- Verify JWT and return payload, returnsResult<String, String>
Automatic Validations (Always Performed):
- โ Signature verification (cryptographic)
- โ
Expiration check (
expmust be in the future) - โ
Issuer matching (
issclaim must match expected issuer)
Optional Validations (Configured via Builder):
- Expected audience matching (if
.audience()is called) - Expected subject matching (if
.subject()is called) - Not before time (
nbfif present in token)
use Builder;
// Basic verification - issuer is REQUIRED
let verifier = new
.public_key
.issuer // REQUIRED
.build?;
let payload = verifier.verify?;
// Advanced verification with additional optional validations
let verifier = new
.public_key
.issuer // REQUIRED
.audience // Optional: validate audience matches
.subject // Optional: validate subject matches
.leeway // Optional: allow 60s clock skew
.build?;
let payload = verifier.verify?;
Enums
MlDsaAlgo
Available algorithm variants:
MlDsaAlgo::Dsa44- NIST Category 2MlDsaAlgo::Dsa65- NIST Category 3 (Recommended)MlDsaAlgo::Dsa87- NIST Category 5
Traits: Debug, Clone, Copy, PartialEq, Eq
KeySource
Indicates the source of a keypair when using load_or_generate:
KeySource::Loaded- Successfully loaded existing key from file or stringKeySource::Generated- Generated new key (file was missing or corrupt)
Traits: Debug, Clone, PartialEq, Eq
use ;
let = load_or_generate
.file?;
match source
๐ Migration Guide
From v0.1.x to v0.2.x
Breaking Change: The sign() function signature has changed to require iss and exp parameters.
Old API (v0.1.x):
let payload = r#"{"sub": "user123", "exp": 1735689600}"#;
let = sign?;
New API (v0.2.x):
use ;
let now = now.duration_since.unwrap.as_secs;
let = sign?;
// jti is now returned - use it for session management
For more complex claims, use the Builder API:
use Builder;
use json;
let signer = new
.algorithm
.private_key
.issuer
.expiration
.subject
.custom_claims
.build?;
let = signer.sign?;
New Features Available
Key File Management:
// Old way - manual file handling
let = generate_keypair?;
write?;
write?;
// New way - built-in
use Builder;
let = new
.algorithm
.save_to_file
.generate?;
Key Rotation:
// New: kid is automatically generated for key rotation
use Builder;
use ;
let now = now.duration_since.unwrap.as_secs;
let signer = new
.algorithm
.private_key
.issuer
.expiration
.build?;
// The kid in the JWT header can be used to identify which public key to use
Reusable Instances:
use ;
let now = now.duration_since.unwrap.as_secs;
// New: Create once, use multiple times
let signer = new
.algorithm
.private_key
.issuer
.expiration
.build?;
// Sign (no parameters needed - uses configured claims)
let = signer.sign?;
let = signer.sign?;
JWT Claims Validation:
// New: Automatic validation of JWT claims
// - exp > iat (expiration must be after issued at)
// - nbf <= iat (not before must be before or equal to issued at)
// Validation happens automatically when calling sign()
๐ Security Considerations
Key Management
- Never commit private keys to version control
- Rotate keys regularly (every 90 days recommended)
- Use environment variables or secret management systems
- Store keys encrypted at rest
- Use file storage with proper permissions (0600 for private keys)
// โ Good - Environment variables
let private_key = var?;
// โ Good - Secure file storage
use Builder;
let = new
.algorithm
.save_to_file_at
.generate?;
// โ Bad - Hardcoded
let private_key = "4343e9e24838dbd8..."; // Never do this
Key Rotation Strategy
use ;
let now = now.duration_since.unwrap.as_secs;
// Step 1: Generate new keypair (kid will be auto-generated)
let = new
.algorithm
.save_to_file_at
.generate?;
// Step 2: Create new signer (kid is auto-generated from public key)
let signer = new
.algorithm
.private_key
.issuer
.expiration
.build?;
// Step 3: Store the public key with its auto-generated kid for verification
// You can extract the kid from a signed JWT's header to identify which key to use
// Step 4: Keep old public keys for verification during transition period
// Step 5: Gradually phase out old keys
Token Best Practices
- Always include expiration (
expclaim) - Use short lifetimes for sensitive operations (15 min - 1 hour)
- Implement token revocation if needed
- Validate claims after verification
- Use HTTPS for token transmission
Example with Expiration
use ;
let now = now.duration_since?.as_secs;
// Sign with issuer and expiration
let = sign?;
๐ช Session Management for Large JWTs
Post-quantum JWTs are significantly larger (3-6 KB) than classical JWTs (~300 bytes), making them impractical to store in cookies due to browser size limits (~4 KB per cookie). Here's a recommended pattern for managing sessions:
Cookie + Server-Side Storage Pattern
Instead of storing the entire JWT in a cookie, store only the jti (JWT ID) and keep the full JWT server-side:
use ;
use ;
// 1. Generate and sign JWT
let = generate_keypair?;
let now = now.duration_since.unwrap.as_secs;
let = sign?;
// 2. Store JWT server-side (Redis, database, etc.)
// redis.set(jti, jwt, expiry=3600)
// OR
// database.insert(jti, jwt, expires_at)
// 3. Store only the jti in cookie (36 bytes as UUID)
// Set-Cookie: session_id={jti}; HttpOnly; Secure; SameSite=Strict
// 4. On subsequent requests, retrieve JWT using jti
// let jwt = redis.get(session_id)?;
// let payload = verify(&jwt, &public_key, "https://myapp.com")?;
Why UUID v7 for JTI?
This library uses UUID v7 (time-ordered) for jti, which provides several benefits:
- Sortable: UUIDs are time-ordered, making them efficient for database indexing
- K-sorted: Improves database performance by reducing index fragmentation
- Timestamp component: Can extract creation time from the UUID
- Collision-resistant: Cryptographically random with timestamp prefix
Implementation Considerations
Storage Backend Options:
// Option 1: Redis (recommended for high-performance)
// - TTL automatically expires sessions
// - In-memory speed for lookups
redis.setex?;
// Option 2: Database (PostgreSQL, MySQL)
// - Persistent storage
// - Can query by user_id, created_at, etc.
db.execute?;
// Option 3: Distributed cache (Memcached)
// - Multi-server support
// - Automatic eviction
cache.set?;
Security Best Practices:
-
Set appropriate cookie attributes:
Set-Cookie: session_id={jti}; HttpOnly; // Prevent XSS access Secure; // HTTPS only SameSite=Strict; // CSRF protection Max-Age=3600 // Match JWT expiration -
Implement TTL matching JWT expiration:
- Server-side storage TTL should match JWT
expclaim - Prevents storage of expired tokens
- Server-side storage TTL should match JWT
-
Rate limit lookups by jti:
- Prevent enumeration attacks
- Limit requests per IP/user
-
Clean up expired sessions:
// Periodic cleanup for database-backed storage db.execute?;
Example: Full Web Application Flow
// Login endpoint
async
// Protected endpoint
async
// Logout endpoint
async
Size Comparison: Cookie Storage
| Approach | Cookie Size | Storage Location |
|---|---|---|
| Classical JWT in cookie | ~300 bytes | Client |
| PQ JWT in cookie | ~4.5 KB โ (exceeds limits) | Client |
| JTI in cookie | 36 bytes โ | Client (jti) + Server (JWT) |
๐ค Why Post-Quantum?
The Quantum Threat
Quantum computers, when fully developed, will break current cryptographic systems:
- RSA - Vulnerable to Shor's algorithm
- ECDSA - Vulnerable to Shor's algorithm
- Diffie-Hellman - Vulnerable to quantum attacks
Timeline
- 2023: NIST standardizes post-quantum algorithms (ML-DSA = FIPS 204)
- 2025-2030: Quantum computers may break RSA-2048
- 2030+: All systems must use post-quantum crypto
"Harvest Now, Decrypt Later"
Attackers can:
- Intercept and store encrypted data today
- Wait for quantum computers to become available
- Decrypt the data retroactively
Solution: Start using post-quantum crypto NOW to protect long-term secrets.
๐ Comparison with Classical JWT
| Feature | pq-jwt (ML-DSA) | Classical (ECDSA) |
|---|---|---|
| Quantum Resistant | โ Yes | โ No |
| NIST Standardized | โ FIPS 204 | โ FIPS 186 |
| Token Size | 3-6 KB | ~300 bytes |
| Sign Speed | ~0.5-1 ms | ~0.05-0.1 ms |
| Verify Speed | ~0.2-0.3 ms | ~0.1-0.2 ms |
| Security Level | 128-256 bit | 128-256 bit |
| Future Proof | โ Yes | โ Vulnerable to quantum |
๐ง Integration Examples
With Actix Web
use ;
use ;
async
With Axum
use ;
use verify;
async
๐งช Testing
Run the test suite:
# Run all tests
# Run with output
# Run specific test
# Run benchmarks
๐ Further Reading
- NIST FIPS 204 - ML-DSA Standard
- Post-Quantum Cryptography FAQ
- JWT RFC 7519
- NIST Post-Quantum Standards
๐ค Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Development Setup
๐ License
This project is dual-licensed under:
- MIT License (LICENSE-MIT or http://opensource.org/licenses/MIT)
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
You may choose either license for your use.
๐จโ๐ป Author
MKSingh (@MKSingh_Dev)
โญ Star History
If you find this project useful, please consider giving it a star! โญ
Made with โค๏ธ for a quantum-safe future