paseto_pq/
lib.rs

1//! # PASETO-PQ: Post-Quantum PASETO Tokens
2//!
3//! A pure post-quantum implementation of PASETO-inspired tokens using ML-DSA (CRYSTALS-Dilithium)
4//! for digital signatures. This crate provides quantum-safe authentication tokens that are
5//! resistant to attacks by quantum computers.
6//!
7//! ## Design Principles
8//!
9//! - **Post-Quantum Only**: Uses ML-DSA-65 (NIST FIPS 204) for all signatures
10//! - **PASETO-Inspired**: Follows PASETO's security model but with PQ algorithms
11//! - **Greenfield**: No legacy compatibility, designed for quantum-safe future
12//! - **Memory Safety**: Automatic zeroization of sensitive keys on drop
13//! - **Cryptographic Hygiene**: Proper HKDF key derivation and secure random generation
14//!
15//! ## ⚠️ Non-Standard Token Format
16//!
17//! **IMPORTANT**: This crate uses a **non-standard** token versioning scheme that diverges
18//! from the official PASETO specification. The tokens use `pq1` to clearly indicate
19//! post-quantum algorithms and avoid confusion with standard PASETO versions.
20//!
21//! ### Token Format
22//!
23//! ```text
24//! paseto.pq1.public.<base64url-encoded-payload>.<base64url-encoded-ml-dsa-signature>
25//! paseto.pq1.local.<base64url-encoded-encrypted-payload>
26//! ```
27//!
28//! ### Interoperability Warning
29//!
30//! These tokens are **NOT** compatible with standard PASETO libraries or tooling.
31//! If you need interoperability with existing PASETO ecosystems, this crate is not suitable.
32//! The `pq1` versioning scheme clearly indicates "post-quantum era" tokens, distinguishing
33//! them from the classical algorithms defined in the PASETO specification.
34//!
35//! Consider this crate for:
36//! - Greenfield applications requiring post-quantum security
37//! - Internal systems where PASETO compatibility is not required
38//! - Future migration paths when post-quantum PASETO standards emerge
39//!
40//! ## Example Usage
41//!
42//! ```rust,no_run
43//! use paseto_pq::{PasetoPQ, Claims, KeyPair};
44//! use time::OffsetDateTime;
45//!
46//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
47//! // Generate a new key pair
48//! let mut rng = rand::thread_rng();
49//! let keypair = KeyPair::generate(&mut rng);
50//!
51//! // Create claims
52//! let mut claims = Claims::new();
53//! claims.set_subject("user123")?;
54//! claims.set_issuer("my-service")?;
55//! claims.set_audience("api.example.com")?;
56//! claims.set_expiration(OffsetDateTime::now_utc() + time::Duration::hours(1))?;
57//! claims.add_custom("tenant_id", "org_abc123")?;
58//! claims.add_custom("roles", &["user", "admin"])?;
59//!
60//! // Sign the token
61//! let token = PasetoPQ::sign(keypair.signing_key(), &claims)?;
62//!
63//! // Verify the token
64//! let verified = PasetoPQ::verify(keypair.verifying_key(), &token)?;
65//! let verified_claims = verified.claims();
66//! assert_eq!(verified_claims.subject(), Some("user123"));
67//! # Ok(())
68//! # }
69//! ```
70
71use std::collections::HashMap;
72use std::fmt;
73
74use anyhow::Result;
75pub mod pae;
76
77use base64::Engine;
78use base64::engine::general_purpose::URL_SAFE_NO_PAD;
79// Conditional compilation for ML-DSA parameter set selection
80#[cfg(all(
81    feature = "ml-dsa-44",
82    not(any(feature = "ml-dsa-65", feature = "ml-dsa-87"))
83))]
84use ml_dsa::MlDsa44 as MlDsaParam;
85
86#[cfg(all(
87    feature = "ml-dsa-65",
88    not(any(feature = "ml-dsa-44", feature = "ml-dsa-87"))
89))]
90use ml_dsa::MlDsa65 as MlDsaParam;
91
92#[cfg(all(
93    feature = "ml-dsa-87",
94    not(any(feature = "ml-dsa-44", feature = "ml-dsa-65"))
95))]
96use ml_dsa::MlDsa87 as MlDsaParam;
97
98// Compilation guards to ensure exactly one parameter set is selected
99#[cfg(not(any(feature = "ml-dsa-44", feature = "ml-dsa-65", feature = "ml-dsa-87")))]
100compile_error!(
101    "Please enable exactly one of the features: `ml-dsa-44`, `ml-dsa-65`, or `ml-dsa-87`."
102);
103
104#[cfg(all(
105    feature = "ml-dsa-44",
106    any(feature = "ml-dsa-65", feature = "ml-dsa-87")
107))]
108compile_error!("Only one of `ml-dsa-44`, `ml-dsa-65`, or `ml-dsa-87` may be enabled.");
109
110#[cfg(all(feature = "ml-dsa-65", feature = "ml-dsa-87"))]
111compile_error!("Only one of `ml-dsa-44`, `ml-dsa-65`, or `ml-dsa-87` may be enabled.");
112
113use ml_dsa::{
114    KeyGen,
115    signature::{SignatureEncoding, Signer, Verifier},
116};
117// ML-KEM imports for real implementation
118use hkdf::Hkdf;
119use ml_kem::{
120    KemCore, MlKem768,
121    kem::{Decapsulate, Encapsulate},
122};
123pub use rand_core::{CryptoRng, RngCore};
124use serde::{Deserialize, Serialize};
125use serde_json::Value;
126use sha2::Sha256;
127use zeroize::{Zeroize, ZeroizeOnDrop};
128
129use time::OffsetDateTime;
130
131// Symmetric encryption imports
132use chacha20poly1305::{
133    ChaCha20Poly1305, Nonce,
134    aead::{AeadCore, AeadInPlace, KeyInit, OsRng as AeadOsRng},
135};
136
137#[cfg(feature = "logging")]
138use tracing::{debug, instrument, warn};
139
140/// Post-quantum PASETO implementation using ML-DSA-65
141pub struct PasetoPQ;
142
143// Re-export core PAE function for advanced users (added in v0.1.1)
144// Internal functions like le64_encode remain private
145pub use pae::pae_encode;
146
147/// A post-quantum key pair for signing and verification
148#[derive(Clone)]
149pub struct KeyPair {
150    signing_key: SigningKey,
151    verifying_key: VerifyingKey,
152}
153
154/// A signing key for creating tokens
155#[derive(Clone)]
156pub struct SigningKey(ml_dsa::SigningKey<MlDsaParam>);
157
158/// A verifying key for validating tokens
159#[derive(Clone)]
160pub struct VerifyingKey(ml_dsa::VerifyingKey<MlDsaParam>);
161
162/// A symmetric key for local token encryption/decryption
163#[derive(Clone, Zeroize, ZeroizeOnDrop)]
164pub struct SymmetricKey([u8; 32]);
165
166/// A post-quantum key encapsulation key pair for key exchange
167#[derive(Clone)]
168pub struct KemKeyPair {
169    pub encapsulation_key: EncapsulationKey,
170    pub decapsulation_key: DecapsulationKey,
171}
172
173/// An encapsulation key for ML-KEM key exchange
174#[derive(Clone)]
175pub struct EncapsulationKey(<MlKem768 as KemCore>::EncapsulationKey);
176
177/// A decapsulation key for ML-KEM key exchange
178#[derive(Clone)]
179pub struct DecapsulationKey(<MlKem768 as KemCore>::DecapsulationKey);
180
181/// Footer data for additional authenticated metadata
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct Footer {
184    /// Key identifier for key rotation and selection
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub kid: Option<String>,
187
188    /// Token version for compatibility tracking
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub version: Option<String>,
191
192    /// Issuer-specific metadata
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub issuer_meta: Option<String>,
195
196    /// Additional custom metadata
197    #[serde(flatten)]
198    pub custom: HashMap<String, Value>,
199}
200
201impl Footer {
202    /// Create a new empty footer
203    pub fn new() -> Self {
204        Self {
205            kid: None,
206            version: None,
207            issuer_meta: None,
208            custom: HashMap::new(),
209        }
210    }
211
212    /// Set the key identifier
213    pub fn set_kid(&mut self, kid: &str) -> Result<(), PqPasetoError> {
214        self.kid = Some(kid.to_string());
215        Ok(())
216    }
217
218    /// Set the version
219    pub fn set_version(&mut self, version: &str) -> Result<(), PqPasetoError> {
220        self.version = Some(version.to_string());
221        Ok(())
222    }
223
224    /// Set issuer metadata
225    pub fn set_issuer_meta(&mut self, issuer_meta: &str) -> Result<(), PqPasetoError> {
226        self.issuer_meta = Some(issuer_meta.to_string());
227        Ok(())
228    }
229
230    /// Add custom footer field
231    pub fn add_custom<T: Serialize + ?Sized>(
232        &mut self,
233        key: &str,
234        value: &T,
235    ) -> Result<(), PqPasetoError> {
236        let json_value = serde_json::to_value(value)?;
237        self.custom.insert(key.to_string(), json_value);
238        Ok(())
239    }
240
241    /// Get custom footer field
242    pub fn get_custom(&self, key: &str) -> Option<&Value> {
243        self.custom.get(key)
244    }
245
246    /// Get key identifier
247    pub fn kid(&self) -> Option<&str> {
248        self.kid.as_deref()
249    }
250
251    /// Get version
252    pub fn version(&self) -> Option<&str> {
253        self.version.as_deref()
254    }
255
256    /// Get issuer metadata
257    pub fn issuer_meta(&self) -> Option<&str> {
258        self.issuer_meta.as_deref()
259    }
260
261    /// Serialize footer to base64url-encoded JSON
262    pub fn to_base64(&self) -> Result<String, PqPasetoError> {
263        let json = serde_json::to_vec(self)?;
264        Ok(URL_SAFE_NO_PAD.encode(&json))
265    }
266
267    /// Deserialize footer from base64url-encoded JSON
268    pub(crate) fn from_base64(encoded: &str) -> Result<Self, PqPasetoError> {
269        let bytes = URL_SAFE_NO_PAD.decode(encoded)?;
270        let footer = serde_json::from_slice(&bytes)?;
271        Ok(footer)
272    }
273}
274
275impl Default for Footer {
276    fn default() -> Self {
277        Self::new()
278    }
279}
280
281/// Claims contained within a token
282#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct Claims {
284    /// Token issuer
285    #[serde(skip_serializing_if = "Option::is_none")]
286    pub iss: Option<String>,
287
288    /// Token subject
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub sub: Option<String>,
291
292    /// Token audience
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub aud: Option<String>,
295
296    /// Token expiration time
297    #[serde(
298        skip_serializing_if = "Option::is_none",
299        default,
300        with = "time::serde::rfc3339::option"
301    )]
302    pub exp: Option<OffsetDateTime>,
303
304    /// Token not-before time
305    #[serde(
306        skip_serializing_if = "Option::is_none",
307        default,
308        with = "time::serde::rfc3339::option"
309    )]
310    pub nbf: Option<OffsetDateTime>,
311
312    /// Token issued-at time
313    #[serde(
314        skip_serializing_if = "Option::is_none",
315        default,
316        with = "time::serde::rfc3339::option"
317    )]
318    pub iat: Option<OffsetDateTime>,
319
320    /// Token identifier (jti)
321    #[serde(skip_serializing_if = "Option::is_none")]
322    pub jti: Option<String>,
323
324    /// Key identifier
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub kid: Option<String>,
327
328    /// Custom claims (Conflux-specific)
329    #[serde(flatten)]
330    pub custom: HashMap<String, Value>,
331}
332
333/// Verified token containing validated claims and optional footer
334#[derive(Debug, Clone)]
335pub struct VerifiedToken {
336    claims: Claims,
337    footer: Option<Footer>,
338    raw_token: String,
339}
340
341/// Parsed token structure for inspection without cryptographic operations
342///
343/// This struct allows you to examine token metadata (purpose, version, footer)
344/// without performing expensive cryptographic verification or decryption.
345/// Useful for debugging, logging, middleware, and routing decisions.
346///
347/// # Example
348///
349/// ```rust,no_run
350/// use paseto_pq::ParsedToken;
351///
352/// let token = "paseto.pq1.public.ABC123...";
353/// let parsed = ParsedToken::parse(token)?;
354///
355/// println!("Purpose: {}", parsed.purpose()); // "public"
356/// println!("Version: {}", parsed.version()); // "pq1"
357/// println!("Has footer: {}", parsed.has_footer());
358///
359/// // Use for routing decisions
360/// match parsed.purpose() {
361///     "public" => println!("Public token - needs verification"),
362///     "local" => println!("Local token - needs decryption"),
363///     _ => println!("Unsupported token type"),
364/// }
365/// # Ok::<(), paseto_pq::PqPasetoError>(())
366/// ```
367#[derive(Debug, Clone)]
368pub struct ParsedToken {
369    purpose: String,
370    version: String,
371    payload: Vec<u8>,
372    signature_or_tag: Option<Vec<u8>>, // For public tokens (signature) or local tokens (auth tag)
373    footer: Option<Footer>,
374    raw_token: String,
375}
376
377/// Token size breakdown showing individual components
378///
379/// This struct provides detailed information about how token size is distributed
380/// across different components, useful for optimization and debugging.
381#[derive(Debug, Clone)]
382pub struct TokenSizeBreakdown {
383    /// Size of the protocol prefix ("paseto.pq1.public." or "paseto.pq1.local.")
384    pub prefix: usize,
385    /// Size of the JSON payload after base64 encoding
386    pub payload: usize,
387    /// Size of signature (public tokens) or authentication tag (local tokens)
388    pub signature_or_tag: usize,
389    /// Size of footer if present
390    pub footer: Option<usize>,
391    /// Size of separator dots between parts
392    pub separators: usize,
393    /// Additional overhead from base64 encoding (~33% expansion)
394    pub base64_overhead: usize,
395}
396
397/// Token size estimator for planning and optimization
398///
399/// This struct allows you to estimate token sizes before creation to avoid
400/// runtime surprises with HTTP headers, cookies, or URL length limits.
401///
402/// # Example
403///
404/// ```rust,no_run
405/// use paseto_pq::{Claims, TokenSizeEstimator};
406///
407/// let mut claims = Claims::new();
408/// claims.set_subject("user123").unwrap();
409/// claims.add_custom("role", "admin").unwrap();
410///
411/// let estimator = TokenSizeEstimator::public(&claims, true);
412/// println!("Estimated size: {} bytes", estimator.total_bytes());
413///
414/// if !estimator.fits_in_cookie() {
415///     println!("Token too large for browser cookies!");
416/// }
417/// # Ok::<(), paseto_pq::PqPasetoError>(())
418/// ```
419#[derive(Debug, Clone)]
420pub struct TokenSizeEstimator {
421    breakdown: TokenSizeBreakdown,
422}
423
424/// Errors that can occur during token operations
425#[derive(Debug, thiserror::Error)]
426pub enum PqPasetoError {
427    #[error("Invalid token format: {0}")]
428    InvalidFormat(String),
429
430    #[error("Signature verification failed")]
431    SignatureVerificationFailed,
432
433    #[error("Token has expired")]
434    TokenExpired,
435
436    #[error("Token is not yet valid (nbf claim)")]
437    TokenNotYetValid,
438
439    #[error("Invalid audience: expected {expected}, got {actual}")]
440    InvalidAudience { expected: String, actual: String },
441
442    #[error("Invalid issuer: expected {expected}, got {actual}")]
443    InvalidIssuer { expected: String, actual: String },
444
445    #[error("JSON serialization error: {0}")]
446    SerializationError(#[from] serde_json::Error),
447
448    #[error("Base64 decoding error: {0}")]
449    Base64Error(#[from] base64::DecodeError),
450
451    #[error("Time parsing error: {0}")]
452    TimeError(#[from] time::error::ComponentRange),
453
454    #[error("Cryptographic error: {0}")]
455    CryptoError(String),
456
457    #[error("Encryption error: {0}")]
458    EncryptionError(String),
459
460    #[error("Decryption error: {0}")]
461    DecryptionError(String),
462
463    #[error("Token parsing error: {0}")]
464    TokenParsingError(String),
465}
466
467// Constants for token formatting
468//
469// IMPORTANT: These prefixes use a non-standard versioning scheme!
470// The "pq1" here indicates "post-quantum era" tokens, NOT the classical
471// algorithms defined in the official PASETO specification.
472//
473// This creates intentional incompatibility with standard PASETO tooling
474// to prevent accidental mixing of classical and post-quantum tokens.
475
476/// Token prefix for public (signature-based) post-quantum tokens
477///
478/// Uses `pq1` versioning to clearly distinguish from standard PASETO tokens.
479/// Standard PASETO v1 uses RSA signatures, while this uses ML-DSA post-quantum signatures.
480pub const TOKEN_PREFIX_PUBLIC: &str = "paseto.pq1.public";
481
482/// Token prefix for local (symmetric encryption) post-quantum tokens
483///
484/// Uses `pq1` versioning to clearly distinguish from standard PASETO tokens.
485/// Standard PASETO v1 uses HMAC, while this uses ChaCha20-Poly1305 with ML-KEM key exchange.
486pub const TOKEN_PREFIX_LOCAL: &str = "paseto.pq1.local";
487
488const MAX_TOKEN_SIZE: usize = 1024 * 1024; // 1MB max token size
489const SYMMETRIC_KEY_SIZE: usize = 32;
490const NONCE_SIZE: usize = 12;
491
492impl KeyPair {
493    /// Generate a new post-quantum key pair
494    #[cfg_attr(feature = "logging", instrument(skip(rng)))]
495    pub fn generate<R: CryptoRng + RngCore>(rng: &mut R) -> Self {
496        let keypair = MlDsaParam::key_gen(rng);
497
498        #[cfg(feature = "logging")]
499        debug!("Generated new ML-DSA key pair");
500
501        Self {
502            signing_key: SigningKey(keypair.signing_key().clone()),
503            verifying_key: VerifyingKey(keypair.verifying_key().clone()),
504        }
505    }
506
507    /// Get a reference to the signing key
508    pub fn signing_key(&self) -> &SigningKey {
509        &self.signing_key
510    }
511
512    /// Get a reference to the verifying key
513    pub fn verifying_key(&self) -> &VerifyingKey {
514        &self.verifying_key
515    }
516
517    /// Export the signing key as bytes
518    pub fn signing_key_to_bytes(&self) -> Vec<u8> {
519        let encoded = self.signing_key.0.encode();
520        encoded.to_vec()
521    }
522
523    /// Import signing key from bytes
524    pub fn signing_key_from_bytes(bytes: &[u8]) -> Result<SigningKey, PqPasetoError> {
525        let encoded = ml_dsa::EncodedSigningKey::<MlDsaParam>::try_from(bytes)
526            .map_err(|e| PqPasetoError::CryptoError(format!("Invalid key bytes: {:?}", e)))?;
527        let key = ml_dsa::SigningKey::<MlDsaParam>::decode(&encoded);
528        Ok(SigningKey(key))
529    }
530
531    /// Export the verifying key as bytes
532    pub fn verifying_key_to_bytes(&self) -> Vec<u8> {
533        let encoded = self.verifying_key.0.encode();
534        encoded.to_vec()
535    }
536
537    /// Import verifying key from bytes
538    pub fn verifying_key_from_bytes(bytes: &[u8]) -> Result<VerifyingKey, PqPasetoError> {
539        let encoded = ml_dsa::EncodedVerifyingKey::<MlDsaParam>::try_from(bytes)
540            .map_err(|e| PqPasetoError::CryptoError(format!("Invalid key bytes: {:?}", e)))?;
541        let key = ml_dsa::VerifyingKey::<MlDsaParam>::decode(&encoded);
542        Ok(VerifyingKey(key))
543    }
544}
545
546impl SymmetricKey {
547    /// Generate a new random symmetric key
548    #[cfg_attr(feature = "logging", instrument(skip(rng)))]
549    pub fn generate<R: CryptoRng + RngCore>(rng: &mut R) -> Self {
550        let mut key_bytes = [0u8; SYMMETRIC_KEY_SIZE];
551        rng.fill_bytes(&mut key_bytes);
552
553        #[cfg(feature = "logging")]
554        debug!("Generated new symmetric key");
555
556        Self(key_bytes)
557    }
558
559    /// Create symmetric key from bytes
560    pub fn from_bytes(bytes: &[u8]) -> Result<Self, PqPasetoError> {
561        if bytes.len() != SYMMETRIC_KEY_SIZE {
562            return Err(PqPasetoError::CryptoError(format!(
563                "Invalid symmetric key length: expected {}, got {}",
564                SYMMETRIC_KEY_SIZE,
565                bytes.len()
566            )));
567        }
568        let mut key_bytes = [0u8; SYMMETRIC_KEY_SIZE];
569        key_bytes.copy_from_slice(bytes);
570        Ok(Self(key_bytes))
571    }
572
573    /// Export symmetric key as bytes
574    pub fn to_bytes(&self) -> [u8; SYMMETRIC_KEY_SIZE] {
575        self.0
576    }
577
578    /// Derive a symmetric key from shared secret using proper HKDF-SHA256
579    ///
580    /// Uses RFC 5869 HKDF with SHA-256 for cryptographically sound key derivation.
581    /// The salt is set to None for domain separation, following best practices
582    /// for post-quantum key exchange scenarios.
583    pub fn derive_from_shared_secret(shared_secret: &[u8], info: &[u8]) -> Self {
584        // Use proper HKDF with SHA-256 (no salt - appropriate for PQ key exchange)
585        let hk = Hkdf::<Sha256>::new(None, shared_secret);
586
587        let mut key_bytes = [0u8; SYMMETRIC_KEY_SIZE];
588        hk.expand(info, &mut key_bytes)
589            .expect("SYMMETRIC_KEY_SIZE (32) is valid for SHA-256 HKDF output");
590
591        Self(key_bytes)
592    }
593}
594
595impl KemKeyPair {
596    /// Generate a new post-quantum KEM key pair using ML-KEM-768
597    #[cfg_attr(feature = "logging", instrument(skip(_rng)))]
598    pub fn generate<R: CryptoRng + RngCore>(_rng: &mut R) -> Self {
599        // Generate actual ML-KEM-768 key pair
600        let (dk, ek) = MlKem768::generate(&mut chacha20poly1305::aead::OsRng);
601
602        #[cfg(feature = "logging")]
603        debug!("Generated new ML-KEM-768 key pair");
604
605        Self {
606            encapsulation_key: EncapsulationKey(ek),
607            decapsulation_key: DecapsulationKey(dk),
608        }
609    }
610
611    /// Export the encapsulation key as bytes
612    pub fn encapsulation_key_to_bytes(&self) -> Vec<u8> {
613        use ml_kem::EncodedSizeUser;
614        self.encapsulation_key.0.as_bytes().to_vec()
615    }
616
617    /// Import encapsulation key from bytes
618    pub fn encapsulation_key_from_bytes(bytes: &[u8]) -> Result<EncapsulationKey, PqPasetoError> {
619        use ml_kem::{EncodedSizeUser, array::Array};
620        if bytes.len() != 1184 {
621            return Err(PqPasetoError::CryptoError(
622                "Invalid encapsulation key length".to_string(),
623            ));
624        }
625        let array: Array<u8, _> = Array::try_from(bytes)
626            .map_err(|_| PqPasetoError::CryptoError("Invalid key format".to_string()))?;
627        Ok(EncapsulationKey(
628            <MlKem768 as KemCore>::EncapsulationKey::from_bytes(&array),
629        ))
630    }
631
632    /// Export the decapsulation key as bytes
633    pub fn decapsulation_key_to_bytes(&self) -> Vec<u8> {
634        use ml_kem::EncodedSizeUser;
635        self.decapsulation_key.0.as_bytes().to_vec()
636    }
637
638    /// Import decapsulation key from bytes
639    pub fn decapsulation_key_from_bytes(bytes: &[u8]) -> Result<DecapsulationKey, PqPasetoError> {
640        use ml_kem::{EncodedSizeUser, array::Array};
641        if bytes.len() != 2400 {
642            return Err(PqPasetoError::CryptoError(
643                "Invalid decapsulation key length".to_string(),
644            ));
645        }
646        let array: Array<u8, _> = Array::try_from(bytes)
647            .map_err(|_| PqPasetoError::CryptoError("Invalid key format".to_string()))?;
648        Ok(DecapsulationKey(
649            <MlKem768 as KemCore>::DecapsulationKey::from_bytes(&array),
650        ))
651    }
652
653    /// Perform key encapsulation (sender side) using ML-KEM-768
654    pub fn encapsulate(&self) -> (SymmetricKey, Vec<u8>) {
655        // Use real ML-KEM-768 encapsulation with OsRng for compatibility
656        let (ciphertext, shared_secret) = self
657            .encapsulation_key
658            .0
659            .encapsulate(&mut chacha20poly1305::aead::OsRng)
660            .unwrap();
661
662        let symmetric_key = SymmetricKey::derive_from_shared_secret(
663            shared_secret.as_slice(),
664            b"PASETO-PQ-LOCAL-pq1",
665        );
666
667        (symmetric_key, ciphertext.as_slice().to_vec())
668    }
669
670    /// Perform key decapsulation (receiver side) using ML-KEM-768
671    pub fn decapsulate(&self, ciphertext: &[u8]) -> Result<SymmetricKey, PqPasetoError> {
672        use ml_kem::array::Array;
673
674        // Parse ciphertext into the correct type
675        if ciphertext.len() != 1088 {
676            return Err(PqPasetoError::CryptoError(
677                "Invalid ciphertext length".to_string(),
678            ));
679        }
680
681        let ct_array: Array<u8, _> = Array::try_from(ciphertext)
682            .map_err(|_| PqPasetoError::CryptoError("Invalid ciphertext format".to_string()))?;
683        let ct = ml_kem::Ciphertext::<MlKem768>::from(ct_array);
684
685        // Use real ML-KEM-768 decapsulation
686        let shared_secret = self.decapsulation_key.0.decapsulate(&ct).unwrap();
687
688        Ok(SymmetricKey::derive_from_shared_secret(
689            shared_secret.as_ref(),
690            b"PASETO-PQ-LOCAL-pq1",
691        ))
692    }
693}
694
695impl Claims {
696    /// Create a new empty claims set
697    pub fn new() -> Self {
698        Self {
699            iss: None,
700            sub: None,
701            aud: None,
702            exp: None,
703            nbf: None,
704            iat: None,
705            jti: None,
706            kid: None,
707            custom: HashMap::new(),
708        }
709    }
710
711    /// Set the issuer claim
712    pub fn set_issuer(&mut self, issuer: impl Into<String>) -> Result<(), PqPasetoError> {
713        self.iss = Some(issuer.into());
714        Ok(())
715    }
716
717    /// Set the subject claim
718    pub fn set_subject(&mut self, subject: impl Into<String>) -> Result<(), PqPasetoError> {
719        self.sub = Some(subject.into());
720        Ok(())
721    }
722
723    /// Set the audience claim
724    pub fn set_audience(&mut self, audience: impl Into<String>) -> Result<(), PqPasetoError> {
725        self.aud = Some(audience.into());
726        Ok(())
727    }
728
729    /// Set the expiration time
730    pub fn set_expiration(&mut self, exp: OffsetDateTime) -> Result<(), PqPasetoError> {
731        self.exp = Some(exp);
732        Ok(())
733    }
734
735    /// Set the not-before time
736    pub fn set_not_before(&mut self, nbf: OffsetDateTime) -> Result<(), PqPasetoError> {
737        self.nbf = Some(nbf);
738        Ok(())
739    }
740
741    /// Set the issued-at time
742    pub fn set_issued_at(&mut self, iat: OffsetDateTime) -> Result<(), PqPasetoError> {
743        self.iat = Some(iat);
744        Ok(())
745    }
746
747    /// Set the token identifier
748    pub fn set_jti(&mut self, jti: impl Into<String>) -> Result<(), PqPasetoError> {
749        self.jti = Some(jti.into());
750        Ok(())
751    }
752
753    /// Set the key identifier
754    pub fn set_kid(&mut self, kid: impl Into<String>) -> Result<(), PqPasetoError> {
755        self.kid = Some(kid.into());
756        Ok(())
757    }
758
759    /// Add a custom claim
760    pub fn add_custom(
761        &mut self,
762        key: impl Into<String>,
763        value: impl Serialize,
764    ) -> Result<(), PqPasetoError> {
765        let value = serde_json::to_value(value)?;
766        self.custom.insert(key.into(), value);
767        Ok(())
768    }
769
770    /// Get a custom claim
771    pub fn get_custom(&self, key: &str) -> Option<&Value> {
772        self.custom.get(key)
773    }
774
775    /// Validate time-based claims
776    pub fn validate_time(
777        &self,
778        now: OffsetDateTime,
779        clock_skew_tolerance: time::Duration,
780    ) -> Result<(), PqPasetoError> {
781        // Check expiration
782        if let Some(exp) = self.exp {
783            if now > exp + clock_skew_tolerance {
784                return Err(PqPasetoError::TokenExpired);
785            }
786        }
787
788        // Check not-before
789        if let Some(nbf) = self.nbf {
790            if now < nbf - clock_skew_tolerance {
791                return Err(PqPasetoError::TokenNotYetValid);
792            }
793        }
794
795        Ok(())
796    }
797
798    // Getters
799    pub fn issuer(&self) -> Option<&str> {
800        self.iss.as_deref()
801    }
802    pub fn subject(&self) -> Option<&str> {
803        self.sub.as_deref()
804    }
805    pub fn audience(&self) -> Option<&str> {
806        self.aud.as_deref()
807    }
808    pub fn expiration(&self) -> Option<OffsetDateTime> {
809        self.exp
810    }
811    pub fn not_before(&self) -> Option<OffsetDateTime> {
812        self.nbf
813    }
814    pub fn issued_at(&self) -> Option<OffsetDateTime> {
815        self.iat
816    }
817    pub fn jti(&self) -> Option<&str> {
818        self.jti.as_deref()
819    }
820    pub fn kid(&self) -> Option<&str> {
821        self.kid.as_deref()
822    }
823
824    /// Convert claims to a JSON value
825    ///
826    /// This method provides easy integration with logging, databases, and tracing systems.
827    ///
828    /// # Example
829    ///
830    /// ```rust,no_run
831    /// use paseto_pq::Claims;
832    /// use serde_json::Value;
833    ///
834    /// let mut claims = Claims::new();
835    /// claims.set_subject("user123").unwrap();
836    /// claims.add_custom("role", "admin").unwrap();
837    ///
838    /// let json_value: Value = claims.to_json_value();
839    /// println!("Claims as JSON: {}", json_value);
840    /// ```
841    pub fn to_json_value(&self) -> serde_json::Value {
842        serde_json::Value::from(self.clone())
843    }
844
845    /// Convert claims to a JSON string
846    ///
847    /// This method provides easy integration with logging, databases, and tracing systems.
848    ///
849    /// # Example
850    ///
851    /// ```rust,no_run
852    /// use paseto_pq::Claims;
853    ///
854    /// let mut claims = Claims::new();
855    /// claims.set_subject("user123").unwrap();
856    /// claims.add_custom("role", "admin").unwrap();
857    ///
858    /// let json_string = claims.to_json_string().unwrap();
859    /// println!("User claims: {}", json_string);
860    /// ```
861    pub fn to_json_string(&self) -> Result<String, serde_json::Error> {
862        serde_json::to_string(self)
863    }
864
865    /// Convert claims to a pretty-printed JSON string
866    ///
867    /// Useful for debugging and development environments.
868    ///
869    /// # Example
870    ///
871    /// ```rust,no_run
872    /// use paseto_pq::Claims;
873    ///
874    /// let mut claims = Claims::new();
875    /// claims.set_subject("user123").unwrap();
876    /// claims.add_custom("role", "admin").unwrap();
877    ///
878    /// let pretty_json = claims.to_json_string_pretty().unwrap();
879    /// println!("Claims:\n{}", pretty_json);
880    /// ```
881    pub fn to_json_string_pretty(&self) -> Result<String, serde_json::Error> {
882        serde_json::to_string_pretty(self)
883    }
884}
885
886impl Default for Claims {
887    fn default() -> Self {
888        Self::new()
889    }
890}
891
892/// Convert Claims to serde_json::Value for easy integration with logging, databases, and tracing
893///
894/// # Example
895///
896/// ```rust,no_run
897/// use paseto_pq::Claims;
898/// use serde_json::Value;
899///
900/// let mut claims = Claims::new();
901/// claims.set_subject("user123").unwrap();
902/// claims.add_custom("tenant_id", "org_abc123").unwrap();
903///
904/// // Direct conversion
905/// let json_value: Value = claims.into();
906///
907/// // Use in logging
908/// println!("User authenticated with claims: {}", json_value);
909///
910/// // Use in database operations
911/// // db.insert_audit_log(json_value).await?;
912/// ```
913impl From<Claims> for serde_json::Value {
914    fn from(claims: Claims) -> Self {
915        // Use serde to convert the Claims to JSON Value
916        // This leverages the existing Serialize implementation on Claims
917        serde_json::to_value(claims).unwrap_or(serde_json::Value::Null)
918    }
919}
920
921/// Convert &Claims to serde_json::Value (borrowed version)
922impl From<&Claims> for serde_json::Value {
923    fn from(claims: &Claims) -> Self {
924        serde_json::to_value(claims).unwrap_or(serde_json::Value::Null)
925    }
926}
927
928impl TokenSizeBreakdown {
929    /// Get the total size from all components
930    pub fn total(&self) -> usize {
931        self.prefix
932            + self.payload
933            + self.signature_or_tag
934            + self.footer.unwrap_or(0)
935            + self.separators
936            + self.base64_overhead
937    }
938}
939
940impl TokenSizeEstimator {
941    /// Estimate the size of a public token
942    ///
943    /// # Arguments
944    ///
945    /// * `claims` - The claims that will be included in the token
946    /// * `has_footer` - Whether the token will include a footer
947    ///
948    /// # Example
949    ///
950    /// ```rust,no_run
951    /// use paseto_pq::{Claims, TokenSizeEstimator};
952    ///
953    /// let mut claims = Claims::new();
954    /// claims.set_subject("user123").unwrap();
955    ///
956    /// let estimator = TokenSizeEstimator::public(&claims, false);
957    /// println!("Public token will be ~{} bytes", estimator.total_bytes());
958    /// # Ok::<(), paseto_pq::PqPasetoError>(())
959    /// ```
960    pub fn public(claims: &Claims, has_footer: bool) -> Self {
961        // Serialize claims to get actual payload size
962        let claims_json = serde_json::to_string(claims).unwrap_or_default();
963        let claims_bytes = claims_json.len();
964
965        // Calculate base64 encoded payload size
966        let payload_b64_len = claims_bytes.div_ceil(3) * 4; // Base64 encoding
967
968        // Constants for public tokens
969        let prefix_len = TOKEN_PREFIX_PUBLIC.len() + 1; // +1 for trailing dot
970        // Signature size varies by parameter set
971        let signature_len = if cfg!(feature = "ml-dsa-44") {
972            2800 // ML-DSA-44 signature is smaller
973        } else if cfg!(feature = "ml-dsa-65") {
974            4300 // ML-DSA-65 signature is ~2,420 bytes -> ~3,227 base64 -> actual ~4.3KB
975        } else {
976            5000 // ML-DSA-87 signature is largest
977        };
978        let footer_len = if has_footer { 150 } else { 0 }; // Estimated footer size
979        let separators = if has_footer { 3 } else { 2 }; // Dots between parts
980        let base64_overhead = (claims_bytes * 4).div_ceil(3) - claims_bytes; // More accurate base64 overhead
981
982        let breakdown = TokenSizeBreakdown {
983            prefix: prefix_len,
984            payload: payload_b64_len,
985            signature_or_tag: signature_len,
986            footer: if has_footer { Some(footer_len) } else { None },
987            separators,
988            base64_overhead,
989        };
990
991        Self { breakdown }
992    }
993
994    /// Estimate the size of a local token
995    ///
996    /// # Arguments
997    ///
998    /// * `claims` - The claims that will be included in the token
999    /// * `has_footer` - Whether the token will include a footer
1000    ///
1001    /// # Example
1002    ///
1003    /// ```rust,no_run
1004    /// use paseto_pq::{Claims, TokenSizeEstimator};
1005    ///
1006    /// let mut claims = Claims::new();
1007    /// claims.set_subject("user123").unwrap();
1008    ///
1009    /// let estimator = TokenSizeEstimator::local(&claims, false);
1010    /// println!("Local token will be ~{} bytes", estimator.total_bytes());
1011    /// # Ok::<(), paseto_pq::PqPasetoError>(())
1012    /// ```
1013    pub fn local(claims: &Claims, has_footer: bool) -> Self {
1014        // Serialize claims to get actual payload size
1015        let claims_json = serde_json::to_string(claims).unwrap_or_default();
1016        let claims_bytes = claims_json.len();
1017
1018        // Local tokens encrypt the payload, add nonce (12 bytes) and auth tag (16 bytes)
1019        let encrypted_payload_len = claims_bytes + 12 + 16; // nonce + tag
1020        let payload_b64_len = encrypted_payload_len.div_ceil(3) * 4; // Base64 encoding
1021
1022        // Constants for local tokens
1023        let prefix_len = TOKEN_PREFIX_LOCAL.len() + 1; // +1 for trailing dot
1024        let footer_len = if has_footer { 150 } else { 0 }; // Estimated footer size
1025        let separators = if has_footer { 2 } else { 1 }; // Dots between parts
1026        let base64_overhead = (encrypted_payload_len * 4).div_ceil(3) - encrypted_payload_len; // More accurate base64 overhead
1027
1028        let breakdown = TokenSizeBreakdown {
1029            prefix: prefix_len,
1030            payload: payload_b64_len,
1031            signature_or_tag: 0, // Local tokens don't have separate signature
1032            footer: if has_footer { Some(footer_len) } else { None },
1033            separators,
1034            base64_overhead,
1035        };
1036
1037        Self { breakdown }
1038    }
1039
1040    /// Get the estimated total size in bytes
1041    pub fn total_bytes(&self) -> usize {
1042        self.breakdown.total()
1043    }
1044
1045    /// Check if the token fits within typical cookie size limits (4KB)
1046    pub fn fits_in_cookie(&self) -> bool {
1047        self.total_bytes() <= 4096
1048    }
1049
1050    /// Check if the token fits within typical URL length limits (2KB)
1051    pub fn fits_in_url(&self) -> bool {
1052        self.total_bytes() <= 2048
1053    }
1054
1055    /// Check if the token fits within typical HTTP header limits (8KB)
1056    pub fn fits_in_header(&self) -> bool {
1057        self.total_bytes() <= 8192
1058    }
1059
1060    /// Get detailed breakdown of size components
1061    pub fn breakdown(&self) -> &TokenSizeBreakdown {
1062        &self.breakdown
1063    }
1064
1065    /// Get optimization suggestions if the token is large
1066    pub fn optimization_suggestions(&self) -> Vec<String> {
1067        let mut suggestions = Vec::new();
1068        let total = self.total_bytes();
1069
1070        if total > 4096 {
1071            suggestions.push("Token exceeds cookie size limit (4KB)".to_string());
1072            suggestions.push("Consider using shorter claim values".to_string());
1073            suggestions.push("Move large data to footer or external storage".to_string());
1074            suggestions.push("Use local tokens for internal services (smaller)".to_string());
1075        }
1076
1077        if total > 2048 {
1078            suggestions.push("Token exceeds URL length limits".to_string());
1079            suggestions.push("Avoid passing token in query parameters".to_string());
1080        }
1081
1082        if self.breakdown.payload > total / 2 {
1083            suggestions.push("Payload is majority of token size - reduce claim data".to_string());
1084        }
1085
1086        if self.breakdown.footer.unwrap_or(0) > 200 {
1087            suggestions.push("Footer is large - consider minimal metadata only".to_string());
1088        }
1089
1090        suggestions
1091    }
1092
1093    /// Compare token size to typical JWT tokens
1094    pub fn compare_to_jwt(&self) -> String {
1095        let jwt_typical = 200; // Typical JWT size
1096        let ratio = self.total_bytes() as f64 / jwt_typical as f64;
1097        format!(
1098            "{:.1}x larger than typical JWT ({} bytes)",
1099            ratio, jwt_typical
1100        )
1101    }
1102
1103    /// Get a human-readable size summary
1104    pub fn size_summary(&self) -> String {
1105        format!(
1106            "Token size: {} bytes (payload: {}, signature: {}, overhead: {})",
1107            self.total_bytes(),
1108            self.breakdown.payload,
1109            self.breakdown.signature_or_tag,
1110            self.breakdown.base64_overhead + self.breakdown.separators + self.breakdown.prefix
1111        )
1112    }
1113}
1114
1115impl ParsedToken {
1116    /// Parse a PASETO token string to extract structural information
1117    ///
1118    /// This method performs **no cryptographic operations** - it only parses the token
1119    /// structure to extract metadata. Use this for debugging, logging, middleware,
1120    /// and routing decisions.
1121    ///
1122    /// # Arguments
1123    ///
1124    /// * `token` - The PASETO token string to parse
1125    ///
1126    /// # Returns
1127    ///
1128    /// Returns a `ParsedToken` containing the token's structural information,
1129    /// or an error if the token format is invalid.
1130    ///
1131    /// # Example
1132    ///
1133    /// ```rust,no_run
1134    /// use paseto_pq::ParsedToken;
1135    ///
1136    /// let token = "paseto.pq1.public.ABC123.DEF456.eyJraWQiOiJ0ZXN0In0";
1137    /// let parsed = ParsedToken::parse(token)?;
1138    ///
1139    /// assert_eq!(parsed.purpose(), "public");
1140    /// assert_eq!(parsed.version(), "pq1");
1141    /// assert!(parsed.has_footer());
1142    /// # Ok::<(), paseto_pq::PqPasetoError>(())
1143    /// ```
1144    pub fn parse(token: &str) -> Result<Self, PqPasetoError> {
1145        let parts: Vec<&str> = token.split('.').collect();
1146
1147        // Validate minimum structure: paseto.pq1.purpose.payload
1148        if parts.len() < 4 {
1149            return Err(PqPasetoError::TokenParsingError(format!(
1150                "Invalid token format: expected at least 4 parts, got {}",
1151                parts.len()
1152            )));
1153        }
1154
1155        // Validate protocol
1156        if parts[0] != "paseto" {
1157            return Err(PqPasetoError::TokenParsingError(format!(
1158                "Invalid protocol: expected 'paseto', got '{}'",
1159                parts[0]
1160            )));
1161        }
1162
1163        // Extract version
1164        let version = parts[1].to_string();
1165
1166        // Extract purpose
1167        let purpose = parts[2].to_string();
1168
1169        // Validate known formats
1170        match (version.as_str(), purpose.as_str()) {
1171            ("pq1", "public") | ("pq1", "local") => {}
1172            _ => {
1173                return Err(PqPasetoError::TokenParsingError(format!(
1174                    "Unsupported token format: {}.{}.{}",
1175                    parts[0], parts[1], parts[2]
1176                )));
1177            }
1178        }
1179
1180        // Decode payload
1181        let payload = URL_SAFE_NO_PAD.decode(parts[3]).map_err(|e| {
1182            PqPasetoError::TokenParsingError(format!("Invalid payload base64: {}", e))
1183        })?;
1184
1185        let mut signature_or_tag = None;
1186        let mut footer = None;
1187
1188        // Parse remaining parts based on token type
1189        match purpose.as_str() {
1190            "public" => {
1191                // Public tokens: paseto.pq1.public.payload.signature[.footer]
1192                if parts.len() > 6 {
1193                    return Err(PqPasetoError::TokenParsingError(
1194                        "Public token has too many parts".to_string(),
1195                    ));
1196                }
1197                if parts.len() >= 5 {
1198                    signature_or_tag = Some(URL_SAFE_NO_PAD.decode(parts[4]).map_err(|e| {
1199                        PqPasetoError::TokenParsingError(format!("Invalid signature base64: {}", e))
1200                    })?);
1201                }
1202                if parts.len() >= 6 {
1203                    footer = Some(Footer::from_base64(parts[5])?);
1204                }
1205            }
1206            "local" => {
1207                // Local tokens: paseto.pq1.local.payload[.footer]
1208                if parts.len() > 5 {
1209                    return Err(PqPasetoError::TokenParsingError(
1210                        "Local token has too many parts".to_string(),
1211                    ));
1212                }
1213                if parts.len() >= 5 {
1214                    footer = Some(Footer::from_base64(parts[4])?);
1215                }
1216            }
1217            _ => unreachable!(), // Already validated above
1218        }
1219
1220        Ok(ParsedToken {
1221            purpose,
1222            version,
1223            payload,
1224            signature_or_tag,
1225            footer,
1226            raw_token: token.to_string(),
1227        })
1228    }
1229
1230    /// Get the token purpose ("public" for public tokens, "local" for local tokens)
1231    pub fn purpose(&self) -> &str {
1232        &self.purpose
1233    }
1234
1235    /// Get the token version (currently "pq1")
1236    pub fn version(&self) -> &str {
1237        &self.version
1238    }
1239
1240    /// Check if the token has a footer
1241    pub fn has_footer(&self) -> bool {
1242        self.footer.is_some()
1243    }
1244
1245    /// Get the footer, if present
1246    pub fn footer(&self) -> Option<&Footer> {
1247        self.footer.as_ref()
1248    }
1249
1250    /// Get the raw payload bytes (base64-decoded)
1251    pub fn payload_bytes(&self) -> &[u8] {
1252        &self.payload
1253    }
1254
1255    /// Get the signature or authentication tag bytes, if present
1256    ///
1257    /// For public tokens, this is the ML-DSA signature.
1258    /// For local tokens, this is None (auth tag is embedded in payload).
1259    pub fn signature_bytes(&self) -> Option<&[u8]> {
1260        self.signature_or_tag.as_deref()
1261    }
1262
1263    /// Get the length of the payload in bytes
1264    pub fn payload_length(&self) -> usize {
1265        self.payload.len()
1266    }
1267
1268    /// Get the total length of the token string
1269    pub fn total_length(&self) -> usize {
1270        self.raw_token.len()
1271    }
1272
1273    /// Get the raw token string
1274    pub fn raw_token(&self) -> &str {
1275        &self.raw_token
1276    }
1277
1278    /// Get footer as JSON string, if present
1279    pub fn footer_json(&self) -> Option<Result<String, serde_json::Error>> {
1280        self.footer.as_ref().map(serde_json::to_string)
1281    }
1282
1283    /// Get footer as pretty-printed JSON string, if present
1284    pub fn footer_json_pretty(&self) -> Option<Result<String, serde_json::Error>> {
1285        self.footer.as_ref().map(serde_json::to_string_pretty)
1286    }
1287
1288    /// Check if this is a public token (uses signatures)
1289    pub fn is_public(&self) -> bool {
1290        self.purpose == "public"
1291    }
1292
1293    /// Check if this is a local token (uses symmetric encryption)
1294    pub fn is_local(&self) -> bool {
1295        self.purpose == "local"
1296    }
1297
1298    /// Get token format summary for debugging
1299    pub fn format_summary(&self) -> String {
1300        format!(
1301            "paseto.{}.{} (payload: {} bytes, signature: {}, footer: {})",
1302            self.version,
1303            self.purpose,
1304            self.payload.len(),
1305            if self.signature_or_tag.is_some() {
1306                "present"
1307            } else {
1308                "none"
1309            },
1310            if self.footer.is_some() {
1311                "present"
1312            } else {
1313                "none"
1314            }
1315        )
1316    }
1317}
1318
1319impl VerifiedToken {
1320    /// Get the claims from the verified token
1321    pub fn claims(&self) -> &Claims {
1322        &self.claims
1323    }
1324
1325    /// Get the footer from the verified token, if present
1326    pub fn footer(&self) -> Option<&Footer> {
1327        self.footer.as_ref()
1328    }
1329
1330    /// Get the raw token string
1331    pub fn raw_token(&self) -> &str {
1332        &self.raw_token
1333    }
1334
1335    /// Consume the verified token and return the claims
1336    pub fn into_claims(self) -> Claims {
1337        self.claims
1338    }
1339
1340    /// Consume the verified token and return both claims and footer
1341    pub fn into_parts(self) -> (Claims, Option<Footer>) {
1342        (self.claims, self.footer)
1343    }
1344}
1345
1346impl PasetoPQ {
1347    /// Get the current token prefix used for public tokens
1348    ///
1349    /// Returns the prefix string used in public token generation.
1350    /// This allows applications to inspect the versioning scheme being used.
1351    pub fn public_token_prefix() -> &'static str {
1352        TOKEN_PREFIX_PUBLIC
1353    }
1354
1355    /// Get the current token prefix used for local tokens
1356    ///
1357    /// Returns the prefix string used in local token generation.
1358    /// This allows applications to inspect the versioning scheme being used.
1359    pub fn local_token_prefix() -> &'static str {
1360        TOKEN_PREFIX_LOCAL
1361    }
1362
1363    /// Check if this implementation uses standard PASETO versioning
1364    ///
1365    /// Returns `false` because this crate uses non-standard `pq1` versioning
1366    /// that is incompatible with the official PASETO specification.
1367    pub fn is_standard_paseto_compatible() -> bool {
1368        false
1369    }
1370    /// Parse a PASETO token for inspection without cryptographic operations
1371    ///
1372    /// This method allows you to examine token structure, purpose, version, and footer
1373    /// without performing expensive signature verification or decryption. Useful for
1374    /// debugging, logging, middleware, and routing decisions.
1375    ///
1376    /// # Arguments
1377    ///
1378    /// * `token` - The PASETO token string to parse
1379    ///
1380    /// # Example
1381    ///
1382    /// ```rust,no_run
1383    /// use paseto_pq::PasetoPQ;
1384    ///
1385    /// let token = "paseto.pq1.public.ABC123...";
1386    /// let parsed = PasetoPQ::parse_token(token)?;
1387    ///
1388    /// println!("Token type: {}", parsed.purpose());
1389    /// println!("Has footer: {}", parsed.has_footer());
1390    ///
1391    /// // Route based on token type
1392    /// match parsed.purpose() {
1393    ///     "public" => println!("Public token - needs signature verification"),
1394    ///     "local" => println!("Local token - needs decryption"),
1395    ///     _ => println!("Unknown token type"),
1396    /// }
1397    /// # Ok::<(), paseto_pq::PqPasetoError>(())
1398    /// ```
1399    pub fn parse_token(token: &str) -> Result<ParsedToken, PqPasetoError> {
1400        ParsedToken::parse(token)
1401    }
1402
1403    /// Estimate the size of a public token before creation
1404    ///
1405    /// This method helps you plan token usage and avoid size-related issues
1406    /// with HTTP headers, cookies, or URL length limits.
1407    ///
1408    /// # Arguments
1409    ///
1410    /// * `claims` - The claims that will be included in the token
1411    /// * `has_footer` - Whether the token will include a footer
1412    ///
1413    /// # Example
1414    ///
1415    /// ```rust,no_run
1416    /// use paseto_pq::{PasetoPQ, Claims};
1417    ///
1418    /// let mut claims = Claims::new();
1419    /// claims.set_subject("user123").unwrap();
1420    /// claims.add_custom("role", "admin").unwrap();
1421    ///
1422    /// let estimator = PasetoPQ::estimate_public_size(&claims, false);
1423    /// println!("Token will be ~{} bytes", estimator.total_bytes());
1424    ///
1425    /// if !estimator.fits_in_cookie() {
1426    ///     println!("Warning: Token too large for cookies!");
1427    /// }
1428    /// # Ok::<(), paseto_pq::PqPasetoError>(())
1429    /// ```
1430    pub fn estimate_public_size(claims: &Claims, has_footer: bool) -> TokenSizeEstimator {
1431        TokenSizeEstimator::public(claims, has_footer)
1432    }
1433
1434    /// Estimate the size of a local token before creation
1435    ///
1436    /// This method helps you plan token usage and avoid size-related issues
1437    /// with HTTP headers, cookies, or URL length limits.
1438    ///
1439    /// # Arguments
1440    ///
1441    /// * `claims` - The claims that will be included in the token
1442    /// * `has_footer` - Whether the token will include a footer
1443    ///
1444    /// # Example
1445    ///
1446    /// ```rust,no_run
1447    /// use paseto_pq::{PasetoPQ, Claims};
1448    ///
1449    /// let mut claims = Claims::new();
1450    /// claims.set_subject("user123").unwrap();
1451    /// claims.add_custom("session_data", "confidential").unwrap();
1452    ///
1453    /// let estimator = PasetoPQ::estimate_local_size(&claims, true);
1454    /// println!("Token will be ~{} bytes", estimator.total_bytes());
1455    ///
1456    /// if estimator.fits_in_header() {
1457    ///     println!("Token fits in HTTP headers");
1458    /// }
1459    /// # Ok::<(), paseto_pq::PqPasetoError>(())
1460    /// ```
1461    pub fn estimate_local_size(claims: &Claims, has_footer: bool) -> TokenSizeEstimator {
1462        TokenSizeEstimator::local(claims, has_footer)
1463    }
1464
1465    /// Sign claims to create a public token
1466    #[cfg_attr(feature = "logging", instrument(skip(signing_key)))]
1467    pub fn sign(signing_key: &SigningKey, claims: &Claims) -> Result<String, PqPasetoError> {
1468        Self::sign_with_footer(signing_key, claims, None)
1469    }
1470
1471    /// Sign claims with optional footer to create a new public token
1472    #[cfg_attr(feature = "logging", instrument(skip(signing_key)))]
1473    pub fn sign_with_footer(
1474        signing_key: &SigningKey,
1475        claims: &Claims,
1476        footer: Option<&Footer>,
1477    ) -> Result<String, PqPasetoError> {
1478        // Serialize claims to JSON bytes (not base64 for PAE)
1479        let payload_bytes = serde_json::to_vec(claims)?;
1480
1481        #[cfg(feature = "logging")]
1482        debug!("Serialized claims to {} bytes", payload_bytes.len());
1483
1484        // Serialize footer to JSON bytes (empty if None)
1485        let footer_bytes = match footer {
1486            Some(f) => serde_json::to_vec(f)?,
1487            None => Vec::new(), // Empty bytes for no footer
1488        };
1489
1490        // Create PAE message for signing (RFC Section 2.2.1)
1491        // PAE([header, payload_bytes, footer_bytes])
1492        let header = TOKEN_PREFIX_PUBLIC.as_bytes();
1493        let pae_message =
1494            crate::pae::pae_encode_public_token(header, &payload_bytes, &footer_bytes);
1495
1496        #[cfg(feature = "logging")]
1497        debug!(
1498            "Created PAE message of {} bytes for signing",
1499            pae_message.len()
1500        );
1501
1502        // Sign the PAE-encoded message with ML-DSA
1503        let signature = signing_key.0.sign(&pae_message);
1504        let signature_bytes = signature.to_bytes();
1505
1506        // Base64url encode components for token construction
1507        let encoded_payload = URL_SAFE_NO_PAD.encode(&payload_bytes);
1508        let encoded_signature = URL_SAFE_NO_PAD.encode(signature_bytes);
1509
1510        // Construct final token with optional footer
1511        let token = match footer {
1512            Some(f) => {
1513                let footer_b64 = f.to_base64()?;
1514                format!(
1515                    "{}.{}.{}.{}",
1516                    TOKEN_PREFIX_PUBLIC, encoded_payload, encoded_signature, footer_b64
1517                )
1518            }
1519            None => format!(
1520                "{}.{}.{}",
1521                TOKEN_PREFIX_PUBLIC, encoded_payload, encoded_signature
1522            ),
1523        };
1524
1525        #[cfg(feature = "logging")]
1526        debug!(
1527            "Generated v0.1.1 token with {} byte signature and PAE footer authentication{}",
1528            signature_bytes.len(),
1529            if footer.is_some() { " with footer" } else { "" }
1530        );
1531
1532        Ok(token)
1533    }
1534
1535    /// Encrypt claims to create a new local token
1536    #[cfg_attr(feature = "logging", instrument(skip(symmetric_key)))]
1537    pub fn encrypt(symmetric_key: &SymmetricKey, claims: &Claims) -> Result<String, PqPasetoError> {
1538        Self::encrypt_with_footer(symmetric_key, claims, None)
1539    }
1540
1541    /// Encrypt claims with optional footer to create a new local token
1542    #[cfg_attr(feature = "logging", instrument(skip(symmetric_key)))]
1543    pub fn encrypt_with_footer(
1544        symmetric_key: &SymmetricKey,
1545        claims: &Claims,
1546        footer: Option<&Footer>,
1547    ) -> Result<String, PqPasetoError> {
1548        // Serialize claims to JSON bytes
1549        let payload_bytes = serde_json::to_vec(claims)?;
1550
1551        #[cfg(feature = "logging")]
1552        debug!("Serialized claims to {} bytes", payload_bytes.len());
1553
1554        // Create cipher
1555        let cipher = ChaCha20Poly1305::new((&symmetric_key.0).into());
1556
1557        // Generate random nonce
1558        let nonce = ChaCha20Poly1305::generate_nonce(&mut AeadOsRng);
1559
1560        // Serialize footer to JSON bytes (empty if None)
1561        let footer_bytes = match footer {
1562            Some(f) => serde_json::to_vec(f)?,
1563            None => Vec::new(), // Empty bytes for no footer
1564        };
1565
1566        // Create PAE-encoded AAD for footer authentication (RFC Section 2.2.1)
1567        // PAE([header, nonce_bytes, footer_bytes])
1568        let header = TOKEN_PREFIX_LOCAL.as_bytes();
1569        let nonce_bytes = nonce.as_slice();
1570        let aad = crate::pae::pae_encode_local_token(header, nonce_bytes, &footer_bytes);
1571
1572        #[cfg(feature = "logging")]
1573        debug!(
1574            "Created PAE AAD of {} bytes for footer authentication",
1575            aad.len()
1576        );
1577
1578        // Encrypt payload with PAE AAD (footer now authenticated by AEAD!)
1579        let mut buffer = payload_bytes.clone();
1580        let tag = cipher
1581            .encrypt_in_place_detached(&nonce, &aad, &mut buffer)
1582            .map_err(|e| PqPasetoError::EncryptionError(format!("Encryption failed: {}", e)))?;
1583
1584        // Combine encrypted payload with authentication tag
1585        let mut ciphertext = buffer;
1586        ciphertext.extend_from_slice(&tag);
1587
1588        // Combine nonce + ciphertext + tag
1589        let mut encrypted_data = Vec::new();
1590        encrypted_data.extend_from_slice(&nonce);
1591        encrypted_data.extend_from_slice(&ciphertext);
1592
1593        // Base64url encode the encrypted data
1594        let encoded_payload = URL_SAFE_NO_PAD.encode(&encrypted_data);
1595
1596        // Construct final token with optional footer
1597        let token = match footer {
1598            Some(f) => {
1599                let footer_b64 = f.to_base64()?;
1600                format!("{}.{}.{}", TOKEN_PREFIX_LOCAL, encoded_payload, footer_b64)
1601            }
1602            None => format!("{}.{}", TOKEN_PREFIX_LOCAL, encoded_payload),
1603        };
1604
1605        #[cfg(feature = "logging")]
1606        debug!(
1607            "Generated v0.1.1 local token with {} byte payload and PAE footer authentication{}",
1608            encrypted_data.len(),
1609            if footer.is_some() { " with footer" } else { "" }
1610        );
1611
1612        Ok(token)
1613    }
1614
1615    /// Decrypt a local token and extract claims
1616    #[cfg_attr(feature = "logging", instrument(skip(symmetric_key)))]
1617    pub fn decrypt(
1618        symmetric_key: &SymmetricKey,
1619        token: &str,
1620    ) -> Result<VerifiedToken, PqPasetoError> {
1621        Self::decrypt_with_footer(symmetric_key, token)
1622    }
1623
1624    /// Decrypt a local token with footer support and extract claims
1625    #[cfg_attr(feature = "logging", instrument(skip(symmetric_key)))]
1626    pub fn decrypt_with_footer(
1627        symmetric_key: &SymmetricKey,
1628        token: &str,
1629    ) -> Result<VerifiedToken, PqPasetoError> {
1630        // Basic size check
1631        if token.len() > MAX_TOKEN_SIZE {
1632            return Err(PqPasetoError::InvalidFormat("Token too large".into()));
1633        }
1634
1635        // Split token into parts (4 parts without footer, 5 parts with footer)
1636        let parts: Vec<&str> = token.splitn(5, '.').collect();
1637        let (encoded_payload, footer) = if parts.len() == 5 {
1638            // Token with footer: paseto.pq1.local.payload.footer
1639            if parts[0] != "paseto" || parts[1] != "pq1" || parts[2] != "local" {
1640                return Err(PqPasetoError::InvalidFormat(
1641                    "Invalid token format - expected 'paseto.pq1.local'".into(),
1642                ));
1643            }
1644            let footer = Footer::from_base64(parts[4])?;
1645            (parts[3], Some(footer))
1646        } else if parts.len() == 4 {
1647            // Token without footer: paseto.pq1.local.payload
1648            if parts[0] != "paseto" || parts[1] != "pq1" || parts[2] != "local" {
1649                return Err(PqPasetoError::InvalidFormat(
1650                    "Invalid token format - expected 'paseto.pq1.local'".into(),
1651                ));
1652            }
1653            (parts[3], None)
1654        } else {
1655            return Err(PqPasetoError::InvalidFormat(
1656                "Expected 4 or 5 parts separated by '.' for local token".into(),
1657            ));
1658        };
1659
1660        // Decode encrypted payload
1661        let encrypted_data = URL_SAFE_NO_PAD.decode(encoded_payload).map_err(|e| {
1662            PqPasetoError::InvalidFormat(format!("Invalid payload encoding: {}", e))
1663        })?;
1664
1665        // Split nonce, ciphertext, and tag
1666        if encrypted_data.len() < NONCE_SIZE + 16 {
1667            return Err(PqPasetoError::DecryptionError(
1668                "Encrypted data too short for nonce and tag".into(),
1669            ));
1670        }
1671
1672        let nonce = Nonce::from_slice(&encrypted_data[..NONCE_SIZE]);
1673        let ciphertext_with_tag = &encrypted_data[NONCE_SIZE..];
1674
1675        // Split ciphertext and authentication tag (last 16 bytes)
1676        if ciphertext_with_tag.len() < 16 {
1677            return Err(PqPasetoError::DecryptionError(
1678                "Encrypted data too short for authentication tag".into(),
1679            ));
1680        }
1681
1682        let tag_start = ciphertext_with_tag.len() - 16;
1683        let mut ciphertext = ciphertext_with_tag[..tag_start].to_vec();
1684        let tag = &ciphertext_with_tag[tag_start..];
1685
1686        // Serialize footer to JSON bytes (empty if None)
1687        let footer_bytes = match &footer {
1688            Some(f) => serde_json::to_vec(f)?,
1689            None => Vec::new(), // Empty bytes for no footer
1690        };
1691
1692        // Reconstruct PAE-encoded AAD for footer validation (v0.1.1 RFC-compliant)
1693        // PAE([header, nonce_bytes, footer_bytes])
1694        let header = TOKEN_PREFIX_LOCAL.as_bytes();
1695        let nonce_bytes = nonce.as_slice();
1696        let aad = crate::pae::pae_encode_local_token(header, nonce_bytes, &footer_bytes);
1697
1698        #[cfg(feature = "logging")]
1699        debug!(
1700            "Reconstructed PAE AAD of {} bytes for footer validation",
1701            aad.len()
1702        );
1703
1704        // Create cipher and decrypt with PAE AAD (footer tampering now detected!)
1705        let cipher = ChaCha20Poly1305::new((&symmetric_key.0).into());
1706
1707        // Convert tag slice to GenericArray
1708        use chacha20poly1305::aead::generic_array::GenericArray;
1709        let tag_array = GenericArray::from_slice(tag);
1710
1711        let payload_bytes = cipher
1712            .decrypt_in_place_detached(nonce, &aad, &mut ciphertext, tag_array)
1713            .map_err(|e| {
1714                PqPasetoError::DecryptionError(format!(
1715                    "Decryption failed (footer authentication failed): {}",
1716                    e
1717                ))
1718            })
1719            .map(|_| ciphertext)?;
1720
1721        #[cfg(feature = "logging")]
1722        debug!("v0.1.1 PAE decryption successful with footer authentication");
1723
1724        // Parse claims
1725        let claims: Claims = serde_json::from_slice(&payload_bytes)?;
1726
1727        // Basic time validation (with default 30s clock skew tolerance)
1728        claims.validate_time(OffsetDateTime::now_utc(), time::Duration::seconds(30))?;
1729
1730        Ok(VerifiedToken {
1731            claims,
1732            footer,
1733            raw_token: token.to_string(),
1734        })
1735    }
1736
1737    /// Decrypt a local token with custom validation options
1738    pub fn decrypt_with_options(
1739        symmetric_key: &SymmetricKey,
1740        token: &str,
1741        expected_audience: Option<&str>,
1742        expected_issuer: Option<&str>,
1743        clock_skew_tolerance: time::Duration,
1744    ) -> Result<VerifiedToken, PqPasetoError> {
1745        let verified = Self::decrypt(symmetric_key, token)?;
1746
1747        // Validate audience if specified
1748        if let Some(expected_aud) = expected_audience {
1749            match verified.claims.audience() {
1750                Some(actual_aud) if actual_aud == expected_aud => {}
1751                Some(actual_aud) => {
1752                    return Err(PqPasetoError::InvalidAudience {
1753                        expected: expected_aud.to_string(),
1754                        actual: actual_aud.to_string(),
1755                    });
1756                }
1757                None => {
1758                    return Err(PqPasetoError::InvalidAudience {
1759                        expected: expected_aud.to_string(),
1760                        actual: "none".to_string(),
1761                    });
1762                }
1763            }
1764        }
1765
1766        // Validate issuer if specified
1767        if let Some(expected_iss) = expected_issuer {
1768            match verified.claims.issuer() {
1769                Some(actual_iss) if actual_iss == expected_iss => {}
1770                Some(actual_iss) => {
1771                    return Err(PqPasetoError::InvalidIssuer {
1772                        expected: expected_iss.to_string(),
1773                        actual: actual_iss.to_string(),
1774                    });
1775                }
1776                None => {
1777                    return Err(PqPasetoError::InvalidIssuer {
1778                        expected: expected_iss.to_string(),
1779                        actual: "none".to_string(),
1780                    });
1781                }
1782            }
1783        }
1784
1785        // Re-validate time with custom tolerance
1786        verified
1787            .claims
1788            .validate_time(OffsetDateTime::now_utc(), clock_skew_tolerance)?;
1789
1790        Ok(verified)
1791    }
1792
1793    /// Verify a token and extract claims
1794    #[cfg_attr(feature = "logging", instrument(skip(verifying_key)))]
1795    pub fn verify(
1796        verifying_key: &VerifyingKey,
1797        token: &str,
1798    ) -> Result<VerifiedToken, PqPasetoError> {
1799        Self::verify_with_footer(verifying_key, token)
1800    }
1801
1802    /// Verify a token with footer support and extract claims
1803    #[cfg_attr(feature = "logging", instrument(skip(verifying_key)))]
1804    pub fn verify_with_footer(
1805        verifying_key: &VerifyingKey,
1806        token: &str,
1807    ) -> Result<VerifiedToken, PqPasetoError> {
1808        // Basic size check
1809        if token.len() > MAX_TOKEN_SIZE {
1810            return Err(PqPasetoError::InvalidFormat("Token too large".into()));
1811        }
1812
1813        // Split token into parts (5 parts without footer, 6 parts with footer)
1814        let parts: Vec<&str> = token.splitn(6, '.').collect();
1815        let (encoded_payload, encoded_signature, footer) = if parts.len() == 6 {
1816            // Token with footer: paseto.pq1.public.payload.signature.footer
1817            if parts[0] != "paseto" || parts[1] != "pq1" || parts[2] != "public" {
1818                return Err(PqPasetoError::InvalidFormat(
1819                    "Invalid token format - expected 'paseto.pq1.public'".into(),
1820                ));
1821            }
1822            let footer = Footer::from_base64(parts[5])?;
1823            (parts[3], parts[4], Some(footer))
1824        } else if parts.len() == 5 {
1825            // Token without footer: paseto.pq1.public.payload.signature
1826            if parts[0] != "paseto" || parts[1] != "pq1" || parts[2] != "public" {
1827                return Err(PqPasetoError::InvalidFormat(
1828                    "Invalid token format - expected 'paseto.pq1.public'".into(),
1829                ));
1830            }
1831            (parts[3], parts[4], None)
1832        } else {
1833            return Err(PqPasetoError::InvalidFormat(
1834                "Expected 5 or 6 parts separated by '.' for public token".into(),
1835            ));
1836        };
1837
1838        // Decode payload bytes
1839        let payload_bytes = URL_SAFE_NO_PAD.decode(encoded_payload).map_err(|e| {
1840            PqPasetoError::InvalidFormat(format!("Invalid payload encoding: {}", e))
1841        })?;
1842
1843        // Serialize footer to bytes for PAE (empty if None)
1844        let footer_bytes = match &footer {
1845            Some(f) => serde_json::to_vec(f)?,
1846            None => Vec::new(), // Empty bytes for no footer
1847        };
1848
1849        // Reconstruct PAE message that was signed (v0.1.1 RFC-compliant)
1850        // PAE([header, payload_bytes, footer_bytes])
1851        let header = TOKEN_PREFIX_PUBLIC.as_bytes();
1852        let pae_message =
1853            crate::pae::pae_encode_public_token(header, &payload_bytes, &footer_bytes);
1854
1855        #[cfg(feature = "logging")]
1856        debug!(
1857            "Reconstructed PAE message of {} bytes for verification",
1858            pae_message.len()
1859        );
1860
1861        // Decode signature
1862        let signature_bytes = URL_SAFE_NO_PAD.decode(encoded_signature).map_err(|e| {
1863            PqPasetoError::InvalidFormat(format!("Invalid signature encoding: {}", e))
1864        })?;
1865
1866        // Reconstruct signature
1867        let encoded_sig = ml_dsa::EncodedSignature::<MlDsaParam>::try_from(
1868            signature_bytes.as_slice(),
1869        )
1870        .map_err(|e| PqPasetoError::CryptoError(format!("Invalid signature bytes: {:?}", e)))?;
1871        let signature = ml_dsa::Signature::<MlDsaParam>::decode(&encoded_sig)
1872            .ok_or_else(|| PqPasetoError::CryptoError("Failed to decode signature".into()))?;
1873
1874        // Verify signature against PAE message (footer tampering now detected!)
1875        verifying_key
1876            .0
1877            .verify(&pae_message, &signature)
1878            .map_err(|_| PqPasetoError::SignatureVerificationFailed)?;
1879
1880        #[cfg(feature = "logging")]
1881        debug!("v0.1.1 PAE signature verification successful with footer authentication");
1882
1883        // Parse claims
1884        let claims: Claims = serde_json::from_slice(&payload_bytes)?;
1885
1886        // Basic time validation (with default 30s clock skew tolerance)
1887        claims.validate_time(OffsetDateTime::now_utc(), time::Duration::seconds(30))?;
1888
1889        Ok(VerifiedToken {
1890            claims,
1891            footer,
1892            raw_token: token.to_string(),
1893        })
1894    }
1895
1896    /// Verify a token with custom validation options
1897    pub fn verify_with_options(
1898        verifying_key: &VerifyingKey,
1899        token: &str,
1900        expected_audience: Option<&str>,
1901        expected_issuer: Option<&str>,
1902        clock_skew_tolerance: time::Duration,
1903    ) -> Result<VerifiedToken, PqPasetoError> {
1904        let verified = Self::verify_with_footer(verifying_key, token)?;
1905
1906        // Validate audience if specified
1907        if let Some(expected_aud) = expected_audience {
1908            match verified.claims.audience() {
1909                Some(actual_aud) if actual_aud == expected_aud => {}
1910                Some(actual_aud) => {
1911                    return Err(PqPasetoError::InvalidAudience {
1912                        expected: expected_aud.to_string(),
1913                        actual: actual_aud.to_string(),
1914                    });
1915                }
1916                None => {
1917                    return Err(PqPasetoError::InvalidAudience {
1918                        expected: expected_aud.to_string(),
1919                        actual: "none".to_string(),
1920                    });
1921                }
1922            }
1923        }
1924
1925        // Validate issuer if specified
1926        if let Some(expected_iss) = expected_issuer {
1927            match verified.claims.issuer() {
1928                Some(actual_iss) if actual_iss == expected_iss => {}
1929                Some(actual_iss) => {
1930                    return Err(PqPasetoError::InvalidIssuer {
1931                        expected: expected_iss.to_string(),
1932                        actual: actual_iss.to_string(),
1933                    });
1934                }
1935                None => {
1936                    return Err(PqPasetoError::InvalidIssuer {
1937                        expected: expected_iss.to_string(),
1938                        actual: "none".to_string(),
1939                    });
1940                }
1941            }
1942        }
1943
1944        // Re-validate time with custom tolerance
1945        verified
1946            .claims
1947            .validate_time(OffsetDateTime::now_utc(), clock_skew_tolerance)?;
1948
1949        Ok(verified)
1950    }
1951}
1952
1953impl fmt::Debug for SigningKey {
1954    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1955        f.debug_struct("SigningKey")
1956            .field("algorithm", &"ML-DSA-65")
1957            .finish_non_exhaustive()
1958    }
1959}
1960
1961impl fmt::Debug for VerifyingKey {
1962    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1963        f.debug_struct("VerifyingKey")
1964            .field("algorithm", &"ML-DSA-65")
1965            .finish_non_exhaustive()
1966    }
1967}
1968
1969impl fmt::Debug for KeyPair {
1970    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1971        f.debug_struct("KeyPair")
1972            .field("signing_key", &self.signing_key)
1973            .field("verifying_key", &self.verifying_key)
1974            .finish()
1975    }
1976}
1977
1978impl fmt::Debug for SymmetricKey {
1979    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1980        f.debug_struct("SymmetricKey")
1981            .field("algorithm", &"ChaCha20-Poly1305")
1982            .finish_non_exhaustive()
1983    }
1984}
1985
1986impl fmt::Debug for EncapsulationKey {
1987    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1988        f.debug_struct("EncapsulationKey")
1989            .field("algorithm", &"ML-KEM-768")
1990            .finish_non_exhaustive()
1991    }
1992}
1993
1994impl fmt::Debug for DecapsulationKey {
1995    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1996        f.debug_struct("DecapsulationKey")
1997            .field("algorithm", &"ML-KEM-768")
1998            .finish_non_exhaustive()
1999    }
2000}
2001
2002impl fmt::Debug for KemKeyPair {
2003    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2004        f.debug_struct("KemKeyPair")
2005            .field("encapsulation_key", &"<encapsulation_key>")
2006            .field("decapsulation_key", &"<decapsulation_key>")
2007            .finish()
2008    }
2009}
2010
2011// Zeroization implementations for sensitive key material
2012// Note: ML-DSA and ML-KEM keys are opaque types that may handle their own zeroization internally.
2013// We implement Drop for best-effort cleanup, but rely on the underlying libraries for complete zeroization.
2014
2015impl Drop for SigningKey {
2016    fn drop(&mut self) {
2017        // ML-DSA SigningKey is opaque - rely on underlying library for zeroization
2018        // The ml-dsa crate is compiled with zeroize feature enabled
2019    }
2020}
2021
2022impl Drop for VerifyingKey {
2023    fn drop(&mut self) {
2024        // ML-DSA VerifyingKey is opaque - rely on underlying library for zeroization
2025        // The ml-dsa crate is compiled with zeroize feature enabled
2026    }
2027}
2028
2029impl Drop for KeyPair {
2030    fn drop(&mut self) {
2031        // Drop implementations for individual keys will handle cleanup
2032    }
2033}
2034
2035impl Drop for EncapsulationKey {
2036    fn drop(&mut self) {
2037        // ML-KEM EncapsulationKey is opaque - rely on underlying library for zeroization
2038        // The ml-kem crate is compiled with zeroize feature enabled
2039    }
2040}
2041
2042impl Drop for DecapsulationKey {
2043    fn drop(&mut self) {
2044        // ML-KEM DecapsulationKey is opaque - rely on underlying library for zeroization
2045        // The ml-kem crate is compiled with zeroize feature enabled
2046    }
2047}
2048
2049impl Drop for KemKeyPair {
2050    fn drop(&mut self) {
2051        // Drop implementations for individual keys will handle cleanup
2052    }
2053}
2054
2055#[cfg(test)]
2056mod tests {
2057    use super::*;
2058    use rand::rng;
2059    use std::thread;
2060    use time::Duration;
2061
2062    #[test]
2063    fn test_keypair_generation() {
2064        thread::Builder::new()
2065            .name("keypair-generation-smoke".to_string())
2066            .stack_size(16 * 1024 * 1024)
2067            .spawn(|| {
2068                let mut rng = rng();
2069                let keypair = KeyPair::generate(&mut rng);
2070
2071                // Test bytes export/import
2072                let signing_bytes = keypair.signing_key_to_bytes();
2073                let verifying_bytes = keypair.verifying_key_to_bytes();
2074
2075                assert!(!signing_bytes.is_empty());
2076                assert!(!verifying_bytes.is_empty());
2077
2078                let imported_signing = KeyPair::signing_key_from_bytes(&signing_bytes).unwrap();
2079                let imported_verifying =
2080                    KeyPair::verifying_key_from_bytes(&verifying_bytes).unwrap();
2081
2082                // Keys should be functionally equivalent (test by signing/verifying)
2083                let mut claims = Claims::new();
2084                claims.set_subject("test").unwrap();
2085
2086                let token1 = PasetoPQ::sign(keypair.signing_key(), &claims).unwrap();
2087                let token2 = PasetoPQ::sign(&imported_signing, &claims).unwrap();
2088
2089                // Both should verify with either key
2090                PasetoPQ::verify(keypair.verifying_key(), &token1).unwrap();
2091                PasetoPQ::verify(&imported_verifying, &token1).unwrap();
2092                PasetoPQ::verify(keypair.verifying_key(), &token2).unwrap();
2093                PasetoPQ::verify(&imported_verifying, &token2).unwrap();
2094            })
2095            .unwrap()
2096            .join()
2097            .unwrap();
2098    }
2099
2100    #[test]
2101    fn test_basic_sign_and_verify() {
2102        let mut rng = rng();
2103        let keypair = KeyPair::generate(&mut rng);
2104
2105        let mut claims = Claims::new();
2106        claims.set_subject("user123").unwrap();
2107        claims.set_issuer("conflux-auth").unwrap();
2108        claims.set_audience("conflux-network").unwrap();
2109        claims.set_jti("token-id-123").unwrap();
2110        claims.add_custom("tenant_id", "org_abc123").unwrap();
2111        claims.add_custom("roles", ["user", "admin"]).unwrap();
2112
2113        let token = PasetoPQ::sign(keypair.signing_key(), &claims).unwrap();
2114        assert!(token.starts_with("paseto.pq1.public."));
2115
2116        let verified = PasetoPQ::verify(keypair.verifying_key(), &token).unwrap();
2117        let verified_claims = verified.claims();
2118
2119        assert_eq!(verified_claims.subject(), Some("user123"));
2120        assert_eq!(verified_claims.issuer(), Some("conflux-auth"));
2121        assert_eq!(verified_claims.audience(), Some("conflux-network"));
2122        assert_eq!(verified_claims.jti(), Some("token-id-123"));
2123
2124        // Check custom claims
2125        assert_eq!(
2126            verified_claims.get_custom("tenant_id").unwrap().as_str(),
2127            Some("org_abc123")
2128        );
2129        let roles: Vec<String> =
2130            serde_json::from_value(verified_claims.get_custom("roles").unwrap().clone()).unwrap();
2131        assert_eq!(roles, vec!["user", "admin"]);
2132    }
2133
2134    #[test]
2135    fn test_time_validation() {
2136        let mut rng = rng();
2137        let keypair = KeyPair::generate(&mut rng);
2138
2139        let now = OffsetDateTime::now_utc();
2140
2141        // Test expired token
2142        let mut expired_claims = Claims::new();
2143        expired_claims.set_subject("user").unwrap();
2144        expired_claims
2145            .set_expiration(now - Duration::hours(1))
2146            .unwrap();
2147
2148        let expired_token = PasetoPQ::sign(keypair.signing_key(), &expired_claims).unwrap();
2149        let result = PasetoPQ::verify(keypair.verifying_key(), &expired_token);
2150        assert!(matches!(result.unwrap_err(), PqPasetoError::TokenExpired));
2151
2152        // Test not-yet-valid token
2153        let mut future_claims = Claims::new();
2154        future_claims.set_subject("user").unwrap();
2155        future_claims
2156            .set_not_before(now + Duration::hours(1))
2157            .unwrap();
2158
2159        let future_token = PasetoPQ::sign(keypair.signing_key(), &future_claims).unwrap();
2160        let result = PasetoPQ::verify(keypair.verifying_key(), &future_token);
2161        assert!(matches!(
2162            result.unwrap_err(),
2163            PqPasetoError::TokenNotYetValid
2164        ));
2165
2166        // Test valid token
2167        let mut valid_claims = Claims::new();
2168        valid_claims.set_subject("user").unwrap();
2169        valid_claims
2170            .set_not_before(now - Duration::minutes(5))
2171            .unwrap();
2172        valid_claims
2173            .set_expiration(now + Duration::hours(1))
2174            .unwrap();
2175
2176        let valid_token = PasetoPQ::sign(keypair.signing_key(), &valid_claims).unwrap();
2177        let verified = PasetoPQ::verify(keypair.verifying_key(), &valid_token).unwrap();
2178        assert_eq!(verified.claims().subject(), Some("user"));
2179    }
2180
2181    #[test]
2182    fn test_audience_and_issuer_validation() {
2183        let mut rng = rng();
2184        let keypair = KeyPair::generate(&mut rng);
2185
2186        let mut claims = Claims::new();
2187        claims.set_subject("user").unwrap();
2188        claims.set_audience("api.example.com").unwrap();
2189        claims.set_issuer("my-service").unwrap();
2190
2191        let token = PasetoPQ::sign(keypair.signing_key(), &claims).unwrap();
2192
2193        // Valid audience and issuer
2194        let verified = PasetoPQ::verify_with_options(
2195            keypair.verifying_key(),
2196            &token,
2197            Some("api.example.com"),
2198            Some("my-service"),
2199            Duration::seconds(30),
2200        )
2201        .unwrap();
2202        assert_eq!(verified.claims().subject(), Some("user"));
2203
2204        // Invalid audience
2205        let result = PasetoPQ::verify_with_options(
2206            keypair.verifying_key(),
2207            &token,
2208            Some("wrong-audience"),
2209            Some("conflux-auth"),
2210            Duration::seconds(30),
2211        );
2212        assert!(matches!(
2213            result.unwrap_err(),
2214            PqPasetoError::InvalidAudience { .. }
2215        ));
2216
2217        // Invalid issuer
2218        let result = PasetoPQ::verify_with_options(
2219            keypair.verifying_key(),
2220            &token,
2221            Some("api.example.com"),
2222            Some("wrong-service"),
2223            Duration::seconds(30),
2224        );
2225        assert!(matches!(
2226            result.unwrap_err(),
2227            PqPasetoError::InvalidIssuer { .. }
2228        ));
2229    }
2230
2231    #[test]
2232    fn test_signature_verification_failure() {
2233        let mut rng = rng();
2234        let keypair1 = KeyPair::generate(&mut rng);
2235        let keypair2 = KeyPair::generate(&mut rng);
2236
2237        let mut claims = Claims::new();
2238        claims.set_subject("user").unwrap();
2239
2240        let token = PasetoPQ::sign(&keypair1.signing_key, &claims).unwrap();
2241
2242        // Try to verify with wrong key
2243        let result = PasetoPQ::verify(&keypair2.verifying_key, &token);
2244        assert!(matches!(
2245            result.unwrap_err(),
2246            PqPasetoError::SignatureVerificationFailed
2247        ));
2248    }
2249
2250    #[test]
2251    fn test_malformed_tokens() {
2252        let mut rng = rng();
2253        let keypair = KeyPair::generate(&mut rng);
2254
2255        // Too few parts
2256        let result = PasetoPQ::verify(keypair.verifying_key(), "paseto.pq1");
2257        assert!(matches!(
2258            result.unwrap_err(),
2259            PqPasetoError::InvalidFormat(_)
2260        ));
2261
2262        // Wrong prefix
2263        let result = PasetoPQ::verify(keypair.verifying_key(), "wrong.pq1.pq.payload.sig");
2264        assert!(matches!(
2265            result.unwrap_err(),
2266            PqPasetoError::InvalidFormat(_)
2267        ));
2268
2269        // Invalid base64 in payload
2270        let result = PasetoPQ::verify(keypair.verifying_key(), "paseto.pq1.public.invalid!!!.sig");
2271        assert!(matches!(
2272            result.unwrap_err(),
2273            PqPasetoError::InvalidFormat(_)
2274        ));
2275
2276        // Invalid signature bytes
2277        let result = PasetoPQ::verify(
2278            keypair.verifying_key(),
2279            "paseto.pq1.public.dGVzdA.invalid_sig",
2280        );
2281        assert!(matches!(result.unwrap_err(), PqPasetoError::CryptoError(_)));
2282    }
2283
2284    #[test]
2285    fn test_symmetric_key_generation() {
2286        let mut rng = rng();
2287        let key = SymmetricKey::generate(&mut rng);
2288
2289        // Test bytes export/import
2290        let key_bytes = key.to_bytes();
2291        assert_eq!(key_bytes.len(), SYMMETRIC_KEY_SIZE);
2292
2293        let imported_key = SymmetricKey::from_bytes(&key_bytes).unwrap();
2294        assert_eq!(key.to_bytes(), imported_key.to_bytes());
2295    }
2296
2297    #[test]
2298    fn test_kem_keypair_generation() {
2299        let mut rng = rng();
2300        let keypair = KemKeyPair::generate(&mut rng);
2301
2302        // Test bytes export/import
2303        let enc_bytes = keypair.encapsulation_key_to_bytes();
2304        let dec_bytes = keypair.decapsulation_key_to_bytes();
2305
2306        assert!(!enc_bytes.is_empty());
2307        assert!(!dec_bytes.is_empty());
2308
2309        let _imported_enc = KemKeyPair::encapsulation_key_from_bytes(&enc_bytes).unwrap();
2310        let _imported_dec = KemKeyPair::decapsulation_key_from_bytes(&dec_bytes).unwrap();
2311
2312        // Test key encapsulation/decapsulation with real ML-KEM implementation
2313        let (sender_key, ciphertext) = keypair.encapsulate();
2314        let receiver_key = keypair.decapsulate(&ciphertext).unwrap();
2315
2316        // Real ML-KEM implementation should produce identical shared secrets
2317        assert_eq!(sender_key.to_bytes(), receiver_key.to_bytes());
2318        assert_ne!(sender_key.to_bytes(), [0u8; 32]); // Should not be all zeros
2319        assert_eq!(ciphertext.len(), 1088); // ML-KEM-768 ciphertext size
2320    }
2321
2322    #[test]
2323    fn test_basic_encrypt_and_decrypt() {
2324        let mut rng = rng();
2325        let key = SymmetricKey::generate(&mut rng);
2326
2327        let mut claims = Claims::new();
2328        claims.set_subject("user123").unwrap();
2329        claims.set_issuer("conflux-auth").unwrap();
2330        claims.set_audience("conflux-network").unwrap();
2331        claims.set_jti("token-id-123").unwrap();
2332        claims.add_custom("tenant_id", "org_abc123").unwrap();
2333        claims.add_custom("roles", ["user", "admin"]).unwrap();
2334
2335        let token = PasetoPQ::encrypt(&key, &claims).unwrap();
2336        assert!(token.starts_with("paseto.pq1.local."));
2337
2338        let verified = PasetoPQ::decrypt(&key, &token).unwrap();
2339        let verified_claims = verified.claims();
2340
2341        assert_eq!(verified_claims.subject(), Some("user123"));
2342        assert_eq!(verified_claims.issuer(), Some("conflux-auth"));
2343        assert_eq!(verified_claims.audience(), Some("conflux-network"));
2344        assert_eq!(verified_claims.jti(), Some("token-id-123"));
2345
2346        // Check custom claims
2347        assert_eq!(
2348            verified_claims.get_custom("tenant_id").unwrap().as_str(),
2349            Some("org_abc123")
2350        );
2351        let roles: Vec<String> =
2352            serde_json::from_value(verified_claims.get_custom("roles").unwrap().clone()).unwrap();
2353        assert_eq!(roles, vec!["user", "admin"]);
2354    }
2355
2356    #[test]
2357    fn test_local_token_time_validation() {
2358        let mut rng = rng();
2359        let key = SymmetricKey::generate(&mut rng);
2360        let now = OffsetDateTime::now_utc();
2361
2362        // Test expired token
2363        let mut expired_claims = Claims::new();
2364        expired_claims.set_subject("user").unwrap();
2365        expired_claims
2366            .set_expiration(now - Duration::hours(1))
2367            .unwrap();
2368
2369        let expired_token = PasetoPQ::encrypt(&key, &expired_claims).unwrap();
2370        let result = PasetoPQ::decrypt(&key, &expired_token);
2371        assert!(matches!(result.unwrap_err(), PqPasetoError::TokenExpired));
2372
2373        // Test not-yet-valid token
2374        let mut future_claims = Claims::new();
2375        future_claims.set_subject("user").unwrap();
2376        future_claims
2377            .set_not_before(now + Duration::hours(1))
2378            .unwrap();
2379
2380        let future_token = PasetoPQ::encrypt(&key, &future_claims).unwrap();
2381        let result = PasetoPQ::decrypt(&key, &future_token);
2382        assert!(matches!(
2383            result.unwrap_err(),
2384            PqPasetoError::TokenNotYetValid
2385        ));
2386
2387        // Test valid token
2388        let mut valid_claims = Claims::new();
2389        valid_claims.set_subject("user").unwrap();
2390        valid_claims
2391            .set_not_before(now - Duration::minutes(5))
2392            .unwrap();
2393        valid_claims
2394            .set_expiration(now + Duration::hours(1))
2395            .unwrap();
2396
2397        let valid_token = PasetoPQ::encrypt(&key, &valid_claims).unwrap();
2398        let verified = PasetoPQ::decrypt(&key, &valid_token).unwrap();
2399        assert_eq!(verified.claims().subject(), Some("user"));
2400    }
2401
2402    #[test]
2403    fn test_local_token_audience_and_issuer_validation() {
2404        let mut rng = rng();
2405        let key = SymmetricKey::generate(&mut rng);
2406
2407        let mut claims = Claims::new();
2408        claims.set_subject("user123").unwrap();
2409        claims.set_issuer("test-issuer").unwrap();
2410        claims.set_audience("test-audience").unwrap();
2411
2412        let token = PasetoPQ::encrypt(&key, &claims).unwrap();
2413
2414        // Valid audience and issuer
2415        let verified = PasetoPQ::decrypt_with_options(
2416            &key,
2417            &token,
2418            Some("test-audience"),
2419            Some("test-issuer"),
2420            Duration::seconds(30),
2421        )
2422        .unwrap();
2423        assert_eq!(verified.claims().subject(), Some("user123"));
2424
2425        // Wrong audience
2426        let result = PasetoPQ::decrypt_with_options(
2427            &key,
2428            &token,
2429            Some("wrong-audience"),
2430            Some("test-issuer"),
2431            Duration::seconds(30),
2432        );
2433        assert!(matches!(
2434            result.unwrap_err(),
2435            PqPasetoError::InvalidAudience { .. }
2436        ));
2437
2438        // Wrong issuer
2439        let result = PasetoPQ::decrypt_with_options(
2440            &key,
2441            &token,
2442            Some("test-audience"),
2443            Some("wrong-issuer"),
2444            Duration::seconds(30),
2445        );
2446        assert!(matches!(
2447            result.unwrap_err(),
2448            PqPasetoError::InvalidIssuer { .. }
2449        ));
2450    }
2451
2452    #[test]
2453    fn test_local_token_tamper_detection() {
2454        let mut rng = rng();
2455        let key = SymmetricKey::generate(&mut rng);
2456
2457        let mut claims = Claims::new();
2458        claims.set_subject("user123").unwrap();
2459
2460        let token = PasetoPQ::encrypt(&key, &claims).unwrap();
2461
2462        // Tamper with the token
2463        let mut tampered_token = token.clone();
2464        tampered_token.push('x'); // Append extra character
2465
2466        let result = PasetoPQ::decrypt(&key, &tampered_token);
2467        assert!(result.is_err());
2468
2469        // Try with wrong key
2470        let wrong_key = SymmetricKey::generate(&mut rng);
2471        let result = PasetoPQ::decrypt(&wrong_key, &token);
2472        assert!(matches!(
2473            result.unwrap_err(),
2474            PqPasetoError::DecryptionError(_)
2475        ));
2476    }
2477
2478    #[test]
2479    fn test_malformed_local_tokens() {
2480        let mut rng = rng();
2481        let key = SymmetricKey::generate(&mut rng);
2482
2483        // Wrong prefix
2484        let result = PasetoPQ::decrypt(&key, "wrong.pq1.local.payload");
2485        assert!(matches!(
2486            result.unwrap_err(),
2487            PqPasetoError::InvalidFormat(_)
2488        ));
2489
2490        // Invalid base64 in payload
2491        let result = PasetoPQ::decrypt(&key, "paseto.pq1.local.invalid!!!");
2492        assert!(matches!(
2493            result.unwrap_err(),
2494            PqPasetoError::InvalidFormat(_)
2495        ));
2496
2497        // Too short payload (no nonce)
2498        let result = PasetoPQ::decrypt(&key, "paseto.pq1.local.dGVzdA");
2499        assert!(matches!(
2500            result.unwrap_err(),
2501            PqPasetoError::DecryptionError(_)
2502        ));
2503    }
2504
2505    #[test]
2506    fn test_mixed_token_types() {
2507        let mut rng = rng();
2508        let asymmetric_keypair = KeyPair::generate(&mut rng);
2509        let symmetric_key = SymmetricKey::generate(&mut rng);
2510
2511        let mut claims = Claims::new();
2512        claims.set_subject("user123").unwrap();
2513
2514        // Create both types of tokens
2515        let public_token = PasetoPQ::sign(asymmetric_keypair.signing_key(), &claims).unwrap();
2516        let local_token = PasetoPQ::encrypt(&symmetric_key, &claims).unwrap();
2517
2518        assert!(public_token.starts_with("paseto.pq1.public."));
2519        assert!(local_token.starts_with("paseto.pq1.local."));
2520
2521        // Verify each with correct method
2522        let verified_public =
2523            PasetoPQ::verify(asymmetric_keypair.verifying_key(), &public_token).unwrap();
2524        let verified_local = PasetoPQ::decrypt(&symmetric_key, &local_token).unwrap();
2525
2526        assert_eq!(verified_public.claims().subject(), Some("user123"));
2527        assert_eq!(verified_local.claims().subject(), Some("user123"));
2528
2529        // Cross-verification should fail
2530        let result = PasetoPQ::decrypt(&symmetric_key, &public_token);
2531        assert!(result.is_err());
2532
2533        let result = PasetoPQ::verify(asymmetric_keypair.verifying_key(), &local_token);
2534        assert!(result.is_err());
2535    }
2536
2537    #[test]
2538    fn test_footer_basic_functionality() {
2539        let mut footer = Footer::new();
2540        footer.set_kid("test-key-123").unwrap();
2541        footer.set_version("1.0.0").unwrap();
2542        footer.add_custom("env", "production").unwrap();
2543
2544        assert_eq!(footer.kid(), Some("test-key-123"));
2545        assert_eq!(footer.version(), Some("1.0.0"));
2546        assert_eq!(
2547            footer.get_custom("env").unwrap().as_str(),
2548            Some("production")
2549        );
2550    }
2551
2552    #[test]
2553    fn test_public_token_with_footer() {
2554        let mut rng = rng();
2555        let keypair = KeyPair::generate(&mut rng);
2556
2557        let mut claims = Claims::new();
2558        claims.set_subject("user123").unwrap();
2559
2560        let mut footer = Footer::new();
2561        footer.set_kid("signing-key-2024").unwrap();
2562        footer.add_custom("deployment", "us-east-1").unwrap();
2563
2564        // Token with footer
2565        let token =
2566            PasetoPQ::sign_with_footer(keypair.signing_key(), &claims, Some(&footer)).unwrap();
2567        assert!(token.starts_with("paseto.pq1.public."));
2568        assert_eq!(token.split('.').count(), 6); // paseto.pq1.public.payload.signature.footer
2569
2570        let verified = PasetoPQ::verify_with_footer(keypair.verifying_key(), &token).unwrap();
2571        assert_eq!(verified.claims().subject(), Some("user123"));
2572
2573        let verified_footer = verified.footer().unwrap();
2574        assert_eq!(verified_footer.kid(), Some("signing-key-2024"));
2575        assert_eq!(
2576            verified_footer.get_custom("deployment").unwrap().as_str(),
2577            Some("us-east-1")
2578        );
2579
2580        // Token without footer should still work
2581        let token_no_footer = PasetoPQ::sign(keypair.signing_key(), &claims).unwrap();
2582        assert_eq!(token_no_footer.split('.').count(), 5);
2583
2584        let verified_no_footer =
2585            PasetoPQ::verify(keypair.verifying_key(), &token_no_footer).unwrap();
2586        assert_eq!(verified_no_footer.claims().subject(), Some("user123"));
2587        assert!(verified_no_footer.footer().is_none());
2588    }
2589
2590    #[test]
2591    fn test_local_token_with_footer() {
2592        let mut rng = rng();
2593        let key = SymmetricKey::generate(&mut rng);
2594
2595        let mut claims = Claims::new();
2596        claims.set_subject("user123").unwrap();
2597        claims.add_custom("session_data", "confidential").unwrap();
2598
2599        let mut footer = Footer::new();
2600        footer.set_kid("encryption-key-2024").unwrap();
2601        footer.add_custom("session_type", "secure").unwrap();
2602
2603        // Token with footer
2604        let token = PasetoPQ::encrypt_with_footer(&key, &claims, Some(&footer)).unwrap();
2605        assert!(token.starts_with("paseto.pq1.local."));
2606        assert_eq!(token.split('.').count(), 5); // paseto.pq1.local.payload.footer
2607
2608        let verified = PasetoPQ::decrypt_with_footer(&key, &token).unwrap();
2609        assert_eq!(verified.claims().subject(), Some("user123"));
2610        assert_eq!(
2611            verified
2612                .claims()
2613                .get_custom("session_data")
2614                .unwrap()
2615                .as_str(),
2616            Some("confidential")
2617        );
2618
2619        let verified_footer = verified.footer().unwrap();
2620        assert_eq!(verified_footer.kid(), Some("encryption-key-2024"));
2621        assert_eq!(
2622            verified_footer.get_custom("session_type").unwrap().as_str(),
2623            Some("secure")
2624        );
2625
2626        // Token without footer should still work
2627        let token_no_footer = PasetoPQ::encrypt(&key, &claims).unwrap();
2628        assert_eq!(token_no_footer.split('.').count(), 4);
2629
2630        let verified_no_footer = PasetoPQ::decrypt(&key, &token_no_footer).unwrap();
2631        assert_eq!(verified_no_footer.claims().subject(), Some("user123"));
2632        assert!(verified_no_footer.footer().is_none());
2633    }
2634
2635    #[test]
2636    fn test_footer_serialization() {
2637        let mut footer = Footer::new();
2638        footer.set_kid("test-key").unwrap();
2639        footer.set_version("1.0.0").unwrap();
2640        footer.add_custom("custom_field", "custom_value").unwrap();
2641
2642        let encoded = footer.to_base64().unwrap();
2643        let decoded = Footer::from_base64(&encoded).unwrap();
2644
2645        assert_eq!(footer.kid(), decoded.kid());
2646        assert_eq!(footer.version(), decoded.version());
2647        assert_eq!(
2648            footer.get_custom("custom_field"),
2649            decoded.get_custom("custom_field")
2650        );
2651    }
2652
2653    #[test]
2654    fn test_footer_tamper_detection() {
2655        let mut rng = rng();
2656        let keypair = KeyPair::generate(&mut rng);
2657
2658        let mut claims = Claims::new();
2659        claims.set_subject("user123").unwrap();
2660
2661        let mut footer = Footer::new();
2662        footer.set_kid("test-key").unwrap();
2663
2664        let token =
2665            PasetoPQ::sign_with_footer(keypair.signing_key(), &claims, Some(&footer)).unwrap();
2666
2667        // Tamper with footer
2668        let mut tampered_token = token.clone();
2669        tampered_token.push('x');
2670
2671        let result = PasetoPQ::verify_with_footer(keypair.verifying_key(), &tampered_token);
2672        assert!(result.is_err()); // Tampered footer should fail verification
2673    }
2674
2675    #[test]
2676    fn test_backward_compatibility() {
2677        let mut rng = rng();
2678        let keypair = KeyPair::generate(&mut rng);
2679        let symmetric_key = SymmetricKey::generate(&mut rng);
2680
2681        let mut claims = Claims::new();
2682        claims.set_subject("user123").unwrap();
2683
2684        // Old format tokens (without footer) should work with new methods
2685        let public_token = PasetoPQ::sign(keypair.signing_key(), &claims).unwrap();
2686        let local_token = PasetoPQ::encrypt(&symmetric_key, &claims).unwrap();
2687
2688        let verified_public =
2689            PasetoPQ::verify_with_footer(keypair.verifying_key(), &public_token).unwrap();
2690        let verified_local = PasetoPQ::decrypt_with_footer(&symmetric_key, &local_token).unwrap();
2691
2692        assert_eq!(verified_public.claims().subject(), Some("user123"));
2693        assert_eq!(verified_local.claims().subject(), Some("user123"));
2694        assert!(verified_public.footer().is_none());
2695        assert!(verified_local.footer().is_none());
2696    }
2697
2698    #[test]
2699    fn test_claims_json_conversion() {
2700        use serde_json::Value;
2701
2702        let mut claims = Claims::new();
2703        claims.set_subject("user123").unwrap();
2704        claims.set_issuer("test-service").unwrap();
2705        claims.set_audience("api.example.com").unwrap();
2706        claims.add_custom("role", "admin").unwrap();
2707        claims.add_custom("tenant_id", "org_abc123").unwrap();
2708        claims
2709            .add_custom("permissions", ["read", "write", "delete"])
2710            .unwrap();
2711
2712        // Test From<Claims> for serde_json::Value
2713        let json_value: Value = claims.clone().into();
2714        assert!(json_value.is_object());
2715        assert_eq!(json_value["sub"], "user123");
2716        assert_eq!(json_value["iss"], "test-service");
2717        assert_eq!(json_value["aud"], "api.example.com");
2718        assert_eq!(json_value["role"], "admin");
2719        assert_eq!(json_value["tenant_id"], "org_abc123");
2720        assert_eq!(json_value["permissions"][0], "read");
2721
2722        // Test From<&Claims> for serde_json::Value
2723        let json_value_ref: Value = (&claims).into();
2724        assert_eq!(json_value, json_value_ref);
2725
2726        // Test to_json_value method
2727        let json_value_method = claims.to_json_value();
2728        assert_eq!(json_value, json_value_method);
2729
2730        // Test to_json_string method
2731        let json_string = claims.to_json_string().unwrap();
2732        assert!(json_string.contains("\"sub\":\"user123\""));
2733        assert!(json_string.contains("\"role\":\"admin\""));
2734
2735        // Test to_json_string_pretty method
2736        let pretty_json = claims.to_json_string_pretty().unwrap();
2737        assert!(pretty_json.contains("\"sub\": \"user123\""));
2738        assert!(pretty_json.contains("\"role\": \"admin\""));
2739        assert!(pretty_json.len() > json_string.len()); // Pretty format should be longer
2740
2741        // Test that optional fields are skipped when None
2742        let minimal_claims = Claims::new();
2743        let minimal_json: Value = minimal_claims.into();
2744        assert!(minimal_json.is_object());
2745        assert!(minimal_json.as_object().unwrap().is_empty());
2746    }
2747
2748    #[test]
2749    fn test_claims_json_with_time_fields() {
2750        use serde_json::Value;
2751        use time::OffsetDateTime;
2752
2753        let mut claims = Claims::new();
2754        let now = OffsetDateTime::now_utc();
2755        let exp_time = now + time::Duration::hours(1);
2756        let nbf_time = now - time::Duration::minutes(5);
2757
2758        claims.set_subject("user456").unwrap();
2759        claims.set_expiration(exp_time).unwrap();
2760        claims.set_not_before(nbf_time).unwrap();
2761        claims.set_issued_at(now).unwrap();
2762
2763        let json_value: Value = claims.into();
2764
2765        // Verify time fields are present and properly formatted as RFC3339 strings
2766        assert!(json_value["exp"].is_string());
2767        assert!(json_value["nbf"].is_string());
2768        assert!(json_value["iat"].is_string());
2769
2770        // Verify the time strings can be parsed back
2771        let exp_str = json_value["exp"].as_str().unwrap();
2772        let parsed_exp =
2773            OffsetDateTime::parse(exp_str, &time::format_description::well_known::Rfc3339).unwrap();
2774        assert_eq!(parsed_exp.unix_timestamp(), exp_time.unix_timestamp());
2775    }
2776
2777    #[test]
2778    fn test_claims_json_integration_example() {
2779        use serde_json::Value;
2780
2781        // Simulate a real-world use case
2782        let mut claims = Claims::new();
2783        claims.set_subject("user789").unwrap();
2784        claims.set_issuer("auth-service").unwrap();
2785        claims.set_audience("api.conflux.dev").unwrap();
2786        claims
2787            .add_custom("session_id", "sess_abc123def456")
2788            .unwrap();
2789        claims.add_custom("user_type", "premium").unwrap();
2790        claims
2791            .add_custom("scopes", ["profile", "email", "admin"])
2792            .unwrap();
2793
2794        // Test integration with logging (simulated)
2795        let json_string = claims.to_json_string().unwrap();
2796        assert!(!json_string.is_empty());
2797
2798        // Test integration with database storage (simulated)
2799        let json_value: Value = claims.clone().into();
2800        let serialized_for_db = serde_json::to_vec(&json_value).unwrap();
2801        assert!(!serialized_for_db.is_empty());
2802
2803        // Test round-trip conversion
2804        let deserialized_value: Value = serde_json::from_slice(&serialized_for_db).unwrap();
2805        assert_eq!(json_value, deserialized_value);
2806
2807        // Verify specific fields for logging/tracing integration
2808        assert_eq!(deserialized_value["sub"], "user789");
2809        assert_eq!(deserialized_value["session_id"], "sess_abc123def456");
2810        assert_eq!(deserialized_value["scopes"].as_array().unwrap().len(), 3);
2811    }
2812
2813    #[test]
2814    fn test_token_parsing_public_tokens() {
2815        let mut rng = rng();
2816        let keypair = KeyPair::generate(&mut rng);
2817
2818        // Create a public token without footer
2819        let mut claims = Claims::new();
2820        claims.set_subject("test-user").unwrap();
2821        claims.add_custom("role", "admin").unwrap();
2822
2823        let token = PasetoPQ::sign(keypair.signing_key(), &claims).unwrap();
2824        let parsed = ParsedToken::parse(&token).unwrap();
2825
2826        // Verify basic properties
2827        assert_eq!(parsed.purpose(), "public");
2828        assert_eq!(parsed.version(), "pq1");
2829        assert!(!parsed.has_footer());
2830        assert!(parsed.is_public());
2831        assert!(!parsed.is_local());
2832        assert!(parsed.signature_bytes().is_some());
2833        assert_eq!(parsed.raw_token(), &token);
2834
2835        // Test alternative API
2836        let parsed_alt = PasetoPQ::parse_token(&token).unwrap();
2837        assert_eq!(parsed.purpose(), parsed_alt.purpose());
2838    }
2839
2840    #[test]
2841    fn test_token_parsing_public_tokens_with_footer() {
2842        let mut rng = rng();
2843        let keypair = KeyPair::generate(&mut rng);
2844
2845        // Create a public token with footer
2846        let mut claims = Claims::new();
2847        claims.set_subject("test-user").unwrap();
2848
2849        let mut footer = Footer::new();
2850        footer.set_kid("test-key-123").unwrap();
2851        footer.add_custom("tenant", "org_abc").unwrap();
2852
2853        let token =
2854            PasetoPQ::sign_with_footer(keypair.signing_key(), &claims, Some(&footer)).unwrap();
2855        let parsed = ParsedToken::parse(&token).unwrap();
2856
2857        // Verify footer parsing
2858        assert!(parsed.has_footer());
2859        let parsed_footer = parsed.footer().unwrap();
2860        assert_eq!(parsed_footer.kid(), Some("test-key-123"));
2861        assert_eq!(
2862            parsed_footer.get_custom("tenant"),
2863            Some(&serde_json::json!("org_abc"))
2864        );
2865
2866        // Test JSON footer methods
2867        let footer_json = parsed.footer_json().unwrap().unwrap();
2868        assert!(footer_json.contains("test-key-123"));
2869        assert!(footer_json.contains("org_abc"));
2870
2871        let footer_pretty = parsed.footer_json_pretty().unwrap().unwrap();
2872        assert!(footer_pretty.len() > footer_json.len()); // Pretty should be longer
2873    }
2874
2875    #[test]
2876    fn test_token_parsing_local_tokens() {
2877        let mut rng = rng();
2878        let key = SymmetricKey::generate(&mut rng);
2879
2880        // Create a local token without footer
2881        let mut claims = Claims::new();
2882        claims.set_subject("local-user").unwrap();
2883        claims.add_custom("session_type", "confidential").unwrap();
2884
2885        let token = PasetoPQ::encrypt(&key, &claims).unwrap();
2886        let parsed = ParsedToken::parse(&token).unwrap();
2887
2888        // Verify basic properties
2889        assert_eq!(parsed.purpose(), "local");
2890        assert_eq!(parsed.version(), "pq1");
2891        assert!(!parsed.has_footer());
2892        assert!(!parsed.is_public());
2893        assert!(parsed.is_local());
2894        assert!(parsed.signature_bytes().is_none()); // Local tokens don't have separate signatures
2895        assert!(parsed.payload_length() > 0);
2896    }
2897
2898    #[test]
2899    fn test_token_parsing_local_tokens_with_footer() {
2900        let mut rng = rng();
2901        let key = SymmetricKey::generate(&mut rng);
2902
2903        // Create a local token with footer
2904        let mut claims = Claims::new();
2905        claims.set_subject("local-user").unwrap();
2906
2907        let mut footer = Footer::new();
2908        footer.set_kid("encryption-key-456").unwrap();
2909        footer.set_version("v2.1").unwrap();
2910
2911        let token = PasetoPQ::encrypt_with_footer(&key, &claims, Some(&footer)).unwrap();
2912        let parsed = ParsedToken::parse(&token).unwrap();
2913
2914        // Verify footer parsing
2915        assert!(parsed.has_footer());
2916        let parsed_footer = parsed.footer().unwrap();
2917        assert_eq!(parsed_footer.kid(), Some("encryption-key-456"));
2918        assert_eq!(parsed_footer.version(), Some("v2.1"));
2919
2920        // Test format summary
2921        let summary = parsed.format_summary();
2922        assert!(summary.contains("paseto.pq1.local"));
2923        assert!(summary.contains("footer: present"));
2924    }
2925
2926    #[test]
2927    fn test_token_parsing_error_cases() {
2928        // Test various malformed tokens
2929        let error_cases = vec![
2930            ("", "expected at least 4 parts"),
2931            ("not.a.token", "expected at least 4 parts"),
2932            ("wrong.pq1.public.payload", "Invalid protocol"),
2933            ("paseto.v2.public.payload", "Unsupported token format"),
2934            ("paseto.pq1.unknown.payload", "Unsupported token format"),
2935            ("paseto.pq1.public.invalid_base64", "Invalid payload base64"),
2936            (
2937                "paseto.pq1.public.dGVzdA.invalid!!!base64",
2938                "Invalid signature base64",
2939            ),
2940            (
2941                "paseto.pq1.public.dGVzdA.dGVzdA.dGVzdA.extra.parts",
2942                "too many parts",
2943            ),
2944            ("paseto.pq1.local.dGVzdA.dGVzdA.extra", "too many parts"),
2945        ];
2946
2947        for (token, expected_error) in error_cases {
2948            let result = ParsedToken::parse(token);
2949            assert!(result.is_err(), "Expected error for token: {}", token);
2950            let error_msg = result.unwrap_err().to_string();
2951            assert!(
2952                error_msg.contains(expected_error),
2953                "Expected '{}' in error '{}' for token '{}'",
2954                expected_error,
2955                error_msg,
2956                token
2957            );
2958        }
2959    }
2960
2961    #[test]
2962    fn test_token_size_estimation_public_tokens() {
2963        // Test basic public token estimation
2964        let mut claims = Claims::new();
2965        claims.set_subject("user123").unwrap();
2966        claims.set_issuer("test-service").unwrap();
2967
2968        let estimator = TokenSizeEstimator::public(&claims, false);
2969
2970        // Public token size expectations vary by ML-DSA parameter set
2971        let (expected_min_size, expected_max_size) = if cfg!(feature = "ml-dsa-44") {
2972            (2800, 3200) // ML-DSA-44: ~2.8KB signature + payload + overhead
2973        } else if cfg!(feature = "ml-dsa-65") {
2974            (4200, 4800) // ML-DSA-65: ~4.3KB signature + payload + overhead
2975        } else {
2976            (5000, 5500) // ML-DSA-87: ~5KB signature + payload + overhead
2977        };
2978
2979        assert!(estimator.total_bytes() >= expected_min_size);
2980        assert!(estimator.total_bytes() < expected_max_size);
2981
2982        // Check size limit methods - expectations vary by parameter set
2983        if cfg!(feature = "ml-dsa-44") {
2984            // ML-DSA-44 tokens (~2.8KB) fit in cookies but not URLs
2985            assert!(estimator.fits_in_cookie()); // 2885 < 4096
2986            assert!(!estimator.fits_in_url()); // 2885 > 2048
2987        } else {
2988            // ML-DSA-65 and ML-DSA-87 tokens are too large for both
2989            assert!(!estimator.fits_in_cookie()); // > 4096
2990            assert!(!estimator.fits_in_url()); // > 2048
2991        }
2992        assert!(estimator.fits_in_header()); // All should fit in headers
2993
2994        // Test breakdown components
2995        let breakdown = estimator.breakdown();
2996        assert!(breakdown.prefix > 0);
2997        assert!(breakdown.payload > 0);
2998
2999        // Signature size expectations based on parameter set
3000        let expected_sig_size = if cfg!(feature = "ml-dsa-44") {
3001            2800
3002        } else if cfg!(feature = "ml-dsa-65") {
3003            4300
3004        } else {
3005            5000
3006        };
3007        assert_eq!(breakdown.signature_or_tag, expected_sig_size);
3008        assert_eq!(breakdown.footer, None);
3009        assert!(breakdown.separators > 0);
3010        assert!(breakdown.base64_overhead > 0);
3011    }
3012
3013    #[test]
3014    fn test_token_size_estimation_local_tokens() {
3015        // Test basic local token estimation
3016        let mut claims = Claims::new();
3017        claims.set_subject("user123").unwrap();
3018        claims.set_issuer("test-service").unwrap();
3019
3020        let estimator = TokenSizeEstimator::local(&claims, false);
3021
3022        // Local tokens should be much smaller than public tokens
3023        assert!(estimator.total_bytes() > 80);
3024        assert!(estimator.total_bytes() < 300); // Should be reasonably small
3025
3026        // Check size limit methods
3027        assert!(estimator.fits_in_cookie());
3028        assert!(estimator.fits_in_url());
3029        assert!(estimator.fits_in_header());
3030
3031        // Test breakdown components
3032        let breakdown = estimator.breakdown();
3033        assert!(breakdown.prefix > 0);
3034        assert!(breakdown.payload > 0);
3035        assert_eq!(breakdown.signature_or_tag, 0); // Local tokens don't have separate signature
3036        assert_eq!(breakdown.footer, None);
3037        assert!(breakdown.separators > 0);
3038        assert!(breakdown.base64_overhead > 0);
3039    }
3040
3041    #[test]
3042    fn test_token_size_estimation_with_footer() {
3043        let mut claims = Claims::new();
3044        claims.set_subject("user123").unwrap();
3045
3046        // Test public token with footer
3047        let estimator_public = TokenSizeEstimator::public(&claims, true);
3048        let estimator_public_no_footer = TokenSizeEstimator::public(&claims, false);
3049
3050        assert!(estimator_public.total_bytes() > estimator_public_no_footer.total_bytes());
3051        assert!(estimator_public.breakdown().footer.is_some());
3052        assert!(estimator_public_no_footer.breakdown().footer.is_none());
3053
3054        // Test local token with footer
3055        let estimator_local = TokenSizeEstimator::local(&claims, true);
3056        let estimator_local_no_footer = TokenSizeEstimator::local(&claims, false);
3057
3058        assert!(estimator_local.total_bytes() > estimator_local_no_footer.total_bytes());
3059        assert!(estimator_local.breakdown().footer.is_some());
3060        assert!(estimator_local_no_footer.breakdown().footer.is_none());
3061    }
3062
3063    #[test]
3064    fn test_token_size_estimation_convenience_methods() {
3065        let mut claims = Claims::new();
3066        claims.set_subject("user123").unwrap();
3067
3068        // Test PasetoPQ convenience methods
3069        let public_estimator = PasetoPQ::estimate_public_size(&claims, false);
3070        let local_estimator = PasetoPQ::estimate_local_size(&claims, true);
3071
3072        // Should work the same as direct construction
3073        let direct_public = TokenSizeEstimator::public(&claims, false);
3074        let direct_local = TokenSizeEstimator::local(&claims, true);
3075
3076        assert_eq!(public_estimator.total_bytes(), direct_public.total_bytes());
3077        assert_eq!(local_estimator.total_bytes(), direct_local.total_bytes());
3078    }
3079
3080    #[test]
3081    fn test_token_size_estimation_optimization_suggestions() {
3082        // Test with small token - should have few suggestions
3083        let small_claims = Claims::new();
3084        let small_estimator = TokenSizeEstimator::local(&small_claims, false);
3085        let small_suggestions = small_estimator.optimization_suggestions();
3086        // Small local tokens should have no suggestions
3087        assert!(small_suggestions.is_empty() || small_estimator.total_bytes() < 1000);
3088
3089        // Test with large token - should have many suggestions
3090        let mut large_claims = Claims::new();
3091        large_claims
3092            .add_custom("huge_data", "x".repeat(5000))
3093            .unwrap();
3094        let large_estimator = TokenSizeEstimator::public(&large_claims, false);
3095        let large_suggestions = large_estimator.optimization_suggestions();
3096
3097        assert!(!large_suggestions.is_empty());
3098        assert!(large_suggestions.iter().any(|s| s.contains("cookie")));
3099        assert!(
3100            large_suggestions
3101                .iter()
3102                .any(|s| s.contains("shorter claim"))
3103        );
3104    }
3105
3106    #[test]
3107    fn test_token_size_breakdown_total() {
3108        let breakdown = TokenSizeBreakdown {
3109            prefix: 10,
3110            payload: 200,
3111            signature_or_tag: 3000,
3112            footer: Some(50),
3113            separators: 3,
3114            base64_overhead: 100,
3115        };
3116
3117        let expected_total = 10 + 200 + 3000 + 50 + 3 + 100;
3118        assert_eq!(breakdown.total(), expected_total);
3119
3120        // Test without footer
3121        let breakdown_no_footer = TokenSizeBreakdown {
3122            prefix: 10,
3123            payload: 200,
3124            signature_or_tag: 3000,
3125            footer: None,
3126            separators: 2,
3127            base64_overhead: 100,
3128        };
3129
3130        let expected_total_no_footer = 10 + 200 + 3000 + 2 + 100;
3131        assert_eq!(breakdown_no_footer.total(), expected_total_no_footer);
3132    }
3133
3134    #[test]
3135    fn test_token_parsing_debugging_methods() {
3136        let mut rng = rng();
3137        let keypair = KeyPair::generate(&mut rng);
3138
3139        let mut claims = Claims::new();
3140        claims.set_subject("debug-user").unwrap();
3141        claims.add_custom("large_data", "x".repeat(500)).unwrap(); // Make it somewhat large
3142
3143        let mut footer = Footer::new();
3144        footer.set_kid("debug-key").unwrap();
3145
3146        let token =
3147            PasetoPQ::sign_with_footer(keypair.signing_key(), &claims, Some(&footer)).unwrap();
3148        let parsed = ParsedToken::parse(&token).unwrap();
3149
3150        // Test debugging methods
3151        assert!(parsed.payload_length() > 100); // Should have substantial payload
3152        assert!(parsed.total_length() > parsed.payload_length()); // Total includes overhead
3153        assert!(!parsed.payload_bytes().is_empty());
3154
3155        let summary = parsed.format_summary();
3156        assert!(summary.contains("paseto.pq1.public"));
3157        assert!(summary.contains("signature: present"));
3158        assert!(summary.contains("footer: present"));
3159        assert!(summary.contains(&format!("{} bytes", parsed.payload_length())));
3160    }
3161
3162    #[test]
3163    fn test_token_parsing_middleware_scenarios() {
3164        let mut rng = rng();
3165        let keypair = KeyPair::generate(&mut rng);
3166        let symmetric_key = SymmetricKey::generate(&mut rng);
3167
3168        // Create different token types
3169        let mut claims = Claims::new();
3170        claims.set_subject("middleware-test").unwrap();
3171
3172        let public_token = PasetoPQ::sign(keypair.signing_key(), &claims).unwrap();
3173        let local_token = PasetoPQ::encrypt(&symmetric_key, &claims).unwrap();
3174
3175        // Simulate middleware routing logic
3176        let tokens = vec![
3177            (public_token, "public", true), // (token, expected_purpose, is_public)
3178            (local_token, "local", false),
3179        ];
3180
3181        for (token, expected_purpose, should_be_public) in tokens {
3182            let parsed = ParsedToken::parse(&token).unwrap();
3183
3184            // Routing decisions
3185            assert_eq!(parsed.purpose(), expected_purpose);
3186            assert_eq!(parsed.is_public(), should_be_public);
3187            assert_eq!(parsed.is_local(), !should_be_public);
3188
3189            // Logging/metrics simulation
3190            let purpose = parsed.purpose();
3191            let version = parsed.version();
3192            let size = parsed.total_length();
3193
3194            assert!(!purpose.is_empty());
3195            assert_eq!(version, "pq1");
3196            assert!(size > 0);
3197
3198            // Simulate size-based alerts
3199            if size > 2048 {
3200                // Would trigger monitoring alert
3201                println!("Large token detected: {} bytes", size);
3202            }
3203        }
3204    }
3205
3206    #[test]
3207    fn test_symmetric_key_zeroization() {
3208        // Test SymmetricKey zeroization - this is the only key type we fully control
3209        {
3210            let mut key = SymmetricKey([0x42u8; 32]);
3211
3212            // Verify key contains expected data
3213            assert_eq!(key.0[0], 0x42);
3214
3215            // Zeroize the key
3216            key.zeroize();
3217
3218            // Verify key is zeroed
3219            assert_eq!(key.0, [0u8; 32]);
3220        }
3221
3222        // Test that SymmetricKey is automatically zeroized on drop (ZeroizeOnDrop)
3223        {
3224            let key = SymmetricKey([0x55u8; 32]);
3225            assert_eq!(key.0[0], 0x55);
3226            // Key will be automatically zeroized when it goes out of scope
3227        }
3228    }
3229
3230    #[test]
3231    fn test_key_operations_with_drop_cleanup() {
3232        // Test that cryptographic operations work correctly with Drop implementations
3233        let mut rng = rng();
3234
3235        // Test ML-DSA keypair operations
3236        {
3237            let keypair = KeyPair::generate(&mut rng);
3238            let test_data = b"test message";
3239            let signature = keypair.signing_key().0.sign(test_data);
3240            assert!(
3241                keypair
3242                    .verifying_key()
3243                    .0
3244                    .verify(test_data, &signature)
3245                    .is_ok()
3246            );
3247            // keypair will be dropped and cleaned up automatically
3248        }
3249
3250        // Test ML-KEM operations
3251        {
3252            let kem_keypair = KemKeyPair::generate(&mut rng);
3253            let (key1, ciphertext) = kem_keypair.encapsulate();
3254            let key2 = kem_keypair.decapsulate(&ciphertext).unwrap();
3255            assert_eq!(key1.to_bytes(), key2.to_bytes());
3256            // All keys will be dropped and cleaned up automatically
3257        }
3258
3259        // Test symmetric key operations
3260        {
3261            let key = SymmetricKey::generate(&mut rng);
3262            let key_bytes = key.to_bytes();
3263            assert_eq!(key_bytes.len(), 32);
3264            // key will be automatically zeroized on drop
3265        }
3266    }
3267
3268    #[test]
3269    fn test_token_versioning_configuration() {
3270        // Test that the prefix constants use pq1 versioning
3271        assert_eq!(TOKEN_PREFIX_PUBLIC, "paseto.pq1.public");
3272        assert_eq!(TOKEN_PREFIX_LOCAL, "paseto.pq1.local");
3273
3274        // Test that we always report non-standard compatibility
3275        assert!(!PasetoPQ::is_standard_paseto_compatible());
3276
3277        // Test prefix accessor methods
3278        assert_eq!(PasetoPQ::public_token_prefix(), TOKEN_PREFIX_PUBLIC);
3279        assert_eq!(PasetoPQ::local_token_prefix(), TOKEN_PREFIX_LOCAL);
3280    }
3281
3282    #[test]
3283    fn test_actual_token_contains_correct_prefix() {
3284        let mut rng = rng();
3285        let keypair = KeyPair::generate(&mut rng);
3286        let symmetric_key = SymmetricKey::generate(&mut rng);
3287
3288        let claims = Claims::new();
3289
3290        // Test public token uses correct prefix
3291        let public_token = PasetoPQ::sign(keypair.signing_key(), &claims).unwrap();
3292        assert!(public_token.starts_with(TOKEN_PREFIX_PUBLIC));
3293
3294        // Test local token uses correct prefix
3295        let local_token = PasetoPQ::encrypt(&symmetric_key, &claims).unwrap();
3296        assert!(local_token.starts_with(TOKEN_PREFIX_LOCAL));
3297
3298        // Verify tokens can be parsed with the correct prefix expectations
3299        let parsed_public = ParsedToken::parse(&public_token).unwrap();
3300        let parsed_local = ParsedToken::parse(&local_token).unwrap();
3301
3302        assert_eq!(parsed_public.version(), "pq1");
3303        assert_eq!(parsed_local.version(), "pq1");
3304
3305        assert_eq!(parsed_public.purpose(), "public");
3306        assert_eq!(parsed_local.purpose(), "local");
3307    }
3308
3309    #[test]
3310    fn test_hkdf_implementation() {
3311        // Test that proper HKDF produces different outputs for different inputs
3312        let shared_secret1 = b"shared_secret_1";
3313        let shared_secret2 = b"shared_secret_2";
3314        let info = b"PASETO-PQ-LOCAL-pq1";
3315
3316        let key1 = SymmetricKey::derive_from_shared_secret(shared_secret1, info);
3317        let key2 = SymmetricKey::derive_from_shared_secret(shared_secret2, info);
3318
3319        // Different secrets should produce different keys
3320        assert_ne!(key1.to_bytes(), key2.to_bytes());
3321
3322        // Same inputs should produce same outputs (deterministic)
3323        let key1_repeat = SymmetricKey::derive_from_shared_secret(shared_secret1, info);
3324        assert_eq!(key1.to_bytes(), key1_repeat.to_bytes());
3325
3326        // Different info should produce different keys with same secret
3327        let info2 = b"DIFFERENT-INFO";
3328        let key_diff_info = SymmetricKey::derive_from_shared_secret(shared_secret1, info2);
3329        assert_ne!(key1.to_bytes(), key_diff_info.to_bytes());
3330
3331        // Verify we get full 32 bytes
3332        assert_eq!(key1.to_bytes().len(), 32);
3333        assert_eq!(key2.to_bytes().len(), 32);
3334    }
3335
3336    #[test]
3337    fn test_footer_authentication_security_v0_1_1() {
3338        // Test that footer tampering is properly detected in v0.1.1
3339        let mut rng = rng();
3340        let keypair = KeyPair::generate(&mut rng);
3341        let symmetric_key = SymmetricKey::generate(&mut rng);
3342
3343        let mut claims = Claims::new();
3344        claims.set_subject("test-user".to_string()).unwrap();
3345        claims.set_issuer("test-issuer".to_string()).unwrap();
3346
3347        let mut footer = Footer::new();
3348        footer.set_kid("test-key-id").unwrap();
3349        footer.set_version("1.0").unwrap();
3350
3351        // Test public token footer authentication
3352        let public_token =
3353            PasetoPQ::sign_with_footer(keypair.signing_key(), &claims, Some(&footer)).unwrap();
3354
3355        // Valid token should verify successfully
3356        let verified =
3357            PasetoPQ::verify_with_footer(keypair.verifying_key(), &public_token).unwrap();
3358        assert_eq!(verified.claims().subject().unwrap(), "test-user");
3359        assert_eq!(verified.footer().unwrap().kid().unwrap(), "test-key-id");
3360
3361        // Tamper with footer in public token - should fail verification
3362        let mut token_parts: Vec<&str> = public_token.split('.').collect();
3363        // Create a valid but different footer JSON
3364        let tampered_footer = Footer::new();
3365        let tampered_footer_b64 = tampered_footer.to_base64().unwrap();
3366        token_parts[5] = &tampered_footer_b64;
3367        let tampered_public = token_parts.join(".");
3368
3369        let result = PasetoPQ::verify_with_footer(keypair.verifying_key(), &tampered_public);
3370        assert!(result.is_err());
3371        assert!(matches!(
3372            result.unwrap_err(),
3373            PqPasetoError::SignatureVerificationFailed
3374        ));
3375
3376        // Test local token footer authentication
3377        let local_token =
3378            PasetoPQ::encrypt_with_footer(&symmetric_key, &claims, Some(&footer)).unwrap();
3379
3380        // Valid token should decrypt successfully
3381        let decrypted = PasetoPQ::decrypt_with_footer(&symmetric_key, &local_token).unwrap();
3382        assert_eq!(decrypted.claims().subject().unwrap(), "test-user");
3383        assert_eq!(decrypted.footer().unwrap().kid().unwrap(), "test-key-id");
3384
3385        // Tamper with footer in local token - should fail decryption
3386        let mut token_parts: Vec<&str> = local_token.split('.').collect();
3387        // Create a valid but different footer JSON
3388        let tampered_footer = Footer::new();
3389        let tampered_footer_b64 = tampered_footer.to_base64().unwrap();
3390        token_parts[4] = &tampered_footer_b64;
3391        let tampered_local = token_parts.join(".");
3392
3393        let result = PasetoPQ::decrypt_with_footer(&symmetric_key, &tampered_local);
3394        assert!(result.is_err());
3395        assert!(matches!(
3396            result.unwrap_err(),
3397            PqPasetoError::DecryptionError(_)
3398        ));
3399    }
3400
3401    #[test]
3402    fn test_pae_integration_v0_1_1() {
3403        // Test that PAE encoding is working correctly in token operations
3404        let mut rng = rng();
3405        let keypair = KeyPair::generate(&mut rng);
3406
3407        let mut claims = Claims::new();
3408        claims.set_subject("pae-test".to_string()).unwrap();
3409        let mut footer = Footer::new();
3410        footer.set_kid("pae-key").unwrap();
3411
3412        // Create token with footer
3413        let token_with_footer =
3414            PasetoPQ::sign_with_footer(keypair.signing_key(), &claims, Some(&footer)).unwrap();
3415
3416        // Create token without footer
3417        let token_without_footer =
3418            PasetoPQ::sign_with_footer(keypair.signing_key(), &claims, None).unwrap();
3419
3420        // Both should verify successfully
3421        let verified_with =
3422            PasetoPQ::verify_with_footer(keypair.verifying_key(), &token_with_footer).unwrap();
3423        let verified_without =
3424            PasetoPQ::verify_with_footer(keypair.verifying_key(), &token_without_footer).unwrap();
3425
3426        assert_eq!(verified_with.claims().subject().unwrap(), "pae-test");
3427        assert_eq!(verified_without.claims().subject().unwrap(), "pae-test");
3428        assert!(verified_with.footer().is_some());
3429        assert!(verified_without.footer().is_none());
3430
3431        // Test that empty footer is handled correctly (should authenticate empty bytes)
3432        let claims_json = serde_json::to_vec(&claims).unwrap();
3433        let empty_footer_bytes = Vec::new();
3434        let header = "paseto.pq1.public".as_bytes();
3435
3436        let pae_message =
3437            crate::pae::pae_encode_public_token(header, &claims_json, &empty_footer_bytes);
3438
3439        // PAE message should contain the empty footer bytes (length 0)
3440        assert!(!pae_message.is_empty());
3441        // This proves empty footers are still authenticated via PAE
3442    }
3443
3444    #[test]
3445    fn test_v0_1_1_security_improvements() {
3446        // Comprehensive test demonstrating v0.1.1 security improvements
3447        let mut rng = rng();
3448        let keypair = KeyPair::generate(&mut rng);
3449        let symmetric_key = SymmetricKey::generate(&mut rng);
3450
3451        let mut claims = Claims::new();
3452        claims.set_subject("security-test".to_string()).unwrap();
3453        claims.set_audience("api.example.com".to_string()).unwrap();
3454
3455        // Test various footer content types
3456        let mut footer1 = Footer::new();
3457        footer1.set_kid("key-1").unwrap();
3458
3459        let mut footer2 = Footer::new();
3460        footer2.set_version("2.0").unwrap();
3461        footer2.set_kid("key-2").unwrap();
3462
3463        let mut footer3 = Footer::new();
3464        let admin_value = "admin";
3465        footer3.add_custom("role", &admin_value).unwrap();
3466
3467        let footers = [footer1, footer2, footer3];
3468
3469        for (i, footer) in footers.iter().enumerate() {
3470            // Test public tokens
3471            let public_token =
3472                PasetoPQ::sign_with_footer(keypair.signing_key(), &claims, Some(footer)).unwrap();
3473
3474            let verified =
3475                PasetoPQ::verify_with_footer(keypair.verifying_key(), &public_token).unwrap();
3476            assert_eq!(verified.claims().subject().unwrap(), "security-test");
3477
3478            // Test local tokens
3479            let local_token =
3480                PasetoPQ::encrypt_with_footer(&symmetric_key, &claims, Some(footer)).unwrap();
3481
3482            let decrypted = PasetoPQ::decrypt_with_footer(&symmetric_key, &local_token).unwrap();
3483            assert_eq!(decrypted.claims().subject().unwrap(), "security-test");
3484
3485            // Verify footer content is preserved
3486            match i {
3487                0 => assert_eq!(verified.footer().unwrap().kid().unwrap(), "key-1"),
3488                1 => {
3489                    assert_eq!(verified.footer().unwrap().version().unwrap(), "2.0");
3490                    assert_eq!(verified.footer().unwrap().kid().unwrap(), "key-2");
3491                }
3492                2 => {
3493                    let custom = verified.footer().unwrap().get_custom("role").unwrap();
3494                    assert_eq!(custom.as_str().unwrap(), "admin");
3495                }
3496                _ => unreachable!(),
3497            }
3498        }
3499    }
3500
3501    #[test]
3502    fn test_hkdf_vs_simple_hash_difference() {
3503        // Verify that proper HKDF produces different results than simple hash
3504        let shared_secret = b"test_shared_secret";
3505        let info = b"PASETO-PQ-LOCAL-pq1";
3506
3507        // Get HKDF result
3508        let hkdf_key = SymmetricKey::derive_from_shared_secret(shared_secret, info);
3509
3510        // Simulate old simple hash approach for comparison
3511        use sha2::{Digest, Sha256};
3512        let mut hasher = Sha256::new();
3513        hasher.update(shared_secret);
3514        hasher.update(info);
3515        let simple_hash = hasher.finalize();
3516
3517        // They should be different (proving we're using proper HKDF)
3518        assert_ne!(hkdf_key.to_bytes(), simple_hash.as_slice());
3519    }
3520}