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