cratestack_cose/claims/sign.rs
1//! Signing a claim object.
2
3use super::error::ClaimsError;
4use super::header;
5use super::types::CwtClaims;
6use crate::alg::CoseMode;
7use crate::cbor::write::{self, EMPTY_MAP, MAJOR_ARRAY, MAJOR_TAG};
8use crate::keys::{CoseSigner, esp256_low_s};
9use crate::tbs::Tbs;
10use crate::thumbprint::KID_LEN;
11
12/// Sign `payload` as a claim object: a tagged `COSE_Sign1` (tag 18) whose
13/// protected header is `{1: alg, 4: kid, ? 15: claims, 16: typ}`, whose
14/// unprotected header is empty and whose payload is embedded.
15///
16/// - `typ` (RFC 9596, label 16) names what the object is. It is signed, and
17/// [`open_claims`](crate::open_claims) refuses any other, so an object
18/// minted for one purpose cannot be replayed as another. It is also what
19/// keeps a claim object from opening as a transport envelope, and the
20/// reverse. Non-empty, at most [`MAX_TYP_LEN`](crate::MAX_TYP_LEN) bytes.
21/// - `claims` are written only when present; see [`CwtClaims`] for the
22/// bounds.
23/// - `external_aad` is signed but not sent: the opener must supply the same
24/// bytes. Put in it what the object is bound to but does not carry, such
25/// as a tenant or a purpose; empty is fine.
26///
27/// The encoding is deterministic (RFC 8949 ยง4.2.1) and an ESP256 signature
28/// is low-`s`, the rules the transport envelope enforces. `signer` must be
29/// a signature signer (Ed25519 or ESP256); an HMAC signer is refused with
30/// [`ClaimsError::Misuse`]. Nothing is read from a clock: `exp` and `iat`
31/// are whatever the caller put in.
32///
33/// ```
34/// use cratestack_cose::{CwtClaims, Ed25519Signer, sign_claims};
35///
36/// # tokio::runtime::Builder::new_current_thread().build().unwrap().block_on(async {
37/// let signer = Ed25519Signer::from_seed(&[7; 32]);
38/// let claims = CwtClaims { exp: Some(1_790_003_600), ..CwtClaims::default() };
39/// let bytes = sign_claims(&signer, "application/vnd.example.grant+cwt", &claims, b"grant", b"")
40/// .await
41/// .unwrap();
42/// assert_eq!(bytes[0], 0xd2); // tag 18
43/// # });
44/// ```
45pub async fn sign_claims(
46 signer: &dyn CoseSigner,
47 typ: &str,
48 claims: &CwtClaims,
49 payload: &[u8],
50 external_aad: &[u8],
51) -> Result<Vec<u8>, ClaimsError> {
52 let alg = signer.alg();
53 if alg.mode() != CoseMode::Sign1 {
54 return Err(ClaimsError::Misuse(
55 "a claim object needs a signature algorithm, not a MAC",
56 ));
57 }
58 if signer.kid().len() != KID_LEN {
59 return Err(ClaimsError::Misuse("the signer's kid must be 8 bytes"));
60 }
61 if !header::typ_ok(typ) {
62 return Err(ClaimsError::Misuse("typ must be 1 to MAX_TYP_LEN bytes"));
63 }
64 header::check_claims(claims).map_err(ClaimsError::Misuse)?;
65
66 let protected = header::encode(alg, signer.kid(), typ, claims);
67 let tbs = Tbs {
68 mode: CoseMode::Sign1,
69 protected: &protected,
70 external_aad,
71 payload,
72 };
73 let signature = match tbs.with_chunks(|chunks| signer.sign_chunks(chunks)) {
74 Some(signed) => signed,
75 None => signer.sign(&tbs.assemble()).await,
76 }
77 .map_err(ClaimsError::Signer)?;
78 if signature.len() != alg.signature_len() {
79 return Err(ClaimsError::Misuse(
80 "the signer returned a signature of the wrong length",
81 ));
82 }
83 let signature = if alg == crate::CoseAlg::Esp256 {
84 esp256_low_s(&signature).ok_or(ClaimsError::Misuse(
85 "the signer returned an invalid ESP256 signature",
86 ))?
87 } else {
88 signature
89 };
90
91 let mut out = Vec::with_capacity(
92 2 + write::bstr_len(protected.len())
93 + 1
94 + write::bstr_len(payload.len())
95 + write::bstr_len(signature.len()),
96 );
97 write::head(&mut out, MAJOR_TAG, CoseMode::Sign1.tag());
98 write::head(&mut out, MAJOR_ARRAY, 4);
99 write::bstr(&mut out, &protected);
100 out.push(EMPTY_MAP);
101 write::bstr(&mut out, payload);
102 write::bstr(&mut out, &signature);
103 Ok(out)
104}