Skip to main content

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}