cratestack_cose/claims/open.rs
1//! Opening a claim object.
2
3use super::error::ClaimsError;
4use super::header;
5use super::types::OpenedClaims;
6use crate::alg::CoseMode;
7use crate::error::Reject;
8use crate::keys::{CoseVerifierResolver, CoseVerifyKey};
9use crate::tbs::Tbs;
10use crate::thumbprint::KID_LEN;
11use crate::wire;
12
13/// Open a claim object: parse it strictly, check its `typ`, and verify its
14/// signature with the key `resolver` returns for its `kid`.
15///
16/// **This checks authenticity, nothing else.** There is no skew window and
17/// no nonce store here, by design: a claim object is signed by one party
18/// and may be delivered much later by another, so a transport envelope's
19/// ±[`DEFAULT_SKEW_SECS`](crate::DEFAULT_SKEW_SECS) freshness would reject
20/// it. Whether `exp` has passed, whether `aud` is this service and whether
21/// the `cti` was already accepted are the caller's to enforce on the
22/// returned [`claims`](OpenedClaims::claims). An object with no `exp` never
23/// expires, and the same bytes open as many times as they are presented.
24///
25/// Anything but an exact match of this profile is [`ClaimsError::Malformed`]
26/// (checked before any key is looked up): an untagged or non-`COSE_Sign1`
27/// message, a non-minimal or indefinite CBOR head, an unknown, duplicate or
28/// out-of-order header label, an unprotected header entry, a detached
29/// payload, a MAC algorithm, a header with no `typ` (which is what a
30/// transport envelope looks like) and trailing bytes. A different `typ` is
31/// [`ClaimsError::WrongType`]; an unknown `kid` or a bad signature is
32/// [`ClaimsError::Unverified`]. `external_aad` must equal the bytes given
33/// to [`sign_claims`](crate::sign_claims). ESP256 accepts low-`s` only.
34///
35/// ```
36/// use cratestack_cose::{
37/// ClaimsError, CwtClaims, Ed25519Signer, StaticVerifierResolver, open_claims, sign_claims,
38/// };
39///
40/// # tokio::runtime::Builder::new_current_thread().build().unwrap().block_on(async {
41/// const TYP: &str = "application/vnd.example.grant+cwt";
42/// let signer = Ed25519Signer::from_seed(&[7; 32]);
43/// let resolver = StaticVerifierResolver::new().with_key(signer.verify_key());
44/// let claims = CwtClaims { exp: Some(1_790_003_600), ..CwtClaims::default() };
45/// let bytes = sign_claims(&signer, TYP, &claims, b"grant", b"tenant-1").await.unwrap();
46///
47/// let opened = open_claims(&resolver, &bytes, TYP, b"tenant-1").await.unwrap();
48/// assert_eq!(opened.payload, b"grant");
49/// // Still verifies years later: expiry is the caller's check.
50/// assert_eq!(opened.claims.exp, Some(1_790_003_600));
51///
52/// let wrong_aad = open_claims(&resolver, &bytes, TYP, b"tenant-2").await;
53/// assert!(matches!(wrong_aad, Err(ClaimsError::Unverified)));
54/// # });
55/// ```
56pub async fn open_claims(
57 resolver: &dyn CoseVerifierResolver,
58 bytes: &[u8],
59 expected_typ: &str,
60 external_aad: &[u8],
61) -> Result<OpenedClaims, ClaimsError> {
62 let malformed = |_: Reject| ClaimsError::Malformed;
63 let parts = wire::parse(CoseMode::Sign1, bytes).map_err(malformed)?;
64 let protected_bytes = &bytes[parts.protected];
65 let parsed = header::parse(protected_bytes).map_err(malformed)?;
66 if parsed.alg.mode() != CoseMode::Sign1 {
67 return Err(ClaimsError::Malformed);
68 }
69 if parsed.typ != expected_typ {
70 return Err(ClaimsError::WrongType);
71 }
72 let kid: [u8; KID_LEN] = protected_bytes[parsed.kid]
73 .try_into()
74 .map_err(|_| ClaimsError::Malformed)?;
75
76 let candidates = resolver
77 .resolve(&kid, parsed.alg)
78 .await
79 .map_err(ClaimsError::Resolver)?;
80 let payload = &bytes[parts.payload];
81 let signature = &bytes[parts.signature];
82 let tbs = Tbs {
83 mode: CoseMode::Sign1,
84 protected: protected_bytes,
85 external_aad,
86 payload,
87 };
88 // Candidates are filtered on the key's own `kid`, as the envelope does,
89 // so a resolver that over-returns cannot let a key sign under another's.
90 let key = candidates
91 .iter()
92 .find(|key| key.kid() == kid && key.verify(parsed.alg, &tbs, signature))
93 .ok_or(ClaimsError::Unverified)?;
94 Ok(OpenedClaims {
95 kid,
96 alg: parsed.alg,
97 key_thumbprint: CoseVerifyKey::thumbprint(key),
98 claims: parsed.claims,
99 payload: payload.to_vec(),
100 })
101}