pq-jwt

Crates.iopq-jwt
lib.rspq-jwt
version0.2.2
created_at2025-11-02 19:31:43.414404+00
updated_at2025-11-08 11:41:14.982345+00
descriptionPost-Quantum JWT implementation using ML-DSA (FIPS 204) signatures for quantum-resistant authentication
homepage
repositoryhttps://github.com/MKSinghDev/pq-jwt-rust
max_upload_size
id1913479
size233,702
Mukesh Singh (MKSinghDev)

documentation

README

๐Ÿ” pq-jwt

Crates.io Documentation CircleCI CI License

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:

[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

  1. Cache Keys: Generate keypairs once and reuse them
  2. Pre-verify Format: Check JWT structure before cryptographic verification
  3. Use ML-DSA-44: If bandwidth is critical and security level 2 is acceptable
  4. 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 variant
  • iss - 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 string
  • public_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 verify
  • public_key_hex - Hex-encoded public key
  • expected_issuer - Expected issuer that must match the JWT's iss claim

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) - 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:

  • 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, returns Result<Verifier, String>

Verifier Methods:

  • .verify(&str) - Verify JWT and return payload, returns Result<String, String>

Automatic Validations (Always Performed):

  • โœ… Signature verification (cryptographic)
  • โœ… Expiration check (exp must be in the future)
  • โœ… Issuer matching (iss claim 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 (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)?;

Enums

MlDsaAlgo

Available algorithm variants:

  • MlDsaAlgo::Dsa44 - NIST Category 2
  • MlDsaAlgo::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 string
  • KeySource::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

  1. Always include expiration (exp claim)
  2. Use short lifetimes for sensitive operations (15 min - 1 hour)
  3. Implement token revocation if needed
  4. Validate claims after verification
  5. 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:

  1. 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
    
  2. Implement TTL matching JWT expiration:

    • Server-side storage TTL should match JWT exp claim
    • Prevents storage of expired tokens
  3. Rate limit lookups by jti:

    • Prevent enumeration attacks
    • Limit requests per IP/user
  4. 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:

  1. Intercept and store encrypted data today
  2. Wait for quantum computers to become available
  3. 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

๐Ÿค 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:

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

Commit count: 0

cargo fmt