1use 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#[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#[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};
117use 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
131use chacha20poly1305::{
133 ChaCha20Poly1305, Nonce,
134 aead::{AeadCore, AeadInPlace, KeyInit, OsRng as AeadOsRng},
135};
136
137#[cfg(feature = "logging")]
138use tracing::{debug, instrument, warn};
139
140pub struct PasetoPQ;
142
143pub use pae::pae_encode;
146
147#[derive(Clone)]
149pub struct KeyPair {
150 signing_key: SigningKey,
151 verifying_key: VerifyingKey,
152}
153
154#[derive(Clone)]
156pub struct SigningKey(ml_dsa::SigningKey<MlDsaParam>);
157
158#[derive(Clone)]
160pub struct VerifyingKey(ml_dsa::VerifyingKey<MlDsaParam>);
161
162#[derive(Clone, Zeroize, ZeroizeOnDrop)]
164pub struct SymmetricKey([u8; 32]);
165
166#[derive(Clone)]
168pub struct KemKeyPair {
169 pub encapsulation_key: EncapsulationKey,
170 pub decapsulation_key: DecapsulationKey,
171}
172
173#[derive(Clone)]
175pub struct EncapsulationKey(<MlKem768 as KemCore>::EncapsulationKey);
176
177#[derive(Clone)]
179pub struct DecapsulationKey(<MlKem768 as KemCore>::DecapsulationKey);
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct Footer {
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub kid: Option<String>,
187
188 #[serde(skip_serializing_if = "Option::is_none")]
190 pub version: Option<String>,
191
192 #[serde(skip_serializing_if = "Option::is_none")]
194 pub issuer_meta: Option<String>,
195
196 #[serde(flatten)]
198 pub custom: HashMap<String, Value>,
199}
200
201impl Footer {
202 pub fn new() -> Self {
204 Self {
205 kid: None,
206 version: None,
207 issuer_meta: None,
208 custom: HashMap::new(),
209 }
210 }
211
212 pub fn set_kid(&mut self, kid: &str) -> Result<(), PqPasetoError> {
214 self.kid = Some(kid.to_string());
215 Ok(())
216 }
217
218 pub fn set_version(&mut self, version: &str) -> Result<(), PqPasetoError> {
220 self.version = Some(version.to_string());
221 Ok(())
222 }
223
224 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 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 pub fn get_custom(&self, key: &str) -> Option<&Value> {
243 self.custom.get(key)
244 }
245
246 pub fn kid(&self) -> Option<&str> {
248 self.kid.as_deref()
249 }
250
251 pub fn version(&self) -> Option<&str> {
253 self.version.as_deref()
254 }
255
256 pub fn issuer_meta(&self) -> Option<&str> {
258 self.issuer_meta.as_deref()
259 }
260
261 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct Claims {
284 #[serde(skip_serializing_if = "Option::is_none")]
286 pub iss: Option<String>,
287
288 #[serde(skip_serializing_if = "Option::is_none")]
290 pub sub: Option<String>,
291
292 #[serde(skip_serializing_if = "Option::is_none")]
294 pub aud: Option<String>,
295
296 #[serde(
298 skip_serializing_if = "Option::is_none",
299 default,
300 with = "time::serde::rfc3339::option"
301 )]
302 pub exp: Option<OffsetDateTime>,
303
304 #[serde(
306 skip_serializing_if = "Option::is_none",
307 default,
308 with = "time::serde::rfc3339::option"
309 )]
310 pub nbf: Option<OffsetDateTime>,
311
312 #[serde(
314 skip_serializing_if = "Option::is_none",
315 default,
316 with = "time::serde::rfc3339::option"
317 )]
318 pub iat: Option<OffsetDateTime>,
319
320 #[serde(skip_serializing_if = "Option::is_none")]
322 pub jti: Option<String>,
323
324 #[serde(skip_serializing_if = "Option::is_none")]
326 pub kid: Option<String>,
327
328 #[serde(flatten)]
330 pub custom: HashMap<String, Value>,
331}
332
333#[derive(Debug, Clone)]
335pub struct VerifiedToken {
336 claims: Claims,
337 footer: Option<Footer>,
338 raw_token: String,
339}
340
341#[derive(Debug, Clone)]
368pub struct ParsedToken {
369 purpose: String,
370 version: String,
371 payload: Vec<u8>,
372 signature_or_tag: Option<Vec<u8>>, footer: Option<Footer>,
374 raw_token: String,
375}
376
377#[derive(Debug, Clone)]
382pub struct TokenSizeBreakdown {
383 pub prefix: usize,
385 pub payload: usize,
387 pub signature_or_tag: usize,
389 pub footer: Option<usize>,
391 pub separators: usize,
393 pub base64_overhead: usize,
395}
396
397#[derive(Debug, Clone)]
420pub struct TokenSizeEstimator {
421 breakdown: TokenSizeBreakdown,
422}
423
424#[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
467pub const TOKEN_PREFIX_PUBLIC: &str = "paseto.pq1.public";
481
482pub const TOKEN_PREFIX_LOCAL: &str = "paseto.pq1.local";
487
488const MAX_TOKEN_SIZE: usize = 1024 * 1024; const SYMMETRIC_KEY_SIZE: usize = 32;
490const NONCE_SIZE: usize = 12;
491
492impl KeyPair {
493 #[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 pub fn signing_key(&self) -> &SigningKey {
509 &self.signing_key
510 }
511
512 pub fn verifying_key(&self) -> &VerifyingKey {
514 &self.verifying_key
515 }
516
517 pub fn signing_key_to_bytes(&self) -> Vec<u8> {
519 let encoded = self.signing_key.0.encode();
520 encoded.to_vec()
521 }
522
523 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 pub fn verifying_key_to_bytes(&self) -> Vec<u8> {
533 let encoded = self.verifying_key.0.encode();
534 encoded.to_vec()
535 }
536
537 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 #[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 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 pub fn to_bytes(&self) -> [u8; SYMMETRIC_KEY_SIZE] {
575 self.0
576 }
577
578 pub fn derive_from_shared_secret(shared_secret: &[u8], info: &[u8]) -> Self {
584 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 #[cfg_attr(feature = "logging", instrument(skip(_rng)))]
598 pub fn generate<R: CryptoRng + RngCore>(_rng: &mut R) -> Self {
599 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 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 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 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 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 pub fn encapsulate(&self) -> (SymmetricKey, Vec<u8>) {
655 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 pub fn decapsulate(&self, ciphertext: &[u8]) -> Result<SymmetricKey, PqPasetoError> {
672 use ml_kem::array::Array;
673
674 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 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 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 pub fn set_issuer(&mut self, issuer: impl Into<String>) -> Result<(), PqPasetoError> {
713 self.iss = Some(issuer.into());
714 Ok(())
715 }
716
717 pub fn set_subject(&mut self, subject: impl Into<String>) -> Result<(), PqPasetoError> {
719 self.sub = Some(subject.into());
720 Ok(())
721 }
722
723 pub fn set_audience(&mut self, audience: impl Into<String>) -> Result<(), PqPasetoError> {
725 self.aud = Some(audience.into());
726 Ok(())
727 }
728
729 pub fn set_expiration(&mut self, exp: OffsetDateTime) -> Result<(), PqPasetoError> {
731 self.exp = Some(exp);
732 Ok(())
733 }
734
735 pub fn set_not_before(&mut self, nbf: OffsetDateTime) -> Result<(), PqPasetoError> {
737 self.nbf = Some(nbf);
738 Ok(())
739 }
740
741 pub fn set_issued_at(&mut self, iat: OffsetDateTime) -> Result<(), PqPasetoError> {
743 self.iat = Some(iat);
744 Ok(())
745 }
746
747 pub fn set_jti(&mut self, jti: impl Into<String>) -> Result<(), PqPasetoError> {
749 self.jti = Some(jti.into());
750 Ok(())
751 }
752
753 pub fn set_kid(&mut self, kid: impl Into<String>) -> Result<(), PqPasetoError> {
755 self.kid = Some(kid.into());
756 Ok(())
757 }
758
759 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 pub fn get_custom(&self, key: &str) -> Option<&Value> {
772 self.custom.get(key)
773 }
774
775 pub fn validate_time(
777 &self,
778 now: OffsetDateTime,
779 clock_skew_tolerance: time::Duration,
780 ) -> Result<(), PqPasetoError> {
781 if let Some(exp) = self.exp {
783 if now > exp + clock_skew_tolerance {
784 return Err(PqPasetoError::TokenExpired);
785 }
786 }
787
788 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 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 pub fn to_json_value(&self) -> serde_json::Value {
842 serde_json::Value::from(self.clone())
843 }
844
845 pub fn to_json_string(&self) -> Result<String, serde_json::Error> {
862 serde_json::to_string(self)
863 }
864
865 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
892impl From<Claims> for serde_json::Value {
914 fn from(claims: Claims) -> Self {
915 serde_json::to_value(claims).unwrap_or(serde_json::Value::Null)
918 }
919}
920
921impl 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 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 pub fn public(claims: &Claims, has_footer: bool) -> Self {
961 let claims_json = serde_json::to_string(claims).unwrap_or_default();
963 let claims_bytes = claims_json.len();
964
965 let payload_b64_len = claims_bytes.div_ceil(3) * 4; let prefix_len = TOKEN_PREFIX_PUBLIC.len() + 1; let signature_len = if cfg!(feature = "ml-dsa-44") {
972 2800 } else if cfg!(feature = "ml-dsa-65") {
974 4300 } else {
976 5000 };
978 let footer_len = if has_footer { 150 } else { 0 }; let separators = if has_footer { 3 } else { 2 }; let base64_overhead = (claims_bytes * 4).div_ceil(3) - claims_bytes; 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 pub fn local(claims: &Claims, has_footer: bool) -> Self {
1014 let claims_json = serde_json::to_string(claims).unwrap_or_default();
1016 let claims_bytes = claims_json.len();
1017
1018 let encrypted_payload_len = claims_bytes + 12 + 16; let payload_b64_len = encrypted_payload_len.div_ceil(3) * 4; let prefix_len = TOKEN_PREFIX_LOCAL.len() + 1; let footer_len = if has_footer { 150 } else { 0 }; let separators = if has_footer { 2 } else { 1 }; let base64_overhead = (encrypted_payload_len * 4).div_ceil(3) - encrypted_payload_len; let breakdown = TokenSizeBreakdown {
1029 prefix: prefix_len,
1030 payload: payload_b64_len,
1031 signature_or_tag: 0, footer: if has_footer { Some(footer_len) } else { None },
1033 separators,
1034 base64_overhead,
1035 };
1036
1037 Self { breakdown }
1038 }
1039
1040 pub fn total_bytes(&self) -> usize {
1042 self.breakdown.total()
1043 }
1044
1045 pub fn fits_in_cookie(&self) -> bool {
1047 self.total_bytes() <= 4096
1048 }
1049
1050 pub fn fits_in_url(&self) -> bool {
1052 self.total_bytes() <= 2048
1053 }
1054
1055 pub fn fits_in_header(&self) -> bool {
1057 self.total_bytes() <= 8192
1058 }
1059
1060 pub fn breakdown(&self) -> &TokenSizeBreakdown {
1062 &self.breakdown
1063 }
1064
1065 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 pub fn compare_to_jwt(&self) -> String {
1095 let jwt_typical = 200; 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 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 pub fn parse(token: &str) -> Result<Self, PqPasetoError> {
1145 let parts: Vec<&str> = token.split('.').collect();
1146
1147 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 if parts[0] != "paseto" {
1157 return Err(PqPasetoError::TokenParsingError(format!(
1158 "Invalid protocol: expected 'paseto', got '{}'",
1159 parts[0]
1160 )));
1161 }
1162
1163 let version = parts[1].to_string();
1165
1166 let purpose = parts[2].to_string();
1168
1169 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 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 match purpose.as_str() {
1190 "public" => {
1191 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 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!(), }
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 pub fn purpose(&self) -> &str {
1232 &self.purpose
1233 }
1234
1235 pub fn version(&self) -> &str {
1237 &self.version
1238 }
1239
1240 pub fn has_footer(&self) -> bool {
1242 self.footer.is_some()
1243 }
1244
1245 pub fn footer(&self) -> Option<&Footer> {
1247 self.footer.as_ref()
1248 }
1249
1250 pub fn payload_bytes(&self) -> &[u8] {
1252 &self.payload
1253 }
1254
1255 pub fn signature_bytes(&self) -> Option<&[u8]> {
1260 self.signature_or_tag.as_deref()
1261 }
1262
1263 pub fn payload_length(&self) -> usize {
1265 self.payload.len()
1266 }
1267
1268 pub fn total_length(&self) -> usize {
1270 self.raw_token.len()
1271 }
1272
1273 pub fn raw_token(&self) -> &str {
1275 &self.raw_token
1276 }
1277
1278 pub fn footer_json(&self) -> Option<Result<String, serde_json::Error>> {
1280 self.footer.as_ref().map(serde_json::to_string)
1281 }
1282
1283 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 pub fn is_public(&self) -> bool {
1290 self.purpose == "public"
1291 }
1292
1293 pub fn is_local(&self) -> bool {
1295 self.purpose == "local"
1296 }
1297
1298 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 pub fn claims(&self) -> &Claims {
1322 &self.claims
1323 }
1324
1325 pub fn footer(&self) -> Option<&Footer> {
1327 self.footer.as_ref()
1328 }
1329
1330 pub fn raw_token(&self) -> &str {
1332 &self.raw_token
1333 }
1334
1335 pub fn into_claims(self) -> Claims {
1337 self.claims
1338 }
1339
1340 pub fn into_parts(self) -> (Claims, Option<Footer>) {
1342 (self.claims, self.footer)
1343 }
1344}
1345
1346impl PasetoPQ {
1347 pub fn public_token_prefix() -> &'static str {
1352 TOKEN_PREFIX_PUBLIC
1353 }
1354
1355 pub fn local_token_prefix() -> &'static str {
1360 TOKEN_PREFIX_LOCAL
1361 }
1362
1363 pub fn is_standard_paseto_compatible() -> bool {
1368 false
1369 }
1370 pub fn parse_token(token: &str) -> Result<ParsedToken, PqPasetoError> {
1400 ParsedToken::parse(token)
1401 }
1402
1403 pub fn estimate_public_size(claims: &Claims, has_footer: bool) -> TokenSizeEstimator {
1431 TokenSizeEstimator::public(claims, has_footer)
1432 }
1433
1434 pub fn estimate_local_size(claims: &Claims, has_footer: bool) -> TokenSizeEstimator {
1462 TokenSizeEstimator::local(claims, has_footer)
1463 }
1464
1465 #[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 #[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 let payload_bytes = serde_json::to_vec(claims)?;
1480
1481 #[cfg(feature = "logging")]
1482 debug!("Serialized claims to {} bytes", payload_bytes.len());
1483
1484 let footer_bytes = match footer {
1486 Some(f) => serde_json::to_vec(f)?,
1487 None => Vec::new(), };
1489
1490 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 let signature = signing_key.0.sign(&pae_message);
1504 let signature_bytes = signature.to_bytes();
1505
1506 let encoded_payload = URL_SAFE_NO_PAD.encode(&payload_bytes);
1508 let encoded_signature = URL_SAFE_NO_PAD.encode(signature_bytes);
1509
1510 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 #[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 #[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 let payload_bytes = serde_json::to_vec(claims)?;
1550
1551 #[cfg(feature = "logging")]
1552 debug!("Serialized claims to {} bytes", payload_bytes.len());
1553
1554 let cipher = ChaCha20Poly1305::new((&symmetric_key.0).into());
1556
1557 let nonce = ChaCha20Poly1305::generate_nonce(&mut AeadOsRng);
1559
1560 let footer_bytes = match footer {
1562 Some(f) => serde_json::to_vec(f)?,
1563 None => Vec::new(), };
1565
1566 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 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 let mut ciphertext = buffer;
1586 ciphertext.extend_from_slice(&tag);
1587
1588 let mut encrypted_data = Vec::new();
1590 encrypted_data.extend_from_slice(&nonce);
1591 encrypted_data.extend_from_slice(&ciphertext);
1592
1593 let encoded_payload = URL_SAFE_NO_PAD.encode(&encrypted_data);
1595
1596 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 #[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 #[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 if token.len() > MAX_TOKEN_SIZE {
1632 return Err(PqPasetoError::InvalidFormat("Token too large".into()));
1633 }
1634
1635 let parts: Vec<&str> = token.splitn(5, '.').collect();
1637 let (encoded_payload, footer) = if parts.len() == 5 {
1638 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 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 let encrypted_data = URL_SAFE_NO_PAD.decode(encoded_payload).map_err(|e| {
1662 PqPasetoError::InvalidFormat(format!("Invalid payload encoding: {}", e))
1663 })?;
1664
1665 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 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 let footer_bytes = match &footer {
1688 Some(f) => serde_json::to_vec(f)?,
1689 None => Vec::new(), };
1691
1692 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 let cipher = ChaCha20Poly1305::new((&symmetric_key.0).into());
1706
1707 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 let claims: Claims = serde_json::from_slice(&payload_bytes)?;
1726
1727 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 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 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 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 verified
1787 .claims
1788 .validate_time(OffsetDateTime::now_utc(), clock_skew_tolerance)?;
1789
1790 Ok(verified)
1791 }
1792
1793 #[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 #[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 if token.len() > MAX_TOKEN_SIZE {
1810 return Err(PqPasetoError::InvalidFormat("Token too large".into()));
1811 }
1812
1813 let parts: Vec<&str> = token.splitn(6, '.').collect();
1815 let (encoded_payload, encoded_signature, footer) = if parts.len() == 6 {
1816 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 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 let payload_bytes = URL_SAFE_NO_PAD.decode(encoded_payload).map_err(|e| {
1840 PqPasetoError::InvalidFormat(format!("Invalid payload encoding: {}", e))
1841 })?;
1842
1843 let footer_bytes = match &footer {
1845 Some(f) => serde_json::to_vec(f)?,
1846 None => Vec::new(), };
1848
1849 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 let signature_bytes = URL_SAFE_NO_PAD.decode(encoded_signature).map_err(|e| {
1863 PqPasetoError::InvalidFormat(format!("Invalid signature encoding: {}", e))
1864 })?;
1865
1866 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 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 let claims: Claims = serde_json::from_slice(&payload_bytes)?;
1885
1886 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 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 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 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 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
2011impl Drop for SigningKey {
2016 fn drop(&mut self) {
2017 }
2020}
2021
2022impl Drop for VerifyingKey {
2023 fn drop(&mut self) {
2024 }
2027}
2028
2029impl Drop for KeyPair {
2030 fn drop(&mut self) {
2031 }
2033}
2034
2035impl Drop for EncapsulationKey {
2036 fn drop(&mut self) {
2037 }
2040}
2041
2042impl Drop for DecapsulationKey {
2043 fn drop(&mut self) {
2044 }
2047}
2048
2049impl Drop for KemKeyPair {
2050 fn drop(&mut self) {
2051 }
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 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 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 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 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 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 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 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 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 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 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 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 let result = PasetoPQ::verify(keypair.verifying_key(), "paseto.pq1");
2257 assert!(matches!(
2258 result.unwrap_err(),
2259 PqPasetoError::InvalidFormat(_)
2260 ));
2261
2262 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 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 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 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 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 let (sender_key, ciphertext) = keypair.encapsulate();
2314 let receiver_key = keypair.decapsulate(&ciphertext).unwrap();
2315
2316 assert_eq!(sender_key.to_bytes(), receiver_key.to_bytes());
2318 assert_ne!(sender_key.to_bytes(), [0u8; 32]); assert_eq!(ciphertext.len(), 1088); }
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 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 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 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 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 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 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 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 let mut tampered_token = token.clone();
2464 tampered_token.push('x'); let result = PasetoPQ::decrypt(&key, &tampered_token);
2467 assert!(result.is_err());
2468
2469 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 let result = PasetoPQ::decrypt(&key, "wrong.pq1.local.payload");
2485 assert!(matches!(
2486 result.unwrap_err(),
2487 PqPasetoError::InvalidFormat(_)
2488 ));
2489
2490 let result = PasetoPQ::decrypt(&key, "paseto.pq1.local.invalid!!!");
2492 assert!(matches!(
2493 result.unwrap_err(),
2494 PqPasetoError::InvalidFormat(_)
2495 ));
2496
2497 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 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 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 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 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); 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 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 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); 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 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 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()); }
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 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 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 let json_value_ref: Value = (&claims).into();
2724 assert_eq!(json_value, json_value_ref);
2725
2726 let json_value_method = claims.to_json_value();
2728 assert_eq!(json_value, json_value_method);
2729
2730 let json_string = claims.to_json_string().unwrap();
2732 assert!(json_string.contains("\"sub\":\"user123\""));
2733 assert!(json_string.contains("\"role\":\"admin\""));
2734
2735 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()); 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 assert!(json_value["exp"].is_string());
2767 assert!(json_value["nbf"].is_string());
2768 assert!(json_value["iat"].is_string());
2769
2770 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 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 let json_string = claims.to_json_string().unwrap();
2796 assert!(!json_string.is_empty());
2797
2798 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 let deserialized_value: Value = serde_json::from_slice(&serialized_for_db).unwrap();
2805 assert_eq!(json_value, deserialized_value);
2806
2807 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 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 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 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 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 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 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()); }
2874
2875 #[test]
2876 fn test_token_parsing_local_tokens() {
2877 let mut rng = rng();
2878 let key = SymmetricKey::generate(&mut rng);
2879
2880 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 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()); 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 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 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 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 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 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 let (expected_min_size, expected_max_size) = if cfg!(feature = "ml-dsa-44") {
2972 (2800, 3200) } else if cfg!(feature = "ml-dsa-65") {
2974 (4200, 4800) } else {
2976 (5000, 5500) };
2978
2979 assert!(estimator.total_bytes() >= expected_min_size);
2980 assert!(estimator.total_bytes() < expected_max_size);
2981
2982 if cfg!(feature = "ml-dsa-44") {
2984 assert!(estimator.fits_in_cookie()); assert!(!estimator.fits_in_url()); } else {
2988 assert!(!estimator.fits_in_cookie()); assert!(!estimator.fits_in_url()); }
2992 assert!(estimator.fits_in_header()); let breakdown = estimator.breakdown();
2996 assert!(breakdown.prefix > 0);
2997 assert!(breakdown.payload > 0);
2998
2999 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 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 assert!(estimator.total_bytes() > 80);
3024 assert!(estimator.total_bytes() < 300); assert!(estimator.fits_in_cookie());
3028 assert!(estimator.fits_in_url());
3029 assert!(estimator.fits_in_header());
3030
3031 let breakdown = estimator.breakdown();
3033 assert!(breakdown.prefix > 0);
3034 assert!(breakdown.payload > 0);
3035 assert_eq!(breakdown.signature_or_tag, 0); 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 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 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 let public_estimator = PasetoPQ::estimate_public_size(&claims, false);
3070 let local_estimator = PasetoPQ::estimate_local_size(&claims, true);
3071
3072 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 let small_claims = Claims::new();
3084 let small_estimator = TokenSizeEstimator::local(&small_claims, false);
3085 let small_suggestions = small_estimator.optimization_suggestions();
3086 assert!(small_suggestions.is_empty() || small_estimator.total_bytes() < 1000);
3088
3089 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 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(); 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 assert!(parsed.payload_length() > 100); assert!(parsed.total_length() > parsed.payload_length()); 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 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 let tokens = vec![
3177 (public_token, "public", true), (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 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 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 if size > 2048 {
3200 println!("Large token detected: {} bytes", size);
3202 }
3203 }
3204 }
3205
3206 #[test]
3207 fn test_symmetric_key_zeroization() {
3208 {
3210 let mut key = SymmetricKey([0x42u8; 32]);
3211
3212 assert_eq!(key.0[0], 0x42);
3214
3215 key.zeroize();
3217
3218 assert_eq!(key.0, [0u8; 32]);
3220 }
3221
3222 {
3224 let key = SymmetricKey([0x55u8; 32]);
3225 assert_eq!(key.0[0], 0x55);
3226 }
3228 }
3229
3230 #[test]
3231 fn test_key_operations_with_drop_cleanup() {
3232 let mut rng = rng();
3234
3235 {
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 }
3249
3250 {
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 }
3258
3259 {
3261 let key = SymmetricKey::generate(&mut rng);
3262 let key_bytes = key.to_bytes();
3263 assert_eq!(key_bytes.len(), 32);
3264 }
3266 }
3267
3268 #[test]
3269 fn test_token_versioning_configuration() {
3270 assert_eq!(TOKEN_PREFIX_PUBLIC, "paseto.pq1.public");
3272 assert_eq!(TOKEN_PREFIX_LOCAL, "paseto.pq1.local");
3273
3274 assert!(!PasetoPQ::is_standard_paseto_compatible());
3276
3277 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 let public_token = PasetoPQ::sign(keypair.signing_key(), &claims).unwrap();
3292 assert!(public_token.starts_with(TOKEN_PREFIX_PUBLIC));
3293
3294 let local_token = PasetoPQ::encrypt(&symmetric_key, &claims).unwrap();
3296 assert!(local_token.starts_with(TOKEN_PREFIX_LOCAL));
3297
3298 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 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 assert_ne!(key1.to_bytes(), key2.to_bytes());
3321
3322 let key1_repeat = SymmetricKey::derive_from_shared_secret(shared_secret1, info);
3324 assert_eq!(key1.to_bytes(), key1_repeat.to_bytes());
3325
3326 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 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 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 let public_token =
3353 PasetoPQ::sign_with_footer(keypair.signing_key(), &claims, Some(&footer)).unwrap();
3354
3355 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 let mut token_parts: Vec<&str> = public_token.split('.').collect();
3363 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 let local_token =
3378 PasetoPQ::encrypt_with_footer(&symmetric_key, &claims, Some(&footer)).unwrap();
3379
3380 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 let mut token_parts: Vec<&str> = local_token.split('.').collect();
3387 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 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 let token_with_footer =
3414 PasetoPQ::sign_with_footer(keypair.signing_key(), &claims, Some(&footer)).unwrap();
3415
3416 let token_without_footer =
3418 PasetoPQ::sign_with_footer(keypair.signing_key(), &claims, None).unwrap();
3419
3420 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 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 assert!(!pae_message.is_empty());
3441 }
3443
3444 #[test]
3445 fn test_v0_1_1_security_improvements() {
3446 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 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 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 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 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 let shared_secret = b"test_shared_secret";
3505 let info = b"PASETO-PQ-LOCAL-pq1";
3506
3507 let hkdf_key = SymmetricKey::derive_from_shared_secret(shared_secret, info);
3509
3510 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 assert_ne!(hkdf_key.to_bytes(), simple_hash.as_slice());
3519 }
3520}