Skip to main content

cratestack_cose/keys/
hmac.rs

1//! HMAC secrets and the in-process COSE_Mac0 signer.
2
3use std::fmt;
4
5use cratestack_core::CratestackError;
6use hmac::{Hmac, KeyInit, Mac};
7use sha2::Sha256;
8use subtle::ConstantTimeEq;
9use zeroize::Zeroizing;
10
11use super::traits::CoseSigner;
12use super::verify_key::CoseVerifyKey;
13use crate::alg::CoseAlg;
14use crate::thumbprint::{self, KID_LEN};
15
16/// The shortest HMAC secret accepted, in bytes.
17///
18/// 32 bytes is SHA-256's output size, the key length RFC 2104 recommends
19/// as a floor. It is also what makes the `kid` safe to publish: a Mac0
20/// `kid` is a prefix of the secret's RFC 9679 thumbprint, and RFC 9679 §7
21/// forbids thumbprints of low-entropy secrets because a short one could be
22/// found by hashing candidates (P0 scoping decision).
23pub const MIN_HMAC_SECRET_LEN: usize = 32;
24
25/// An HMAC secret of at least [`MIN_HMAC_SECRET_LEN`] bytes.
26///
27/// **The secret must be uniformly random** (from a CSPRNG or a KMS), not a
28/// password or anything derived from a guessable value. Length alone does
29/// not make it safe: every COSE_Mac0 message publishes the secret's `kid`,
30/// the first 64 bits of `SHA-256` over a fixed encoding of the secret, so
31/// an attacker can test candidate secrets offline against any captured
32/// message at hashing speed, without a single online attempt. A 32-byte
33/// random secret makes that search hopeless; a 32-character passphrase
34/// does not.
35///
36/// `Debug` does not print it, `==` compares in constant time, and the bytes
37/// are wiped when the value is dropped (best effort: copies the caller made
38/// before handing the secret over, or a `Vec` that reallocated on its way
39/// here, are outside this type's reach).
40#[derive(Clone)]
41pub struct HmacSecret(Zeroizing<Vec<u8>>);
42
43impl PartialEq for HmacSecret {
44    fn eq(&self, other: &Self) -> bool {
45        bool::from(self.0.as_slice().ct_eq(other.0.as_slice()))
46    }
47}
48
49impl Eq for HmacSecret {}
50
51impl HmacSecret {
52    /// Take ownership of `secret`. Fails with `CratestackError::Validation`
53    /// below [`MIN_HMAC_SECRET_LEN`] bytes; see the type's docs for why the
54    /// bytes must also be random.
55    pub fn new(secret: impl Into<Vec<u8>>) -> Result<Self, CratestackError> {
56        let secret = Zeroizing::new(secret.into());
57        if secret.len() < MIN_HMAC_SECRET_LEN {
58            return Err(CratestackError::Validation(format!(
59                "HMAC secret must be at least {MIN_HMAC_SECRET_LEN} bytes"
60            )));
61        }
62        Ok(Self(secret))
63    }
64
65    pub(crate) fn expose(&self) -> &[u8] {
66        &self.0
67    }
68}
69
70impl fmt::Debug for HmacSecret {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        f.write_str("HmacSecret(<redacted>)")
73    }
74}
75
76/// HMAC-SHA-256 over the concatenation of `chunks`, fed one at a time (no
77/// contiguous copy), truncated to `alg`'s tag length (the first 8 bytes for
78/// HMAC 256/64, RFC 9053 §3.1). Only called with a MAC `alg`.
79pub(crate) fn hmac_tag(secret: &HmacSecret, alg: CoseAlg, chunks: &[&[u8]]) -> Vec<u8> {
80    // `new_from_slice` cannot fail for HMAC: any key length is valid.
81    let Ok(mut mac) = Hmac::<Sha256>::new_from_slice(secret.expose()) else {
82        return Vec::new();
83    };
84    for chunk in chunks {
85        mac.update(chunk);
86    }
87    let full = mac.finalize().into_bytes();
88    full[..alg.signature_len().min(full.len())].to_vec()
89}
90
91/// Whether `alg` is one of the two MAC algorithms.
92pub(crate) fn is_mac(alg: CoseAlg) -> bool {
93    matches!(alg, CoseAlg::Hmac256_64 | CoseAlg::Hmac256_256)
94}
95
96/// The in-process COSE_Mac0 signer: service to service inside one trust
97/// domain (§3). Its `kid` is the thumbprint prefix of the secret.
98#[derive(Clone)]
99pub struct HmacSigner {
100    secret: HmacSecret,
101    alg: CoseAlg,
102    kid: [u8; KID_LEN],
103}
104
105impl HmacSigner {
106    /// `alg` must be [`CoseAlg::Hmac256_64`] or [`CoseAlg::Hmac256_256`],
107    /// and the secret at least 32 random bytes (see [`HmacSecret`]). Fails
108    /// with `CratestackError::Validation` otherwise.
109    pub fn new(alg: CoseAlg, secret: impl Into<Vec<u8>>) -> Result<Self, CratestackError> {
110        Self::from_secret(alg, HmacSecret::new(secret)?)
111    }
112
113    /// As [`HmacSigner::new`], from an already validated secret.
114    pub fn from_secret(alg: CoseAlg, secret: HmacSecret) -> Result<Self, CratestackError> {
115        if !is_mac(alg) {
116            return Err(CratestackError::Validation(
117                "HmacSigner needs an HMAC algorithm".to_owned(),
118            ));
119        }
120        let kid =
121            thumbprint::kid_from_thumbprint(&thumbprint::symmetric_thumbprint(secret.expose()));
122        Ok(Self { secret, alg, kid })
123    }
124
125    /// The matching verification key, for a resolver. It carries this
126    /// signer's algorithm and accepts no other: a 256/256 key never accepts
127    /// a 64-bit tag made with the same secret.
128    pub fn verify_key(&self) -> CoseVerifyKey {
129        CoseVerifyKey::from_hmac_secret(self.alg, self.secret.clone())
130    }
131}
132
133impl fmt::Debug for HmacSigner {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        f.debug_struct("HmacSigner")
136            .field("alg", &self.alg)
137            .field("kid", &self.kid)
138            .finish_non_exhaustive()
139    }
140}
141
142#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
143#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
144impl CoseSigner for HmacSigner {
145    fn alg(&self) -> CoseAlg {
146        self.alg
147    }
148
149    fn kid(&self) -> &[u8] {
150        &self.kid
151    }
152
153    async fn sign(&self, to_be_signed: &[u8]) -> Result<Vec<u8>, CratestackError> {
154        Ok(hmac_tag(&self.secret, self.alg, &[to_be_signed]))
155    }
156
157    /// In process, so the MAC is computed over the structure's pieces,
158    /// without the envelope building it contiguously.
159    fn sign_chunks(&self, to_be_signed: &[&[u8]]) -> Option<Result<Vec<u8>, CratestackError>> {
160        Some(Ok(hmac_tag(&self.secret, self.alg, to_be_signed)))
161    }
162}