common_access_token/
token.rs

1//! Token implementation for Common Access Token
2
3use crate::claims::{Claims, RegisteredClaims};
4use crate::error::Error;
5use crate::header::{Algorithm, CborValue, Header, HeaderMap, KeyId};
6use crate::utils::{compute_hmac_sha256, current_timestamp, verify_hmac_sha256};
7use minicbor::{Decoder, Encoder};
8use std::collections::BTreeMap;
9
10/// Common Access Token structure
11#[derive(Debug, Clone)]
12pub struct Token {
13    /// Token header
14    pub header: Header,
15    /// Token claims
16    pub claims: Claims,
17    /// Token signature
18    pub signature: Vec<u8>,
19    /// Original payload bytes (for verification)
20    original_payload_bytes: Option<Vec<u8>>,
21}
22
23impl Token {
24    /// Create a new token with the given header, claims, and signature
25    pub fn new(header: Header, claims: Claims, signature: Vec<u8>) -> Self {
26        Self {
27            header,
28            claims,
29            signature,
30            original_payload_bytes: None,
31        }
32    }
33
34    /// Encode the token to CBOR bytes
35    pub fn to_bytes(&self) -> Result<Vec<u8>, Error> {
36        let mut buf = Vec::new();
37        let mut enc = Encoder::new(&mut buf);
38
39        // For HMAC algorithms, use COSE_Mac0 format with CWT tag
40        if let Some(Algorithm::HmacSha256) = self.header.algorithm() {
41            // Apply CWT tag (61)
42            enc.tag(minicbor::data::Tag::new(61))?;
43            // Apply COSE_Mac0 tag (17)
44            enc.tag(minicbor::data::Tag::new(17))?;
45        }
46
47        // COSE structure array with 4 items
48        enc.array(4)?;
49
50        // 1. Protected header (encoded as CBOR and then as bstr)
51        let protected_bytes = encode_map(&self.header.protected)?;
52        enc.bytes(&protected_bytes)?;
53
54        // 2. Unprotected header
55        encode_map_direct(&self.header.unprotected, &mut enc)?;
56
57        // 3. Payload (encoded as CBOR and then as bstr)
58        let claims_map = self.claims.to_map();
59        let claims_bytes = encode_map(&claims_map)?;
60        enc.bytes(&claims_bytes)?;
61
62        // 4. Signature/MAC
63        enc.bytes(&self.signature)?;
64
65        Ok(buf)
66    }
67
68    /// Decode a token from CBOR bytes
69    ///
70    /// This function supports both COSE_Sign1 (tag 18) and COSE_Mac0 (tag 17) structures,
71    /// as well as custom tags. It will automatically skip any tags and process the underlying
72    /// CBOR array.
73    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
74        let mut dec = Decoder::new(bytes);
75
76        // Check if the token starts with a tag (COSE_Sign1 tag = 18, COSE_Mac0 tag = 17, or custom tag = 61)
77        if dec.datatype()? == minicbor::data::Type::Tag {
78            // Skip the tag
79            let _ = dec.tag()?;
80
81            // Check for a second tag
82            if dec.datatype()? == minicbor::data::Type::Tag {
83                let _ = dec.tag()?;
84            }
85        }
86
87        // Expect array with 4 items
88        let array_len = dec.array()?.unwrap_or(0);
89        if array_len != 4 {
90            return Err(Error::InvalidFormat(format!(
91                "Expected array of length 4, got {array_len}"
92            )));
93        }
94
95        // 1. Protected header
96        let protected_bytes = dec.bytes()?;
97        let protected = decode_map(protected_bytes)?;
98
99        // 2. Unprotected header
100        let unprotected = decode_map_direct(&mut dec)?;
101
102        // Create header
103        let header = Header {
104            protected,
105            unprotected,
106        };
107
108        // 3. Payload
109        let claims_bytes = dec.bytes()?;
110        let claims_map = decode_map(claims_bytes)?;
111        let claims = Claims::from_map(&claims_map);
112
113        // 4. Signature
114        let signature = dec.bytes()?.to_vec();
115
116        Ok(Self {
117            header,
118            claims,
119            signature,
120            original_payload_bytes: Some(claims_bytes.to_vec()),
121        })
122    }
123
124    /// Verify the token signature
125    ///
126    /// This function supports both COSE_Sign1 and COSE_Mac0 structures.
127    /// It will first try to verify the signature using the COSE_Sign1 structure,
128    /// and if that fails, it will try the COSE_Mac0 structure.
129    pub fn verify(&self, key: &[u8]) -> Result<(), Error> {
130        let alg = self.header.algorithm().ok_or_else(|| {
131            Error::InvalidFormat("Missing algorithm in protected header".to_string())
132        })?;
133
134        match alg {
135            Algorithm::HmacSha256 => {
136                // Try with COSE_Sign1 structure first
137                let sign1_input = self.sign1_input()?;
138                let sign1_result = verify_hmac_sha256(key, &sign1_input, &self.signature);
139
140                if sign1_result.is_ok() {
141                    return Ok(());
142                }
143
144                // If COSE_Sign1 verification fails, try COSE_Mac0 structure
145                let mac0_input = self.mac0_input()?;
146                verify_hmac_sha256(key, &mac0_input, &self.signature)
147            }
148        }
149    }
150
151    /// Verify the token claims
152    pub fn verify_claims(&self, options: &VerificationOptions) -> Result<(), Error> {
153        let now = current_timestamp();
154
155        // Check expiration
156        if options.verify_exp {
157            if let Some(exp) = self.claims.registered.exp {
158                if now >= exp {
159                    return Err(Error::Expired);
160                }
161            } else if options.require_exp {
162                return Err(Error::MissingClaim("exp".to_string()));
163            }
164        }
165
166        // Check not before
167        if options.verify_nbf {
168            if let Some(nbf) = self.claims.registered.nbf {
169                if now < nbf {
170                    return Err(Error::NotYetValid);
171                }
172            }
173        }
174
175        // Check issuer
176        if let Some(expected_iss) = &options.expected_issuer {
177            if let Some(iss) = &self.claims.registered.iss {
178                if iss != expected_iss {
179                    return Err(Error::InvalidIssuer);
180                }
181            } else if options.require_iss {
182                return Err(Error::MissingClaim("iss".to_string()));
183            }
184        }
185
186        // Check audience
187        if let Some(expected_aud) = &options.expected_audience {
188            if let Some(aud) = &self.claims.registered.aud {
189                if aud != expected_aud {
190                    return Err(Error::InvalidAudience);
191                }
192            } else if options.require_aud {
193                return Err(Error::MissingClaim("aud".to_string()));
194            }
195        }
196
197        // Check CAT-specific claims
198        if options.verify_catu {
199            self.verify_catu_claim(options)?;
200        }
201
202        if options.verify_catm {
203            self.verify_catm_claim(options)?;
204        }
205
206        if options.verify_catreplay {
207            self.verify_catreplay_claim(options)?;
208        }
209
210        Ok(())
211    }
212
213    /// Verify the CATU (URI) claim against the provided URI
214    fn verify_catu_claim(&self, options: &VerificationOptions) -> Result<(), Error> {
215        use crate::constants::{cat_keys, uri_components};
216        use url::Url;
217
218        // Get the URI to verify against
219        let uri = match &options.uri {
220            Some(uri) => uri,
221            None => {
222                return Err(Error::InvalidClaimValue(
223                    "No URI provided for CATU verification".to_string(),
224                ))
225            }
226        };
227
228        // Parse the URI
229        let parsed_uri = match Url::parse(uri) {
230            Ok(url) => url,
231            Err(_) => {
232                return Err(Error::InvalidClaimValue(format!(
233                    "Invalid URI format: {uri}"
234                )))
235            }
236        };
237
238        // Check if token has CATU claim
239        let catu_claim = match self.claims.custom.get(&cat_keys::CATU) {
240            Some(claim) => claim,
241            None => return Ok(()), // No CATU claim, so nothing to verify
242        };
243
244        // CATU claim should be a map
245        let component_map = match catu_claim {
246            CborValue::Map(map) => map,
247            _ => {
248                return Err(Error::InvalidUriClaim(
249                    "CATU claim is not a map".to_string(),
250                ))
251            }
252        };
253
254        // Verify each component in the CATU claim
255        for (component_key, component_value) in component_map {
256            match *component_key {
257                uri_components::SCHEME => {
258                    self.verify_uri_component(
259                        &parsed_uri.scheme().to_string(),
260                        component_value,
261                        "scheme",
262                    )?;
263                }
264                uri_components::HOST => {
265                    self.verify_uri_component(
266                        &parsed_uri.host_str().unwrap_or("").to_string(),
267                        component_value,
268                        "host",
269                    )?;
270                }
271                uri_components::PORT => {
272                    let port = parsed_uri.port().map(|p| p.to_string()).unwrap_or_default();
273                    self.verify_uri_component(&port, component_value, "port")?;
274                }
275                uri_components::PATH => {
276                    self.verify_uri_component(
277                        &parsed_uri.path().to_string(),
278                        component_value,
279                        "path",
280                    )?;
281                }
282                uri_components::QUERY => {
283                    let query = parsed_uri.query().unwrap_or("").to_string();
284                    self.verify_uri_component(&query, component_value, "query")?;
285                }
286                uri_components::EXTENSION => {
287                    // Extract file extension from path
288                    let path = parsed_uri.path();
289                    let extension = path.split('.').next_back().unwrap_or("").to_string();
290                    if !path.contains('.') || path.ends_with('.') {
291                        // No extension or ends with dot
292                        self.verify_uri_component(&"".to_string(), component_value, "extension")?;
293                    } else {
294                        self.verify_uri_component(
295                            &format!(".{extension}"),
296                            component_value,
297                            "extension",
298                        )?;
299                    }
300                }
301                _ => {
302                    // Ignore unsupported components
303                }
304            }
305        }
306
307        Ok(())
308    }
309
310    /// Verify a URI component against match conditions
311    fn verify_uri_component(
312        &self,
313        component: &String,
314        match_conditions: &CborValue,
315        component_name: &str,
316    ) -> Result<(), Error> {
317        use crate::constants::match_types;
318        use hmac_sha256::Hash as Sha256Hash;
319        use hmac_sha512::Hash as Sha512Hash;
320        use regex::Regex;
321
322        // Match conditions should be a map
323        let match_map = match match_conditions {
324            CborValue::Map(map) => map,
325            _ => {
326                return Err(Error::InvalidUriClaim(format!(
327                    "Match conditions for {component_name} is not a map"
328                )))
329            }
330        };
331
332        for (match_type, match_value) in match_map {
333            match *match_type {
334                match_types::EXACT => {
335                    if let CborValue::Text(text) = match_value {
336                        if component != text {
337                            return Err(Error::InvalidUriClaim(format!(
338                                "URI component {component_name} '{component}' does not exactly match required value '{text}'"
339                            )));
340                        }
341                    }
342                }
343                match_types::PREFIX => {
344                    if let CborValue::Text(prefix) = match_value {
345                        if !component.starts_with(prefix) {
346                            return Err(Error::InvalidUriClaim(format!(
347                                "URI component {component_name} '{component}' does not start with required prefix '{prefix}'"
348                            )));
349                        }
350                    }
351                }
352                match_types::SUFFIX => {
353                    if let CborValue::Text(suffix) = match_value {
354                        if !component.ends_with(suffix) {
355                            return Err(Error::InvalidUriClaim(format!(
356                                "URI component {component_name} '{component}' does not end with required suffix '{suffix}'"
357                            )));
358                        }
359                    }
360                }
361                match_types::CONTAINS => {
362                    if let CborValue::Text(contained) = match_value {
363                        if !component.contains(contained) {
364                            return Err(Error::InvalidUriClaim(format!(
365                                "URI component {component_name} '{component}' does not contain required text '{contained}'"
366                            )));
367                        }
368                    }
369                }
370                match_types::REGEX => {
371                    if let CborValue::Array(array) = match_value {
372                        if let Some(CborValue::Text(pattern)) = array.first() {
373                            match Regex::new(pattern) {
374                                Ok(regex) => {
375                                    if !regex.is_match(component) {
376                                        return Err(Error::InvalidUriClaim(format!(
377                                            "URI component {component_name} '{component}' does not match required regex pattern '{pattern}'"
378                                        )));
379                                    }
380                                }
381                                Err(_) => {
382                                    return Err(Error::InvalidUriClaim(format!(
383                                        "Invalid regex pattern: {pattern}"
384                                    )))
385                                }
386                            }
387                        }
388                    }
389                }
390                match_types::SHA256 => {
391                    if let CborValue::Bytes(expected_hash) = match_value {
392                        let hash = Sha256Hash::hash(component.as_bytes());
393
394                        if !ct_codecs::verify(&hash, expected_hash.as_slice()) {
395                            return Err(Error::InvalidUriClaim(format!(
396                                "URI component {component_name} '{component}' SHA-256 hash does not match expected value"
397                            )));
398                        }
399                    }
400                }
401                match_types::SHA512_256 => {
402                    if let CborValue::Bytes(expected_hash) = match_value {
403                        let hash = Sha512Hash::hash(component.as_bytes());
404                        let truncated_hash = &hash[0..32]; // Take first 256 bits (32 bytes)
405
406                        if !ct_codecs::verify(truncated_hash, &expected_hash[..]) {
407                            return Err(Error::InvalidUriClaim(format!(
408                                "URI component {component_name} '{component}' SHA-512/256 hash does not match expected value"
409                            )));
410                        }
411                    }
412                }
413                _ => {
414                    // Ignore unsupported match types
415                }
416            }
417        }
418
419        Ok(())
420    }
421
422    /// Verify the CATM (HTTP method) claim against the provided method
423    fn verify_catm_claim(&self, options: &VerificationOptions) -> Result<(), Error> {
424        use crate::constants::cat_keys;
425
426        // Get the HTTP method to verify against
427        let method = match &options.http_method {
428            Some(method) => method,
429            None => {
430                return Err(Error::InvalidClaimValue(
431                    "No HTTP method provided for CATM verification".to_string(),
432                ))
433            }
434        };
435
436        // Check if token has CATM claim
437        let catm_claim = match self.claims.custom.get(&cat_keys::CATM) {
438            Some(claim) => claim,
439            None => return Ok(()), // No CATM claim, so nothing to verify
440        };
441
442        // CATM claim should be an array of allowed methods
443        let allowed_methods = match catm_claim {
444            CborValue::Array(methods) => methods,
445            _ => {
446                return Err(Error::InvalidMethodClaim(
447                    "CATM claim is not an array".to_string(),
448                ))
449            }
450        };
451
452        // Check if the provided method is in the allowed methods list
453        let method_upper = method.to_uppercase();
454        let method_allowed = allowed_methods.iter().any(|m| {
455            if let CborValue::Text(allowed) = m {
456                allowed.to_uppercase() == method_upper
457            } else {
458                false
459            }
460        });
461
462        if !method_allowed {
463            return Err(Error::InvalidMethodClaim(format!(
464                "HTTP method '{}' is not allowed. Permitted methods: {:?}",
465                method,
466                allowed_methods
467                    .iter()
468                    .filter_map(|m| if let CborValue::Text(t) = m {
469                        Some(t.as_str())
470                    } else {
471                        None
472                    })
473                    .collect::<Vec<&str>>()
474            )));
475        }
476
477        Ok(())
478    }
479
480    /// Verify the CATREPLAY claim for token replay protection
481    fn verify_catreplay_claim(&self, options: &VerificationOptions) -> Result<(), Error> {
482        use crate::constants::{cat_keys, replay_values};
483
484        // Check if token has CATREPLAY claim
485        let catreplay_claim = match self.claims.custom.get(&cat_keys::CATREPLAY) {
486            Some(claim) => claim,
487            None => return Ok(()), // No CATREPLAY claim, so nothing to verify
488        };
489
490        // Get the replay protection value
491        let replay_value = match catreplay_claim {
492            CborValue::Integer(value) => *value as i32,
493            _ => {
494                return Err(Error::InvalidClaimValue(
495                    "CATREPLAY claim is not an integer".to_string(),
496                ))
497            }
498        };
499
500        match replay_value {
501            replay_values::PERMITTED => {
502                // Replay is permitted, no verification needed
503                Ok(())
504            }
505            replay_values::PROHIBITED => {
506                // Replay is prohibited, check if token has been seen before
507                if options.token_seen_before {
508                    Err(Error::ReplayViolation(
509                        "Token replay is prohibited".to_string(),
510                    ))
511                } else {
512                    Ok(())
513                }
514            }
515            replay_values::REUSE_DETECTION => {
516                // Reuse is detected but allowed, no error returned
517                // Implementations should log or notify about reuse
518                Ok(())
519            }
520            _ => Err(Error::InvalidClaimValue(format!(
521                "Invalid CATREPLAY value: {replay_value}"
522            ))),
523        }
524    }
525
526    // Note: signature_input method removed as we now use mac0_input for HMAC algorithms
527
528    /// Get the encoded payload bytes, using original bytes if available
529    fn get_payload_bytes(&self) -> Result<Vec<u8>, Error> {
530        if let Some(ref original) = self.original_payload_bytes {
531            // Use original bytes for verification
532            Ok(original.clone())
533        } else {
534            // Encode claims for newly created tokens
535            let claims_map = self.claims.to_map();
536            encode_map(&claims_map)
537        }
538    }
539
540    /// Get the COSE_Sign1 signature input
541    fn sign1_input(&self) -> Result<Vec<u8>, Error> {
542        // Sig_structure = [
543        //   context : "Signature1",
544        //   protected : bstr .cbor header_map,
545        //   external_aad : bstr,
546        //   payload : bstr .cbor claims
547        // ]
548
549        let mut buf = Vec::new();
550        let mut enc = Encoder::new(&mut buf);
551
552        // Start array with 4 items
553        enc.array(4)?;
554
555        // 1. Context
556        enc.str("Signature1")?;
557
558        // 2. Protected header
559        let protected_bytes = encode_map(&self.header.protected)?;
560        enc.bytes(&protected_bytes)?;
561
562        // 3. External AAD (empty in our case)
563        enc.bytes(&[])?;
564
565        // 4. Payload
566        let claims_bytes = self.get_payload_bytes()?;
567        enc.bytes(&claims_bytes)?;
568
569        Ok(buf)
570    }
571
572    /// Get the COSE_Mac0 signature input
573    fn mac0_input(&self) -> Result<Vec<u8>, Error> {
574        // Mac_structure = [
575        //   context : "MAC0",
576        //   protected : bstr .cbor header_map,
577        //   external_aad : bstr,
578        //   payload : bstr .cbor claims
579        // ]
580
581        let mut buf = Vec::new();
582        let mut enc = Encoder::new(&mut buf);
583
584        // Start array with 4 items
585        enc.array(4)?;
586
587        // 1. Context
588        enc.str("MAC0")?;
589
590        // 2. Protected header
591        let protected_bytes = encode_map(&self.header.protected)?;
592        enc.bytes(&protected_bytes)?;
593
594        // 3. External AAD (empty in our case)
595        enc.bytes(&[])?;
596
597        // 4. Payload
598        let claims_bytes = self.get_payload_bytes()?;
599        enc.bytes(&claims_bytes)?;
600
601        Ok(buf)
602    }
603
604    // Convenience methods for common token operations
605
606    /// Check if the token has expired
607    ///
608    /// Returns `true` if the token has an expiration claim and the current time is at or after it.
609    /// Returns `false` if the token has no expiration claim or if it hasn't expired yet.
610    ///
611    /// # Example
612    ///
613    /// ```
614    /// use common_access_token::{TokenBuilder, Algorithm, RegisteredClaims, current_timestamp};
615    ///
616    /// let key = b"my-secret-key";
617    /// let now = current_timestamp();
618    ///
619    /// // Token that expires in 1 hour
620    /// let token = TokenBuilder::new()
621    ///     .algorithm(Algorithm::HmacSha256)
622    ///     .registered_claims(RegisteredClaims::new().with_expiration(now + 3600))
623    ///     .sign(key)
624    ///     .unwrap();
625    ///
626    /// assert!(!token.is_expired());
627    /// ```
628    pub fn is_expired(&self) -> bool {
629        if let Some(exp) = self.claims.registered.exp {
630            current_timestamp() >= exp
631        } else {
632            false
633        }
634    }
635
636    /// Get the duration until token expiration
637    ///
638    /// Returns `Some(Duration)` if the token has an expiration claim and hasn't expired yet.
639    /// Returns `None` if the token has no expiration claim or has already expired.
640    ///
641    /// # Example
642    ///
643    /// ```
644    /// use common_access_token::{TokenBuilder, Algorithm, RegisteredClaims, current_timestamp};
645    ///
646    /// let key = b"my-secret-key";
647    /// let now = current_timestamp();
648    ///
649    /// let token = TokenBuilder::new()
650    ///     .algorithm(Algorithm::HmacSha256)
651    ///     .registered_claims(RegisteredClaims::new().with_expiration(now + 3600))
652    ///     .sign(key)
653    ///     .unwrap();
654    ///
655    /// if let Some(duration) = token.expires_in() {
656    ///     println!("Token expires in {} seconds", duration.as_secs());
657    /// }
658    /// ```
659    pub fn expires_in(&self) -> Option<std::time::Duration> {
660        if let Some(exp) = self.claims.registered.exp {
661            let now = current_timestamp();
662            if now < exp {
663                Some(std::time::Duration::from_secs(exp - now))
664            } else {
665                None
666            }
667        } else {
668            None
669        }
670    }
671
672    /// Check if the token is valid based on the not-before (nbf) claim
673    ///
674    /// Returns `true` if the token has no nbf claim or if the current time is at or after it.
675    /// Returns `false` if the token has an nbf claim and the current time is before it.
676    ///
677    /// # Example
678    ///
679    /// ```
680    /// use common_access_token::{TokenBuilder, Algorithm, RegisteredClaims, current_timestamp};
681    ///
682    /// let key = b"my-secret-key";
683    /// let now = current_timestamp();
684    ///
685    /// let token = TokenBuilder::new()
686    ///     .algorithm(Algorithm::HmacSha256)
687    ///     .registered_claims(RegisteredClaims::new().with_not_before(now))
688    ///     .sign(key)
689    ///     .unwrap();
690    ///
691    /// assert!(token.is_valid_yet());
692    /// ```
693    pub fn is_valid_yet(&self) -> bool {
694        if let Some(nbf) = self.claims.registered.nbf {
695            current_timestamp() >= nbf
696        } else {
697            true
698        }
699    }
700
701    /// Get the issuer claim value
702    ///
703    /// # Example
704    ///
705    /// ```
706    /// use common_access_token::{TokenBuilder, Algorithm, RegisteredClaims};
707    ///
708    /// let key = b"my-secret-key";
709    /// let token = TokenBuilder::new()
710    ///     .algorithm(Algorithm::HmacSha256)
711    ///     .registered_claims(RegisteredClaims::new().with_issuer("example-issuer"))
712    ///     .sign(key)
713    ///     .unwrap();
714    ///
715    /// assert_eq!(token.issuer(), Some("example-issuer"));
716    /// ```
717    pub fn issuer(&self) -> Option<&str> {
718        self.claims.registered.iss.as_deref()
719    }
720
721    /// Get the subject claim value
722    ///
723    /// # Example
724    ///
725    /// ```
726    /// use common_access_token::{TokenBuilder, Algorithm, RegisteredClaims};
727    ///
728    /// let key = b"my-secret-key";
729    /// let token = TokenBuilder::new()
730    ///     .algorithm(Algorithm::HmacSha256)
731    ///     .registered_claims(RegisteredClaims::new().with_subject("user-123"))
732    ///     .sign(key)
733    ///     .unwrap();
734    ///
735    /// assert_eq!(token.subject(), Some("user-123"));
736    /// ```
737    pub fn subject(&self) -> Option<&str> {
738        self.claims.registered.sub.as_deref()
739    }
740
741    /// Get the audience claim value
742    ///
743    /// # Example
744    ///
745    /// ```
746    /// use common_access_token::{TokenBuilder, Algorithm, RegisteredClaims};
747    ///
748    /// let key = b"my-secret-key";
749    /// let token = TokenBuilder::new()
750    ///     .algorithm(Algorithm::HmacSha256)
751    ///     .registered_claims(RegisteredClaims::new().with_audience("api-service"))
752    ///     .sign(key)
753    ///     .unwrap();
754    ///
755    /// assert_eq!(token.audience(), Some("api-service"));
756    /// ```
757    pub fn audience(&self) -> Option<&str> {
758        self.claims.registered.aud.as_deref()
759    }
760
761    /// Get the expiration timestamp
762    pub fn expiration(&self) -> Option<u64> {
763        self.claims.registered.exp
764    }
765
766    /// Get the not-before timestamp
767    pub fn not_before(&self) -> Option<u64> {
768        self.claims.registered.nbf
769    }
770
771    /// Get the issued-at timestamp
772    pub fn issued_at(&self) -> Option<u64> {
773        self.claims.registered.iat
774    }
775
776    /// Get a custom claim as a string
777    ///
778    /// Returns `Some(&str)` if the claim exists and is a text value, `None` otherwise.
779    ///
780    /// # Example
781    ///
782    /// ```
783    /// use common_access_token::{TokenBuilder, Algorithm};
784    ///
785    /// let key = b"my-secret-key";
786    /// let token = TokenBuilder::new()
787    ///     .algorithm(Algorithm::HmacSha256)
788    ///     .custom_string(100, "custom-value")
789    ///     .sign(key)
790    ///     .unwrap();
791    ///
792    /// assert_eq!(token.get_custom_string(100), Some("custom-value"));
793    /// assert_eq!(token.get_custom_string(999), None);
794    /// ```
795    pub fn get_custom_string(&self, key: i32) -> Option<&str> {
796        match self.claims.custom.get(&key) {
797            Some(CborValue::Text(s)) => Some(s.as_str()),
798            _ => None,
799        }
800    }
801
802    /// Get a custom claim as an integer
803    ///
804    /// Returns `Some(i64)` if the claim exists and is an integer value, `None` otherwise.
805    ///
806    /// # Example
807    ///
808    /// ```
809    /// use common_access_token::{TokenBuilder, Algorithm};
810    ///
811    /// let key = b"my-secret-key";
812    /// let token = TokenBuilder::new()
813    ///     .algorithm(Algorithm::HmacSha256)
814    ///     .custom_int(100, 42)
815    ///     .sign(key)
816    ///     .unwrap();
817    ///
818    /// assert_eq!(token.get_custom_int(100), Some(42));
819    /// assert_eq!(token.get_custom_int(999), None);
820    /// ```
821    pub fn get_custom_int(&self, key: i32) -> Option<i64> {
822        match self.claims.custom.get(&key) {
823            Some(CborValue::Integer(i)) => Some(*i),
824            _ => None,
825        }
826    }
827
828    /// Get a custom claim as binary data
829    ///
830    /// Returns `Some(&[u8])` if the claim exists and is a bytes value, `None` otherwise.
831    ///
832    /// # Example
833    ///
834    /// ```
835    /// use common_access_token::{TokenBuilder, Algorithm};
836    ///
837    /// let key = b"my-secret-key";
838    /// let data = vec![1, 2, 3, 4];
839    /// let token = TokenBuilder::new()
840    ///     .algorithm(Algorithm::HmacSha256)
841    ///     .custom_binary(100, data.clone())
842    ///     .sign(key)
843    ///     .unwrap();
844    ///
845    /// assert_eq!(token.get_custom_binary(100), Some(data.as_slice()));
846    /// assert_eq!(token.get_custom_binary(999), None);
847    /// ```
848    pub fn get_custom_binary(&self, key: i32) -> Option<&[u8]> {
849        match self.claims.custom.get(&key) {
850            Some(CborValue::Bytes(b)) => Some(b.as_slice()),
851            _ => None,
852        }
853    }
854
855    /// Get a reference to a custom claim value
856    ///
857    /// Returns `Some(&CborValue)` if the claim exists, `None` otherwise.
858    ///
859    /// # Example
860    ///
861    /// ```
862    /// use common_access_token::{TokenBuilder, Algorithm, CborValue};
863    ///
864    /// let key = b"my-secret-key";
865    /// let token = TokenBuilder::new()
866    ///     .algorithm(Algorithm::HmacSha256)
867    ///     .custom_string(100, "value")
868    ///     .sign(key)
869    ///     .unwrap();
870    ///
871    /// if let Some(CborValue::Text(s)) = token.get_custom_claim(100) {
872    ///     assert_eq!(s, "value");
873    /// }
874    /// ```
875    pub fn get_custom_claim(&self, key: i32) -> Option<&CborValue> {
876        self.claims.custom.get(&key)
877    }
878
879    /// Check if a custom claim exists
880    ///
881    /// # Example
882    ///
883    /// ```
884    /// use common_access_token::{TokenBuilder, Algorithm};
885    ///
886    /// let key = b"my-secret-key";
887    /// let token = TokenBuilder::new()
888    ///     .algorithm(Algorithm::HmacSha256)
889    ///     .custom_string(100, "value")
890    ///     .sign(key)
891    ///     .unwrap();
892    ///
893    /// assert!(token.has_custom_claim(100));
894    /// assert!(!token.has_custom_claim(999));
895    /// ```
896    pub fn has_custom_claim(&self, key: i32) -> bool {
897        self.claims.custom.contains_key(&key)
898    }
899}
900
901/// Options for token verification
902#[derive(Debug, Clone, Default)]
903pub struct VerificationOptions {
904    /// Verify expiration claim
905    pub verify_exp: bool,
906    /// Require expiration claim
907    pub require_exp: bool,
908    /// Verify not before claim
909    pub verify_nbf: bool,
910    /// Expected issuer
911    pub expected_issuer: Option<String>,
912    /// Require issuer claim
913    pub require_iss: bool,
914    /// Expected audience
915    pub expected_audience: Option<String>,
916    /// Require audience claim
917    pub require_aud: bool,
918    /// Verify CAT-specific URI claim (CATU) against provided URI
919    pub verify_catu: bool,
920    /// URI to verify against CATU claim
921    pub uri: Option<String>,
922    /// Verify CAT-specific HTTP methods claim (CATM) against provided method
923    pub verify_catm: bool,
924    /// HTTP method to verify against CATM claim
925    pub http_method: Option<String>,
926    /// Verify CAT-specific replay protection (CATREPLAY)
927    pub verify_catreplay: bool,
928    /// Whether the token has been seen before (for replay protection)
929    pub token_seen_before: bool,
930}
931
932impl VerificationOptions {
933    /// Create new default verification options
934    pub fn new() -> Self {
935        Self {
936            verify_exp: true,
937            require_exp: false,
938            verify_nbf: true,
939            expected_issuer: None,
940            require_iss: false,
941            expected_audience: None,
942            require_aud: false,
943            verify_catu: false,
944            uri: None,
945            verify_catm: false,
946            http_method: None,
947            verify_catreplay: false,
948            token_seen_before: false,
949        }
950    }
951
952    /// Set whether to verify expiration
953    pub fn verify_exp(mut self, verify: bool) -> Self {
954        self.verify_exp = verify;
955        self
956    }
957
958    /// Set whether to require expiration
959    pub fn require_exp(mut self, require: bool) -> Self {
960        self.require_exp = require;
961        self
962    }
963
964    /// Set whether to verify not before
965    pub fn verify_nbf(mut self, verify: bool) -> Self {
966        self.verify_nbf = verify;
967        self
968    }
969
970    /// Set expected issuer
971    pub fn expected_issuer<S: Into<String>>(mut self, issuer: S) -> Self {
972        self.expected_issuer = Some(issuer.into());
973        self
974    }
975
976    /// Set whether to require issuer
977    pub fn require_iss(mut self, require: bool) -> Self {
978        self.require_iss = require;
979        self
980    }
981
982    /// Set expected audience
983    pub fn expected_audience<S: Into<String>>(mut self, audience: S) -> Self {
984        self.expected_audience = Some(audience.into());
985        self
986    }
987
988    /// Set whether to require audience
989    pub fn require_aud(mut self, require: bool) -> Self {
990        self.require_aud = require;
991        self
992    }
993
994    /// Set whether to verify CAT-specific URI claim (CATU)
995    pub fn verify_catu(mut self, verify: bool) -> Self {
996        self.verify_catu = verify;
997        self
998    }
999
1000    /// Set URI to verify against CATU claim
1001    pub fn uri<S: Into<String>>(mut self, uri: S) -> Self {
1002        self.uri = Some(uri.into());
1003        self
1004    }
1005
1006    /// Set whether to verify CAT-specific HTTP methods claim (CATM)
1007    pub fn verify_catm(mut self, verify: bool) -> Self {
1008        self.verify_catm = verify;
1009        self
1010    }
1011
1012    /// Set HTTP method to verify against CATM claim
1013    pub fn http_method<S: Into<String>>(mut self, method: S) -> Self {
1014        self.http_method = Some(method.into());
1015        self
1016    }
1017
1018    /// Set whether to verify CAT-specific replay protection (CATREPLAY)
1019    pub fn verify_catreplay(mut self, verify: bool) -> Self {
1020        self.verify_catreplay = verify;
1021        self
1022    }
1023
1024    /// Set whether the token has been seen before (for replay protection)
1025    pub fn token_seen_before(mut self, seen: bool) -> Self {
1026        self.token_seen_before = seen;
1027        self
1028    }
1029}
1030
1031/// Builder for creating tokens
1032#[derive(Debug, Clone, Default)]
1033pub struct TokenBuilder {
1034    header: Header,
1035    claims: Claims,
1036}
1037
1038impl TokenBuilder {
1039    /// Create a new token builder
1040    pub fn new() -> Self {
1041        Self::default()
1042    }
1043
1044    /// Set the algorithm
1045    pub fn algorithm(mut self, alg: Algorithm) -> Self {
1046        self.header = self.header.with_algorithm(alg);
1047        self
1048    }
1049
1050    /// Set the key identifier in the protected header
1051    pub fn protected_key_id(mut self, kid: KeyId) -> Self {
1052        self.header = self.header.with_protected_key_id(kid);
1053        self
1054    }
1055
1056    /// Set the key identifier in the unprotected header
1057    pub fn unprotected_key_id(mut self, kid: KeyId) -> Self {
1058        self.header = self.header.with_unprotected_key_id(kid);
1059        self
1060    }
1061
1062    /// Set the registered claims
1063    pub fn registered_claims(mut self, claims: RegisteredClaims) -> Self {
1064        self.claims = self.claims.with_registered_claims(claims);
1065        self
1066    }
1067
1068    /// Add a custom claim with a string value
1069    pub fn custom_string<S: Into<String>>(mut self, key: i32, value: S) -> Self {
1070        self.claims = self.claims.with_custom_string(key, value);
1071        self
1072    }
1073
1074    /// Add a custom claim with a binary value
1075    pub fn custom_binary<B: Into<Vec<u8>>>(mut self, key: i32, value: B) -> Self {
1076        self.claims = self.claims.with_custom_binary(key, value);
1077        self
1078    }
1079
1080    /// Add a custom claim with an integer value
1081    pub fn custom_int(mut self, key: i32, value: i64) -> Self {
1082        self.claims = self.claims.with_custom_int(key, value);
1083        self
1084    }
1085
1086    /// Add a custom claim with a nested map value
1087    pub fn custom_map(mut self, key: i32, value: BTreeMap<i32, CborValue>) -> Self {
1088        self.claims = self.claims.with_custom_map(key, value);
1089        self
1090    }
1091
1092    /// Add a custom claim with a CborValue directly
1093    pub fn custom_cbor(mut self, key: i32, value: CborValue) -> Self {
1094        self.claims.custom.insert(key, value);
1095        self
1096    }
1097
1098    /// Add a custom claim with an array value
1099    pub fn custom_array(mut self, key: i32, value: Vec<CborValue>) -> Self {
1100        self.claims.custom.insert(key, CborValue::Array(value));
1101        self
1102    }
1103
1104    /// Set expiration time relative to now (in seconds)
1105    ///
1106    /// This is a convenience method that sets the expiration claim to the current time plus the specified number of seconds.
1107    ///
1108    /// # Example
1109    ///
1110    /// ```
1111    /// use common_access_token::{TokenBuilder, Algorithm, current_timestamp};
1112    ///
1113    /// let key = b"my-secret-key";
1114    ///
1115    /// // Token expires in 1 hour
1116    /// let token = TokenBuilder::new()
1117    ///     .algorithm(Algorithm::HmacSha256)
1118    ///     .expires_in_secs(3600)
1119    ///     .sign(key)
1120    ///     .unwrap();
1121    ///
1122    /// assert!(!token.is_expired());
1123    /// ```
1124    pub fn expires_in_secs(mut self, seconds: u64) -> Self {
1125        let exp = current_timestamp() + seconds;
1126        self.claims.registered.exp = Some(exp);
1127        self
1128    }
1129
1130    /// Set expiration time relative to now using a Duration
1131    ///
1132    /// This is a convenience method that sets the expiration claim to the current time plus the specified duration.
1133    ///
1134    /// # Example
1135    ///
1136    /// ```
1137    /// use common_access_token::{TokenBuilder, Algorithm};
1138    /// use std::time::Duration;
1139    ///
1140    /// let key = b"my-secret-key";
1141    ///
1142    /// // Token expires in 1 hour
1143    /// let token = TokenBuilder::new()
1144    ///     .algorithm(Algorithm::HmacSha256)
1145    ///     .expires_in(Duration::from_secs(3600))
1146    ///     .sign(key)
1147    ///     .unwrap();
1148    ///
1149    /// assert!(!token.is_expired());
1150    /// ```
1151    pub fn expires_in(self, duration: std::time::Duration) -> Self {
1152        self.expires_in_secs(duration.as_secs())
1153    }
1154
1155    /// Set token lifetime with issued-at and expiration claims
1156    ///
1157    /// This convenience method sets both the `iat` (issued at) and `exp` (expiration) claims.
1158    /// The issued-at is set to the current time, and expiration is set to current time plus the specified seconds.
1159    ///
1160    /// # Example
1161    ///
1162    /// ```
1163    /// use common_access_token::{TokenBuilder, Algorithm};
1164    ///
1165    /// let key = b"my-secret-key";
1166    ///
1167    /// // Token valid for 1 hour
1168    /// let token = TokenBuilder::new()
1169    ///     .algorithm(Algorithm::HmacSha256)
1170    ///     .valid_for_secs(3600)
1171    ///     .sign(key)
1172    ///     .unwrap();
1173    ///
1174    /// assert!(token.issued_at().is_some());
1175    /// assert!(token.expiration().is_some());
1176    /// ```
1177    pub fn valid_for_secs(mut self, seconds: u64) -> Self {
1178        let now = current_timestamp();
1179        self.claims.registered.iat = Some(now);
1180        self.claims.registered.exp = Some(now + seconds);
1181        self
1182    }
1183
1184    /// Set token lifetime with issued-at and expiration claims using a Duration
1185    ///
1186    /// This convenience method sets both the `iat` (issued at) and `exp` (expiration) claims.
1187    /// The issued-at is set to the current time, and expiration is set to current time plus the specified duration.
1188    ///
1189    /// # Example
1190    ///
1191    /// ```
1192    /// use common_access_token::{TokenBuilder, Algorithm};
1193    /// use std::time::Duration;
1194    ///
1195    /// let key = b"my-secret-key";
1196    ///
1197    /// // Token valid for 1 hour
1198    /// let token = TokenBuilder::new()
1199    ///     .algorithm(Algorithm::HmacSha256)
1200    ///     .valid_for(Duration::from_secs(3600))
1201    ///     .sign(key)
1202    ///     .unwrap();
1203    ///
1204    /// assert!(token.issued_at().is_some());
1205    /// assert!(token.expiration().is_some());
1206    /// ```
1207    pub fn valid_for(self, duration: std::time::Duration) -> Self {
1208        self.valid_for_secs(duration.as_secs())
1209    }
1210
1211    /// Build and sign the token
1212    pub fn sign(self, key: &[u8]) -> Result<Token, Error> {
1213        // Ensure we have an algorithm
1214        let alg = self.header.algorithm().ok_or_else(|| {
1215            Error::InvalidFormat("Missing algorithm in protected header".to_string())
1216        })?;
1217
1218        // Create token without signature
1219        let token = Token {
1220            header: self.header,
1221            claims: self.claims,
1222            signature: Vec::new(),
1223            original_payload_bytes: None,
1224        };
1225
1226        // Compute signature input based on algorithm
1227        // HMAC algorithms use COSE_Mac0 structure, others use COSE_Sign1
1228        let (_signature_input, signature) = match alg {
1229            Algorithm::HmacSha256 => {
1230                let mac_input = token.mac0_input()?;
1231                let mac = compute_hmac_sha256(key, &mac_input);
1232                (mac_input, mac)
1233            }
1234        };
1235
1236        // Create final token with signature
1237        Ok(Token {
1238            header: token.header,
1239            claims: token.claims,
1240            signature,
1241            original_payload_bytes: None,
1242        })
1243    }
1244}
1245
1246// Helper functions for CBOR encoding/decoding
1247
1248fn encode_map(map: &HeaderMap) -> Result<Vec<u8>, Error> {
1249    let mut buf = Vec::new();
1250    let mut enc = Encoder::new(&mut buf);
1251
1252    encode_map_direct(map, &mut enc)?;
1253
1254    Ok(buf)
1255}
1256
1257/// Encode a CBOR value directly to the encoder
1258fn encode_cbor_value(value: &CborValue, enc: &mut Encoder<&mut Vec<u8>>) -> Result<(), Error> {
1259    match value {
1260        CborValue::Integer(i) => {
1261            enc.i64(*i)?;
1262        }
1263        CborValue::Bytes(b) => {
1264            enc.bytes(b)?;
1265        }
1266        CborValue::Text(s) => {
1267            enc.str(s)?;
1268        }
1269        CborValue::Map(nested_map) => {
1270            // Create a nested encoder for the map
1271            encode_map_direct(nested_map, enc)?;
1272        }
1273        CborValue::Array(arr) => {
1274            // Create a nested encoder for the array
1275            enc.array(arr.len() as u64)?;
1276            for item in arr {
1277                encode_cbor_value(item, enc)?;
1278            }
1279        }
1280        CborValue::Null => {
1281            enc.null()?;
1282        }
1283    }
1284    Ok(())
1285}
1286
1287fn encode_map_direct(map: &HeaderMap, enc: &mut Encoder<&mut Vec<u8>>) -> Result<(), Error> {
1288    enc.map(map.len() as u64)?;
1289
1290    for (key, value) in map {
1291        enc.i32(*key)?;
1292        encode_cbor_value(value, enc)?;
1293    }
1294
1295    Ok(())
1296}
1297
1298fn decode_map(bytes: &[u8]) -> Result<HeaderMap, Error> {
1299    let mut dec = Decoder::new(bytes);
1300    decode_map_direct(&mut dec)
1301}
1302
1303/// Decode a CBOR array
1304fn decode_array(dec: &mut Decoder<'_>) -> Result<Vec<CborValue>, Error> {
1305    let array_len = dec.array()?.unwrap_or(0);
1306    let mut array = Vec::with_capacity(array_len as usize);
1307
1308    for _ in 0..array_len {
1309        // Try to decode based on the datatype
1310        let datatype = dec.datatype()?;
1311
1312        // Handle each type separately
1313        let value = if datatype == minicbor::data::Type::Int {
1314            // Integer value
1315            let i = dec.i64()?;
1316            CborValue::Integer(i)
1317        } else if datatype == minicbor::data::Type::U8
1318            || datatype == minicbor::data::Type::U16
1319            || datatype == minicbor::data::Type::U32
1320            || datatype == minicbor::data::Type::U64
1321        {
1322            // Unsigned integer value
1323            let i = dec.u64()? as i64;
1324            CborValue::Integer(i)
1325        } else if datatype == minicbor::data::Type::Bytes {
1326            // Byte string
1327            let b = dec.bytes()?;
1328            CborValue::Bytes(b.to_vec())
1329        } else if datatype == minicbor::data::Type::String {
1330            // Text string
1331            let s = dec.str()?;
1332            CborValue::Text(s.to_string())
1333        } else if datatype == minicbor::data::Type::Map {
1334            // Nested map
1335            let nested_map = decode_map_direct(dec)?;
1336            CborValue::Map(nested_map)
1337        } else if datatype == minicbor::data::Type::Array {
1338            // Nested array
1339            let nested_array = decode_array(dec)?;
1340            CborValue::Array(nested_array)
1341        } else if datatype == minicbor::data::Type::Null {
1342            // Null value
1343            dec.null()?;
1344            CborValue::Null
1345        } else {
1346            // Unsupported type
1347            return Err(Error::InvalidFormat(format!(
1348                "Unsupported CBOR type in array: {datatype:?}"
1349            )));
1350        };
1351
1352        array.push(value);
1353    }
1354
1355    Ok(array)
1356}
1357
1358fn decode_map_direct(dec: &mut Decoder<'_>) -> Result<HeaderMap, Error> {
1359    let map_len = dec.map()?.unwrap_or(0);
1360    let mut map = HeaderMap::new();
1361
1362    for _ in 0..map_len {
1363        let key = dec.i32()?;
1364
1365        // Try to decode based on the datatype
1366        let datatype = dec.datatype()?;
1367
1368        // Handle each type separately
1369        let value = if datatype == minicbor::data::Type::Int {
1370            // Integer value
1371            let i = dec.i64()?;
1372            CborValue::Integer(i)
1373        } else if datatype == minicbor::data::Type::U8
1374            || datatype == minicbor::data::Type::U16
1375            || datatype == minicbor::data::Type::U32
1376            || datatype == minicbor::data::Type::U64
1377        {
1378            // Unsigned integer value
1379            let i = dec.u64()? as i64;
1380            CborValue::Integer(i)
1381        } else if datatype == minicbor::data::Type::Bytes {
1382            // Byte string
1383            let b = dec.bytes()?;
1384            CborValue::Bytes(b.to_vec())
1385        } else if datatype == minicbor::data::Type::String {
1386            // Text string
1387            let s = dec.str()?;
1388            CborValue::Text(s.to_string())
1389        } else if datatype == minicbor::data::Type::Map {
1390            // Nested map
1391            let nested_map = decode_map_direct(dec)?;
1392            CborValue::Map(nested_map)
1393        } else if datatype == minicbor::data::Type::Array {
1394            // Array
1395            let array = decode_array(dec)?;
1396            CborValue::Array(array)
1397        } else if datatype == minicbor::data::Type::Null {
1398            // Null value
1399            dec.null()?;
1400            CborValue::Null
1401        } else {
1402            // Unsupported type
1403            return Err(Error::InvalidFormat(format!(
1404                "Unsupported CBOR type: {datatype:?}"
1405            )));
1406        };
1407
1408        map.insert(key, value);
1409    }
1410
1411    Ok(map)
1412}