| Crates.io | pq-jwt |
| lib.rs | pq-jwt |
| version | 0.2.2 |
| created_at | 2025-11-02 19:31:43.414404+00 |
| updated_at | 2025-11-08 11:41:14.982345+00 |
| description | Post-Quantum JWT implementation using ML-DSA (FIPS 204) signatures for quantum-resistant authentication |
| homepage | |
| repository | https://github.com/MKSinghDev/pq-jwt-rust |
| max_upload_size | |
| id | 1913479 |
| size | 233,702 |
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.
kid (Key ID) in JWT headers|
Operations
Standard Claims
|
Claim Validation
Custom Claims
|
| 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.
Add this to your Cargo.toml:
[dependencies]
pq-jwt = "0.1.0"
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(())
}
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);
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);
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)
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);
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!
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
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)?;
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),
}
}
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),
}
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);
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 |
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;
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)
| 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.
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 keyReturns: (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 managementNote: 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's iss claimReturns: payload if valid, error otherwise
let payload = verify(&jwt, &public_key, "https://myapp.com")?;
keygen::BuilderGeneration 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)(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(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::BuilderConfiguration Methods:
.algorithm(MlDsaAlgo) - Set the algorithm variant (REQUIRED).private_key(&str) - Set the private key (REQUIRED)Standard JWT Claims Methods:
.issuer(&str) - Set iss claim (REQUIRED).expiration(u64) - Set exp claim as Unix timestamp (REQUIRED).subject(&str) - Set sub claim (optional).audience(&str) - Set aud claim (optional).issued_at(Option<u64>) - Set iat claim, defaults to signing time if not set (optional).not_before(u64) - Set nbf claim as Unix timestamp (optional).jwt_id(&str) - Override the auto-generated jti claim (UUID v7 by default).custom_claims(serde_json::Value) - Add custom claims (optional)Build Method:
.build() - Build Signer instance, returns Result<Signer, String>Signer Methods:
.sign() - Sign the configured claims, returns Result<(String, String, String), String> as (jwt, public_key, jti)Notes:
iat (issued at) defaults to the current signing time if not explicitly setexp > iat, nbf <= iat)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::BuilderRequired 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, returns Result<Verifier, String>Verifier Methods:
.verify(&str) - Verify JWT and return payload, returns Result<String, String>Automatic Validations (Always Performed):
exp must be in the future)iss claim must match expected issuer)Optional Validations (Configured via Builder):
.audience() is called).subject() is called)nbf if 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)?;
MlDsaAlgoAvailable algorithm variants:
MlDsaAlgo::Dsa44 - NIST Category 2MlDsaAlgo::Dsa65 - NIST Category 3 (Recommended)MlDsaAlgo::Dsa87 - NIST Category 5Traits: Debug, Clone, Copy, PartialEq, Eq
KeySourceIndicates 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"),
}
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()?;
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()
// โ 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
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
exp claim)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
)?;
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:
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")?;
This library uses UUID v7 (time-ordered) for jti, which provides several benefits:
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:
exp claimRate limit lookups by jti:
Clean up expired sessions:
// Periodic cleanup for database-backed storage
db.execute("DELETE FROM sessions WHERE expires_at < NOW()")?;
// 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()))
}
| 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) |
Quantum computers, when fully developed, will break current cryptographic systems:
Attackers can:
Solution: Start using post-quantum crypto NOW to protect long-term secrets.
| 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 |
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")),
}
}
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)
}
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
Contributions are welcome! Please feel free to submit a Pull Request.
git clone https://github.com/MKSinghDev/pq-jwt-rust.git
cd pq-jwt-rust
cargo build
cargo test
This project is dual-licensed under:
You may choose either license for your use.
MKSingh (@MKSingh_Dev)
If you find this project useful, please consider giving it a star! โญ
Made with โค๏ธ for a quantum-safe future