| Crates.io | paseto-pq |
| lib.rs | paseto-pq |
| version | 0.1.2 |
| created_at | 2025-11-16 14:41:06.127896+00 |
| updated_at | 2025-11-17 16:09:46.350938+00 |
| description | Post-quantum PASETO tokens with RFC-compliant footer authentication using ML-DSA signatures |
| homepage | https://github.com/thatnewyorker/paseto-pq |
| repository | https://github.com/thatnewyorker/paseto-pq |
| max_upload_size | |
| id | 1935565 |
| size | 340,731 |
A pure post-quantum implementation of PASETO tokens using ML-DSA (CRYSTALS-Dilithium) signatures and ChaCha20-Poly1305 encryption. This crate provides quantum-safe authentication and encryption tokens with comprehensive metadata support, resistant to attacks by quantum computers implementing Shor's algorithm.
Latest Release: Version 0.1.2 includes code quality improvements and critical security fixes:
-D warnings (v0.1.2)Users should upgrade to v0.1.2 for the latest security and quality improvements.
Default: ml-dsa-44 - Optimized for distributed systems and network protocols
Upgrade to ml-dsa-65 for high-value or long-term secrets
Upgrade to ml-dsa-87 for critical infrastructure
# Default (recommended for most applications)
paseto-pq = "0.1.2"
# High security applications
paseto-pq = { version = "0.1.2", features = ["balanced"] }
# Maximum security applications
paseto-pq = { version = "0.1.2", features = ["maximum-security"] }
# Explicit parameter set selection
paseto-pq = { version = "0.1.2", features = ["ml-dsa-65"], default-features = false }
Add this to your Cargo.toml:
[dependencies]
paseto-pq = "0.1.0" # Uses ml-dsa-44 by default for optimal network performance
time = { version = "0.3", features = ["serde", "formatting", "parsing"] }
rand = "0.10.0-rc.1"
use paseto_pq::{PasetoPQ, Claims, KeyPair};
use time::OffsetDateTime;
use rand::rng;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Generate a new key pair
let mut rng = rng();
let keypair = KeyPair::generate(&mut rng);
// Create claims
let mut claims = Claims::new();
claims.set_subject("user123")?;
claims.set_issuer("my-service")?;
claims.set_audience("api.example.com")?;
claims.set_expiration(OffsetDateTime::now_utc() + time::Duration::hours(1))?;
claims.add_custom("tenant_id", "org_abc123")?;
claims.add_custom("roles", &["user", "admin"])?;
// Sign the token (public)
let token = PasetoPQ::sign(&keypair.signing_key, &claims)?;
println!("Public Token: {}", token);
// Verify the token
let verified = PasetoPQ::verify(&keypair.verifying_key, &token)?;
let verified_claims = verified.claims();
assert_eq!(verified_claims.subject(), Some("user123"));
assert_eq!(verified_claims.issuer(), Some("my-service"));
Ok(())
}
use paseto_pq::{PasetoPQ, Claims, SymmetricKey};
use time::OffsetDateTime;
use rand::rng;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Generate a symmetric key
let mut rng = rng();
let key = SymmetricKey::generate(&mut rng);
// Create claims (same as public tokens)
let mut claims = Claims::new();
claims.set_subject("user123")?;
claims.set_issuer("my-service")?;
claims.set_audience("api.example.com")?;
claims.set_expiration(OffsetDateTime::now_utc() + time::Duration::hours(1))?;
claims.add_custom("sensitive_data", "confidential-info")?;
// Encrypt the token (local)
let token = PasetoPQ::encrypt(&key, &claims)?;
println!("Local Token: {}", token);
// Decrypt the token
let verified = PasetoPQ::decrypt(&key, &token)?;
let verified_claims = verified.claims();
assert_eq!(verified_claims.subject(), Some("user123"));
assert_eq!(verified_claims.issuer(), Some("my-service"));
Ok(())
}
PASETO-PQ supports authenticated footers for metadata that doesn't belong in claims:
use paseto_pq::{PasetoPQ, Claims, Footer, KeyPair};
use time::OffsetDateTime;
use rand::rng;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut rng = rng();
let keypair = KeyPair::generate(&mut rng);
// Create claims
let mut claims = Claims::new();
claims.set_subject("user123")?;
claims.set_issuer("my-service")?;
// Create footer with metadata
let mut footer = Footer::new();
footer.set_kid("signing-key-2025-01")?; // Key identifier for rotation
footer.set_version("v2.1.0")?; // Service version
footer.add_custom("trace_id", "trace-abc-123")?; // Distributed tracing
footer.add_custom("deployment", "us-east-1")?; // Infrastructure info
// Sign with footer
let token = PasetoPQ::sign_with_footer(&keypair.signing_key, &claims, Some(&footer))?;
// Verify and access footer
let verified = PasetoPQ::verify_with_footer(&keypair.verifying_key, &token)?;
let verified_footer = verified.footer().unwrap();
println!("Key ID: {}", verified_footer.kid().unwrap());
println!("Trace ID: {}", verified_footer.get_custom("trace_id").unwrap().as_str().unwrap());
Ok(())
}
โ ๏ธ NON-STANDARD VERSIONING: PASETO-PQ uses a non-standard token format that is incompatible with official PASETO libraries. The pq1 version identifier clearly indicates "post-quantum era" tokens, distinguishing them from classical algorithms defined in the PASETO specification.
Both token types support optional footers:
# Without footer
paseto.pq1.public.<base64url-payload>.<base64url-signature>
# With footer
paseto.pq1.public.<base64url-payload>.<base64url-signature>.<base64url-footer>
paseto: Protocol identifierpq1: Post-quantum version identifier (non-standard, distinct from official PASETO)public: Purpose (signature-based tokens)payload: Base64url-encoded JSON claimssignature: Base64url-encoded ML-DSA signature (size varies by parameter set)footer: Base64url-encoded JSON metadata (optional, authenticated)# Without footer
paseto.pq1.local.<base64url-encrypted-payload>
# With footer
paseto.pq1.local.<base64url-encrypted-payload>.<base64url-footer>
paseto: Protocol identifierpq1: Post-quantum version identifier (non-standard, distinct from official PASETO)local: Purpose (symmetric encryption)encrypted-payload: Base64url-encoded nonce + ChaCha20-Poly1305 ciphertextfooter: Base64url-encoded JSON metadata (optional, encrypted with payload)| Parameter Set | Security Level | Signature Size | Public Key Size | Token Size (approx.) |
|---|---|---|---|---|
| ml-dsa-44 | 128-bit (default) | ~2,420 bytes | ~1,312 bytes | ~3.2-3.4KB |
| ml-dsa-65 | 192-bit | ~3,309 bytes | ~1,952 bytes | ~4.3-4.5KB |
| ml-dsa-87 | 256-bit | ~4,627 bytes | ~2,592 bytes | ~6.0-6.2KB |
Comparison to Classical Algorithms:
| Operation | ML-DSA (avg) | Ed25519 (reference) | Ratio |
|---|---|---|---|
| Key Generation | ~10-30ms | ~100ยตs | 100-300x slower |
| Signing | ~5-20ms | ~50ยตs | 100-400x slower |
| Verification | ~2-5ms | ~80ยตs | 25-60x slower |
| Signature Size | 2,420-4,627 bytes | 64 bytes | 38-72x larger |
| Public Key | 1,312-2,592 bytes | 32 bytes | 41-81x larger |
| Operation | PASETO-PQ Local | Traditional PASETO v4.local | Ratio |
|---|---|---|---|
| Key Generation | ~1ยตs | ~1ยตs | ~1x |
| Encryption | ~1-5ยตs | ~1-5ยตs | ~1x |
| Decryption | ~1-5ยตs | ~1-5ยตs | ~1x |
| Token Overhead | ~30 bytes | ~30 bytes | ~1x |
| Symmetric Key | 32 bytes | 32 bytes | 1x |
| Token Size | ~100-300 bytes | ~100-300 bytes | ~1x |
| Operation | ML-KEM-768 | ECDH P-256 (reference) | Ratio |
|---|---|---|---|
| Key Generation | ~100ยตs | ~50ยตs | 2x slower |
| Encapsulation | ~150ยตs | ~100ยตs | 1.5x slower |
| Decapsulation | ~200ยตs | ~100ยตs | 2x slower |
| Ciphertext Size | 1,088 bytes | 33 bytes | 33x larger |
| Public Key | 1,184 bytes | 33 bytes | 36x larger |
Note: Performance varies by hardware. These numbers are from benchmarks on modern x86-64.
Token Size Implications:
โ Recommended For:
โ ๏ธ Consider Carefully:
โ Recommended For:
โ ๏ธ Consider Carefully:
โ Recommended For:
use paseto_pq::{PasetoPQ, Claims, KeyPair};
use time::Duration;
let verified = PasetoPQ::verify_with_options(
&verifying_key,
&token,
Some("expected-audience"), // Validate audience
Some("expected-issuer"), // Validate issuer
Duration::minutes(5), // Clock skew tolerance
)?;
use paseto_pq::{Footer, PasetoPQ};
// Create and populate footer
let mut footer = Footer::new();
footer.set_kid("key-2025-01")?;
footer.set_version("v1.0.0")?;
footer.add_custom("trace_id", "abc-123")?;
footer.add_custom("environment", "production")?;
// Use with public tokens (footer is authenticated by signature)
let token = PasetoPQ::sign_with_footer(&signing_key, &claims, Some(&footer))?;
let verified = PasetoPQ::verify_with_footer(&verifying_key, &token)?;
// Access footer data
if let Some(footer_data) = verified.footer() {
println!("Key ID: {}", footer_data.kid().unwrap_or("none"));
println!("Trace: {}", footer_data.get_custom("trace_id").unwrap().as_str().unwrap());
}
// Use with local tokens (footer is encrypted with payload)
let local_token = PasetoPQ::encrypt_with_footer(&symmetric_key, &claims, Some(&footer))?;
let decrypted = PasetoPQ::decrypt_with_footer(&symmetric_key, &local_token)?;
use paseto_pq::{PasetoPQ, SymmetricKey};
use time::Duration;
let verified = PasetoPQ::decrypt_with_options(
&symmetric_key,
&token,
Some("expected-audience"), // Validate audience
Some("expected-issuer"), // Validate issuer
Duration::minutes(5), // Clock skew tolerance
)?;
// Public/Private keys
let signing_bytes = keypair.signing_key_to_bytes();
let verifying_bytes = keypair.verifying_key_to_bytes();
let signing_key = KeyPair::signing_key_from_bytes(&signing_bytes)?;
let verifying_key = KeyPair::verifying_key_from_bytes(&verifying_bytes)?;
// Symmetric keys
let sym_bytes = symmetric_key.to_bytes();
let symmetric_key = SymmetricKey::from_bytes(&sym_bytes)?;
// KEM keys
let enc_bytes = kem_keypair.encapsulation_key_to_bytes();
let dec_bytes = kem_keypair.decapsulation_key_to_bytes();
let enc_key = KemKeyPair::encapsulation_key_from_bytes(&enc_bytes)?;
let dec_key = KemKeyPair::decapsulation_key_from_bytes(&dec_bytes)?;
use paseto_pq::{KemKeyPair, SymmetricKey, PasetoPQ};
use rand::rng;
// Generate KEM keypair
let mut rng = rng();
let kem_keypair = KemKeyPair::generate(&mut rng);
// Sender: encapsulate shared secret
let (shared_key_sender, ciphertext) = kem_keypair.encapsulate(&mut rng);
// Receiver: decapsulate shared secret
let shared_key_receiver = kem_keypair.decapsulate(&ciphertext)?;
// Both parties now have the same symmetric key
assert_eq!(shared_key_sender.to_bytes(), shared_key_receiver.to_bytes());
// Use for local tokens
let token = PasetoPQ::encrypt(&shared_key_sender, &claims)?;
let verified = PasetoPQ::decrypt(&shared_key_receiver, &token)?;
let mut claims = Claims::new();
claims.set_subject("user123")?;
// Add custom business logic
claims.add_custom("tenant_id", "org_123")?;
claims.add_custom("permissions", &["read:users", "write:posts"])?;
claims.add_custom("metadata", &serde_json::json!({
"client_version": "2.1.0",
"platform": "web"
}))?;
// Works with both public and local tokens
let public_token = PasetoPQ::sign(&keypair.signing_key, &claims)?;
let local_token = PasetoPQ::encrypt(&symmetric_key, &claims)?;
// Access custom claims after verification/decryption
if let Some(tenant) = verified_claims.get_custom("tenant_id") {
println!("Tenant: {}", tenant.as_str().unwrap());
}
PASETO-PQ provides seamless JSON integration for easy use with logging systems, databases, and distributed tracing:
use paseto_pq::Claims;
use serde_json::Value;
use time::OffsetDateTime;
let mut claims = Claims::new();
claims.set_subject("user123")?;
claims.set_issuer("auth-service")?;
claims.set_expiration(OffsetDateTime::now_utc() + time::Duration::hours(2))?;
claims.add_custom("tenant_id", "org_abc123")?;
claims.add_custom("roles", &["admin", "user"])?;
// Convert to JSON Value for flexible use
let json_value: Value = claims.clone().into();
// Convert to JSON string for logging
let json_string = claims.to_json_string()?;
println!("User authenticated: {}", json_string);
// Pretty JSON for debugging
let pretty_json = claims.to_json_string_pretty()?;
// Time fields are RFC3339 strings for maximum compatibility
// {"exp": "2025-01-15T10:30:00Z", "iat": "2025-01-14T10:30:00Z"}
Integration Examples:
Run the JSON integration demo:
cargo run --example json_integration_demo
Parse tokens for inspection without expensive cryptographic operations. Perfect for debugging, middleware routing, logging, and monitoring:
use paseto_pq::{ParsedToken, PasetoPQ};
let token = "paseto.pq1.public.ABC123...";
let parsed = ParsedToken::parse(token)?;
// Inspect token structure (no crypto operations)
println!("Purpose: {}", parsed.purpose()); // "public" or "local"
println!("Version: {}", parsed.version()); // "pq1"
println!("Has footer: {}", parsed.has_footer());
println!("Size: {} bytes", parsed.total_length());
println!("Is public token: {}", parsed.is_public());
println!("Is local token: {}", parsed.is_local());
// Middleware routing based on token type
match parsed.purpose() {
"public" => route_to_signature_service(token),
"local" => route_to_decryption_service(token),
_ => return_error("unsupported token type"),
}
// Debugging information
println!("Token summary: {}", parsed.format_summary());
if let Some(footer) = parsed.footer() {
if let Some(kid) = footer.kid() {
println!("Key ID: {}", kid);
}
// Pretty-print footer JSON
println!("Footer: {}", parsed.footer_json_pretty()?);
}
// Quick access with PasetoPQ wrapper
let parsed_alt = PasetoPQ::parse_token(token)?;
Use Cases:
Run the token parsing demo:
cargo run --example token_parsing_demo
Estimate token sizes before creation to avoid runtime surprises with HTTP headers, cookies, or URL length limits:
use paseto_pq::{Claims, TokenSizeEstimator, PasetoPQ};
let mut claims = Claims::new();
claims.set_subject("user123")?;
claims.add_custom("role", "admin")?;
claims.add_custom("permissions", &["read", "write", "admin"])?;
// Estimate before creating
let estimator = TokenSizeEstimator::public(&claims, false);
println!("Public token will be ~{} bytes", estimator.total_bytes());
// Check transport compatibility
if !estimator.fits_in_cookie() {
println!("โ ๏ธ Warning: Token too large for cookies (4KB limit)!");
println!("๐ก Consider using session storage or local tokens instead");
}
if estimator.fits_in_header() {
println!("โ
Token fits in Authorization header (8KB typical limit)");
}
if estimator.fits_in_url() {
println!("โ
Token fits in URL parameters (2KB practical limit)");
} else {
println!("โ ๏ธ Token too large for URL parameters");
}
// Get detailed breakdown
let breakdown = estimator.breakdown();
println!("Size breakdown:");
println!(" Payload: {} bytes", breakdown.payload);
println!(" Signature: {} bytes", breakdown.signature_or_tag);
println!(" Base64 overhead: {} bytes", breakdown.base64_overhead);
println!(" Total: {} bytes", breakdown.total());
// Get optimization suggestions
if estimator.total_bytes() > 4000 {
for suggestion in estimator.optimization_suggestions() {
println!("๐ก {}", suggestion);
}
}
// Compare token types
let public_est = PasetoPQ::estimate_public_size(&claims, false);
let local_est = PasetoPQ::estimate_local_size(&claims, false);
println!("Size comparison:");
println!(" Public token: {} bytes", public_est.total_bytes());
println!(" Local token: {} bytes", local_est.total_bytes());
// Compare to JWT (for reference)
println!("Compared to JWT: {}", estimator.compare_to_jwt());
// Generate size summary
println!("Summary: {}", estimator.size_summary());
Size Limits & Recommendations:
Use Cases:
Run the token size estimation demo:
cargo run --example token_size_demo
ZeroizeOnDrop traitDrop for automatic cleanup when out of scopeParsedToken::parse() performs no crypto operations, safe for untrusted input[dependencies]
paseto-pq = { version = "0.1.0", features = ["logging"] }
logging - Enable structured logging with tracingstd - Standard library support (enabled by default)serde: JSON serialization support (enabled by default)time: Time-based claims validation (enabled by default)All major features are enabled by default for ease of use.
Run the complete test suite:
# Run all tests with full backtrace (recommended)
RUST_BACKTRACE=full cargo nextest run --workspace --all-targets
# Run specific test categories
cargo test test_keypair_generation
cargo test test_json_conversion
cargo test test_token_parsing
cargo test test_token_size_estimation
# Run examples to verify functionality
cargo run --example json_integration_demo
cargo run --example token_parsing_demo
cargo run --example token_size_demo
cargo run --example footer_demo
cargo run --example local_tokens_demo
# Run benchmarks
cargo bench
We welcome contributions! Please see our Contributing Guide for details.
git clone https://github.com/thatnewyorker/paseto-pq.git
cd paseto-pq
cargo build
cargo test
Licensed under either of:
at your option.
IMPORTANT: This implementation has not yet undergone independent security audit. While built on NIST-standardized algorithms and well-tested Rust cryptographic libraries, please conduct your own security review before using in production systems.