๐ 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
๐ฆ 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>
Signs JWT claims and returns a JWT with the public key.
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)
Note: The iat (issued at) claim defaults to the current time.
use ;
let now = now.duration_since.unwrap.as_secs;
let = sign?;
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)- Setjticlaim (optional).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>
Notes:
- The Key ID (kid) is automatically generated from the public key using SHA-256
- 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?;
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?;
๐ค 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