5 releases
Uses new Rust 2024
| 0.2.2 | Nov 8, 2025 |
|---|---|
| 0.2.1 | Nov 3, 2025 |
| 0.2.0 | Nov 3, 2025 |
| 0.1.2 | Nov 2, 2025 |
| 0.1.1 | Nov 2, 2025 |
#301 in Cryptography
140KB
2K
SLoC
๐ 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
Standard Claims
|
Claim Validation
Custom Claims
|
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:
[dependencies]
pq-jwt = "0.1.0"
๐ Quick Start
use pq_jwt::{generate_keypair, sign, verify, MlDsaAlgo};
use std::time::{SystemTime, UNIX_EPOCH};
fn main() -> Result<(), String> {
// 1. Generate a keypair
let (private_key, public_key) = generate_keypair(MlDsaAlgo::Dsa65)?;
// 2. Create and sign a JWT with issuer and expiration
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
let (jwt, _, jti) = sign(
MlDsaAlgo::Dsa65,
"https://myapp.com", // Issuer
now + 3600, // Expires in 1 hour
&private_key
)?;
println!("JWT: {}", jwt);
println!("JWT ID (jti): {}", jti);
// 3. Verify the JWT
let verified_payload = verify(&jwt, &public_key, "https://myapp.com")?;
println!("Verified payload: {}", verified_payload);
println!("โ JWT verified successfully!");
Ok(())
}
๐ Usage Examples
Basic Authentication Token (Simple API)
use pq_jwt::{generate_keypair, sign, verify, MlDsaAlgo};
use std::time::{SystemTime, UNIX_EPOCH};
// Generate long-term keypair (store securely!)
let (private_key, public_key) = generate_keypair(MlDsaAlgo::Dsa65)?;
// Create user session token
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
let (jwt, _, jti) = sign(
MlDsaAlgo::Dsa65,
"https://myapp.com", // Issuer
now + 3600, // Expires in 1 hour
&private_key
)?;
// Later: verify the token
let payload = verify(&jwt, &public_key, "https://myapp.com")?;
println!("Authenticated user: {}", payload);
Advanced Authentication Token (Builder API with Custom Claims)
use pq_jwt::signer::Builder;
use pq_jwt::MlDsaAlgo;
use std::time::{SystemTime, UNIX_EPOCH};
use serde_json::json;
let (private_key, public_key) = generate_keypair(MlDsaAlgo::Dsa65)?;
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
// Create signer with all standard claims and custom data
let signer = Builder::new()
.algorithm(MlDsaAlgo::Dsa65)
.private_key(&private_key)
.issuer("https://myapp.com")
.expiration(now + 3600)
.subject("user123")
.audience("https://api.myapp.com")
.custom_claims(json!({
"name": "Alice",
"role": "admin",
"permissions": ["read", "write", "delete"]
}))
.build()?;
let (jwt, _, jti) = signer.sign()?;
// Verify
let payload = verify(&jwt, &public_key, "https://myapp.com")?;
println!("Token payload: {}", payload);
Generate and Save Keys to File
use pq_jwt::keygen::Builder;
use pq_jwt::MlDsaAlgo;
// Generate and save to default location (keys/)
let (private_key, public_key) = Builder::new()
.algorithm(MlDsaAlgo::Dsa65)
.save_to_file()
.generate()?;
// Or save to custom location
let (private_key, public_key) = Builder::new()
.algorithm(MlDsaAlgo::Dsa65)
.save_to_file_at("./my-secure-keys")
.generate()?;
// Files created:
// - ml_dsa_65_1704139200_private.key
// - ml_dsa_65_1704139200_public.key (derived from private key)
Load Keys from File
use pq_jwt::keygen::{Builder, KeySource};
use pq_jwt::MlDsaAlgo;
// Load from default location (keys/) - picks latest by timestamp
let (private_key, public_key, source) = Builder::from(MlDsaAlgo::Dsa65)
.file()?;
// Load from custom location
let (private_key, public_key, source) = Builder::from(MlDsaAlgo::Dsa65)
.file_at("./my-secure-keys")?;
// Public key is automatically derived from private key
assert_eq!(source, KeySource::Loaded);
Load or Generate Keys (Automatic Fallback)
use pq_jwt::keygen::{Builder, KeySource};
use pq_jwt::MlDsaAlgo;
// Try to load existing key, generate if missing
let (private_key, public_key, source) = Builder::load_or_generate(MlDsaAlgo::Dsa65)
.file()?;
match source {
KeySource::Loaded => println!("Using existing key"),
KeySource::Generated => println!("Generated new key and saved"),
}
// Custom location
let (private_key, public_key, source) = Builder::load_or_generate(MlDsaAlgo::Dsa65)
.file_at("./my-secure-keys")?;
// Perfect for server initialization - always has a valid key!
Load Keys from String (Database/Environment)
use pq_jwt::keygen::{Builder, KeySource};
use pq_jwt::MlDsaAlgo;
// Load private key from database or environment
let private_key_from_db = std::env::var("JWT_PRIVATE_KEY")?;
// Derive public key from private key
let (private_key, public_key, source) = Builder::from(MlDsaAlgo::Dsa65)
.private_key_str(&private_key_from_db)?;
assert_eq!(source, KeySource::Loaded);
// 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 pq_jwt::signer::Builder as SignerBuilder;
use pq_jwt::verifier::Builder as VerifierBuilder;
use pq_jwt::{generate_keypair, MlDsaAlgo};
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
// Generate keypair
let (priv_key_v2, pub_key_v2) = generate_keypair(MlDsaAlgo::Dsa65)?;
// Create signer (kid is auto-generated from public key)
let signer = SignerBuilder::new()
.algorithm(MlDsaAlgo::Dsa65)
.private_key(&priv_key_v2)
.issuer("https://myapp.com")
.expiration(now + 3600)
.build()?;
let (jwt, _, jti) = signer.sign()?;
// Verify (kid from JWT header can be used to identify which key to use)
let verifier = VerifierBuilder::new()
.public_key(&pub_key_v2)
.issuer("https://myapp.com")
.build()?;
let payload = verifier.verify(&jwt)?;
Reusable Signer and Verifier
use pq_jwt::signer::Builder as SignerBuilder;
use pq_jwt::verifier::Builder as VerifierBuilder;
use pq_jwt::MlDsaAlgo;
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
// Create once, use many times
let signer = SignerBuilder::new()
.algorithm(MlDsaAlgo::Dsa65)
.private_key(&private_key)
.issuer("https://myapp.com")
.expiration(now + 3600)
.build()?;
// Sign (no parameters needed - uses configured claims)
let (jwt1, _, jti1) = signer.sign()?;
let (jwt2, _, jti2) = signer.sign()?;
let (jwt3, _, jti3) = signer.sign()?;
// Create reusable verifier
let verifier = VerifierBuilder::new()
.public_key(&public_key)
.issuer("https://myapp.com")
.build()?;
// Verify multiple tokens
for jwt in [jwt1, jwt2, jwt3] {
match verifier.verify(&jwt) {
Ok(payload) => println!("Valid: {}", payload),
Err(e) => println!("Invalid: {}", e),
}
}
API Authentication
use pq_jwt::{generate_keypair, MlDsaAlgo};
use pq_jwt::signer::Builder;
use pq_jwt::verifier;
use std::time::{SystemTime, UNIX_EPOCH};
use serde_json::json;
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
// Server initialization
let (server_private_key, server_public_key) =
generate_keypair(MlDsaAlgo::Dsa65)?;
// Issue API token with custom claims
let signer = Builder::new()
.algorithm(MlDsaAlgo::Dsa65)
.private_key(&server_private_key)
.issuer("https://api.myapp.com")
.expiration(now + 86400) // 24 hours
.subject("ak_live_123456")
.custom_claims(json!({
"scope": ["read", "write"],
"rate_limit": 1000
}))
.build()?;
let (api_token, _, jti) = signer.sign()?;
// Client sends: Authorization: Bearer <api_token>
// Server verifies:
match verifier::verify(&api_token, &server_public_key, "https://api.myapp.com") {
Ok(claims) => println!("Valid API token: {}", claims),
Err(e) => println!("Invalid token: {}", e),
}
Custom Payload with Type Safety
use pq_jwt::signer::Builder;
use pq_jwt::{verify, MlDsaAlgo};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Serialize, Deserialize)]
struct CustomData {
user_id: u64,
role: String,
permissions: Vec<String>,
}
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
let custom_data = CustomData {
user_id: 42,
role: "admin".to_string(),
permissions: vec!["read".to_string(), "write".to_string()],
};
// Build JWT with standard claims + custom data
let signer = Builder::new()
.algorithm(MlDsaAlgo::Dsa65)
.private_key(&private_key)
.issuer("https://myapp.com")
.expiration(now + 3600)
.subject("user_42")
.custom_claims(serde_json::to_value(&custom_data)?)
.build()?;
let (jwt, _, jti) = signer.sign()?;
// Later... verify and extract
let verified = verify(&jwt, &public_key, "https://myapp.com")?;
let payload: serde_json::Value = serde_json::from_str(&verified)?;
let custom: CustomData = serde_json::from_value(payload)?;
println!("User {} has role: {}", custom.user_id, custom.role);
๐ 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 pq_jwt::MlDsaAlgo;
// For most web applications (recommended)
let algo = MlDsaAlgo::Dsa65;
// For IoT or bandwidth-constrained environments
let algo = MlDsaAlgo::Dsa44;
// For maximum security (government, financial)
let algo = MlDsaAlgo::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 (private_key, public_key) = generate_keypair(MlDsaAlgo::Dsa65)?;
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 std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
let (jwt, pub_key, jti) = sign(
MlDsaAlgo::Dsa65,
"https://myapp.com",
now + 3600,
&private_key
)?;
println!("JWT ID for session tracking: {}", jti);
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(&jwt, &public_key, "https://myapp.com")?;
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 pq_jwt::keygen::{Builder, KeySource};
// Generate and save
let (priv_key, pub_key) = Builder::new()
.algorithm(MlDsaAlgo::Dsa65)
.save_to_file_at("./secure-keys")
.generate()?;
// Load from file (error if missing)
let (priv_key, pub_key, source) = Builder::from(MlDsaAlgo::Dsa65)
.file_at("./secure-keys")?;
// Load or generate (auto-fallback)
let (priv_key, pub_key, source) = Builder::load_or_generate(MlDsaAlgo::Dsa65)
.file_at("./secure-keys")?;
// Load from string
let (priv_key, pub_key, source) = Builder::from(MlDsaAlgo::Dsa65)
.private_key_str(&hex_string)?;
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 pq_jwt::signer::Builder;
use std::time::{SystemTime, UNIX_EPOCH};
use serde_json::json;
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
let signer = Builder::new()
.algorithm(MlDsaAlgo::Dsa65)
.private_key(&priv_key)
.issuer("https://myapp.com")
.expiration(now + 3600)
.subject("user@example.com")
.custom_claims(json!({
"role": "admin",
"permissions": ["read", "write"]
}))
.build()?;
let (jwt, pub_key, jti) = 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 pq_jwt::verifier::Builder;
// Basic verification - issuer is REQUIRED
let verifier = Builder::new()
.public_key(&pub_key)
.issuer("https://myapp.com") // REQUIRED
.build()?;
let payload = verifier.verify(&jwt)?;
// Advanced verification with additional optional validations
let verifier = Builder::new()
.public_key(&pub_key)
.issuer("https://myapp.com") // REQUIRED
.audience("https://api.myapp.com") // Optional: validate audience matches
.subject("user@example.com") // Optional: validate subject matches
.leeway(60) // Optional: allow 60s clock skew
.build()?;
let payload = verifier.verify(&jwt)?;
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 pq_jwt::keygen::{Builder, KeySource};
let (priv_key, pub_key, source) = Builder::load_or_generate(MlDsaAlgo::Dsa65)
.file()?;
match source {
KeySource::Loaded => println!("Reusing existing key"),
KeySource::Generated => println!("Created new key"),
}
๐ 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 (jwt, _) = sign(MlDsaAlgo::Dsa65, payload, &priv_key)?;
New API (v0.2.x):
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
let (jwt, _, jti) = sign(
MlDsaAlgo::Dsa65,
"https://myapp.com", // issuer (required)
now + 3600, // expiration (required)
&priv_key
)?;
// jti is now returned - use it for session management
For more complex claims, use the Builder API:
use pq_jwt::signer::Builder;
use serde_json::json;
let signer = Builder::new()
.algorithm(MlDsaAlgo::Dsa65)
.private_key(&priv_key)
.issuer("https://myapp.com")
.expiration(now + 3600)
.subject("user123")
.custom_claims(json!({
"role": "admin",
"permissions": ["read", "write"]
}))
.build()?;
let (jwt, _, jti) = signer.sign()?;
New Features Available
Key File Management:
// Old way - manual file handling
let (priv_key, pub_key) = generate_keypair(MlDsaAlgo::Dsa65)?;
std::fs::write("private.key", &priv_key)?;
std::fs::write("public.key", &pub_key)?;
// New way - built-in
use pq_jwt::keygen::Builder;
let (priv_key, pub_key) = Builder::new()
.algorithm(MlDsaAlgo::Dsa65)
.save_to_file()
.generate()?;
Key Rotation:
// New: kid is automatically generated for key rotation
use pq_jwt::signer::Builder;
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
let signer = Builder::new()
.algorithm(MlDsaAlgo::Dsa65)
.private_key(&priv_key)
.issuer("https://myapp.com")
.expiration(now + 3600)
.build()?;
// The kid in the JWT header can be used to identify which public key to use
Reusable Instances:
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
// New: Create once, use multiple times
let signer = signer::Builder::new()
.algorithm(MlDsaAlgo::Dsa65)
.private_key(&priv_key)
.issuer("https://myapp.com")
.expiration(now + 3600)
.build()?;
// Sign (no parameters needed - uses configured claims)
let (jwt1, _, jti1) = signer.sign()?;
let (jwt2, _, jti2) = 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 = std::env::var("JWT_PRIVATE_KEY")?;
// โ Good - Secure file storage
use pq_jwt::keygen::Builder;
let (priv_key, pub_key) = Builder::new()
.algorithm(MlDsaAlgo::Dsa65)
.save_to_file_at("/secure/keys")
.generate()?;
// โ Bad - Hardcoded
let private_key = "4343e9e24838dbd8..."; // Never do this
Key Rotation Strategy
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
// Step 1: Generate new keypair (kid will be auto-generated)
let (new_priv, new_pub) = keygen::Builder::new()
.algorithm(MlDsaAlgo::Dsa65)
.save_to_file_at("/keys/v3")
.generate()?;
// Step 2: Create new signer (kid is auto-generated from public key)
let signer = signer::Builder::new()
.algorithm(MlDsaAlgo::Dsa65)
.private_key(&new_priv)
.issuer("https://myapp.com")
.expiration(now + 3600)
.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 std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
// Sign with issuer and expiration
let (jwt, _, jti) = sign(
MlDsaAlgo::Dsa65,
"https://example.com", // issuer
now + 3600, // expiration (1 hour from now)
&private_key
)?;
๐ช 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 pq_jwt::{generate_keypair, sign, verify, MlDsaAlgo};
use std::time::{SystemTime, UNIX_EPOCH};
// 1. Generate and sign JWT
let (private_key, public_key) = generate_keypair(MlDsaAlgo::Dsa65)?;
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
let (jwt, _, jti) = sign(
MlDsaAlgo::Dsa65,
"https://myapp.com",
now + 3600, // 1 hour expiration
&private_key
)?;
// 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(jti, 3600, jwt)?;
// Option 2: Database (PostgreSQL, MySQL)
// - Persistent storage
// - Can query by user_id, created_at, etc.
db.execute(
"INSERT INTO sessions (jti, jwt, expires_at) VALUES ($1, $2, $3)",
&[&jti, &jwt, &(now + 3600)]
)?;
// Option 3: Distributed cache (Memcached)
// - Multi-server support
// - Automatic eviction
cache.set(jti, jwt, 3600)?;
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("DELETE FROM sessions WHERE expires_at < NOW()")?;
Example: Full Web Application Flow
// Login endpoint
async fn login(credentials: Credentials) -> Result<Response> {
// Authenticate user...
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
let (jwt, _, jti) = sign(
MlDsaAlgo::Dsa65,
"https://myapp.com",
now + 3600,
&private_key
)?;
// Store in Redis with TTL
redis.setex(&jti, 3600, &jwt).await?;
// Return cookie with jti only (36 bytes vs 4.5 KB)
Ok(Response::new()
.cookie(Cookie::build("session_id", jti)
.http_only(true)
.secure(true)
.same_site(SameSite::Strict)
.max_age(Duration::seconds(3600))
.finish()))
}
// Protected endpoint
async fn protected(session_id: String) -> Result<Response> {
// Lookup full JWT from Redis
let jwt = redis.get(&session_id).await?
.ok_or("Session not found")?;
// Verify JWT
let payload = verify(&jwt, &public_key, "https://myapp.com")?;
// Process request...
Ok(Response::new().body(payload))
}
// Logout endpoint
async fn logout(session_id: String) -> Result<Response> {
// Delete from Redis
redis.del(&session_id).await?;
Ok(Response::new()
.cookie(Cookie::build("session_id", "")
.max_age(Duration::seconds(0))
.finish()))
}
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 actix_web::{web, App, HttpRequest, HttpServer, Result};
use pq_jwt::{verify, MlDsaAlgo};
async fn protected_route(req: HttpRequest) -> Result<String> {
let auth_header = req
.headers()
.get("Authorization")
.and_then(|h| h.to_str().ok())
.ok_or_else(|| actix_web::error::ErrorUnauthorized("Missing token"))?;
let token = auth_header.strip_prefix("Bearer ")
.ok_or_else(|| actix_web::error::ErrorUnauthorized("Invalid format"))?;
let public_key = std::env::var("JWT_PUBLIC_KEY")
.map_err(|_| actix_web::error::ErrorInternalServerError("Config error"))?;
match verify(token, &public_key, "https://myapp.com") {
Ok(payload) => Ok(format!("Authenticated: {}", payload)),
Err(_) => Err(actix_web::error::ErrorUnauthorized("Invalid token")),
}
}
With Axum
use axum::{
extract::Request,
http::{StatusCode, HeaderMap},
middleware::Next,
response::Response,
};
use pq_jwt::verify;
async fn auth_middleware(
headers: HeaderMap,
request: Request,
next: Next,
) -> Result<Response, StatusCode> {
let auth_header = headers
.get("Authorization")
.and_then(|h| h.to_str().ok())
.ok_or(StatusCode::UNAUTHORIZED)?;
let token = auth_header
.strip_prefix("Bearer ")
.ok_or(StatusCode::UNAUTHORIZED)?;
let public_key = std::env::var("JWT_PUBLIC_KEY")
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
verify(token, &public_key, "https://myapp.com")
.map_err(|_| StatusCode::UNAUTHORIZED)?;
Ok(next.run(request).await)
}
๐งช Testing
Run the test suite:
# Run all tests
cargo test
# Run with output
cargo test -- --nocapture
# Run specific test
cargo test test_full_workflow
# Run benchmarks
cargo test --release
๐ 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
git clone https://github.com/MKSinghDev/pq-jwt-rust.git
cd pq-jwt-rust
cargo build
cargo test
๐ 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
Dependencies
~5.5MB
~94K SLoC