Skip to main content

cratestack_cose/keys/
traits.rs

1//! The signing and key-resolution seams (ADR 0006 §1).
2
3use cratestack_core::CratestackError;
4
5use super::verify_key::CoseVerifyKey;
6use crate::alg::CoseAlg;
7use crate::maybe_send::MaybeSendSync;
8
9/// Produces signatures (or MAC tags) without handing out the key.
10///
11/// **Target-split** (cratestack#1007, the same split as the client's
12/// `RequestAuthorizer`): natively the trait is `Send + Sync` and its futures
13/// are `Send`, exactly as before. On `wasm32` it is neither, because a
14/// signer there is a WebCrypto call or a JS callback, and a `JsFuture` is
15/// never `Send`. An implementation that builds for both targets writes
16/// `#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]` and
17/// `#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]`.
18///
19/// `KeyProvider::resolve_signing_key` returns raw key bytes, which fits
20/// HMAC but not an asymmetric key that lives in an HSM or a KMS (§1: "keys
21/// are signed with, not exported"). A signer is asked to sign the complete
22/// `Sig_structure` / `MAC_structure` and nothing else.
23///
24/// `#[async_trait]` rather than `impl Future`: signers are held as
25/// `Arc<dyn CoseSigner>` so one envelope type serves every key backend, and
26/// a remote signer is a network call anyway, next to which the boxed
27/// future is noise.
28///
29/// The envelope checks what a signer returns: a `kid` that is not 8 bytes,
30/// an algorithm that does not fit the envelope's mode, or a signature whose
31/// length is not [`CoseAlg::signature_len`] is refused with a `500` before
32/// anything reaches the wire. An ESP256 signature is normalised to low-`s`.
33#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
34#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
35pub trait CoseSigner: MaybeSendSync + 'static {
36    /// The algorithm every signature from this signer uses.
37    fn alg(&self) -> CoseAlg;
38
39    /// The key's `kid`: the first 8 bytes of its RFC 9679 thumbprint (§3).
40    fn kid(&self) -> &[u8];
41
42    /// Sign (or MAC) `to_be_signed`. For HMAC 256/64 this returns the
43    /// truncated 8-byte tag. An error is a backend failure (a `500`).
44    async fn sign(&self, to_be_signed: &[u8]) -> Result<Vec<u8>, CratestackError>;
45
46    /// Sign the concatenation of `to_be_signed` without it ever being
47    /// built in one buffer, or `None` to have the envelope build it and
48    /// call [`sign`](Self::sign) instead (the default).
49    ///
50    /// This is how the in-process signers (HMAC, ESP256 and Ed25519) keep
51    /// the payload in the one place `seal_value` encoded it (maintainer
52    /// decisions on cratestack#1005): each hashes its input (Ed25519 twice),
53    /// so they can be fed the structure's pieces, which lie in the output
54    /// buffer, on the stack and in the header. The design has to stay
55    /// dyn-compatible (signers are `Arc<dyn CoseSigner>`), which rules out
56    /// a generic "feed this hasher" method; a slice of slices is the
57    /// dyn-compatible form of a stream of bytes. It is a provided method,
58    /// so adding it broke no signer. It is synchronous because it exists
59    /// for in-process signers only: a remote signer needs the bytes (or a
60    /// digest) on the wire and keeps `None`, so a KMS or HSM signer is
61    /// handed the contiguous structure.
62    ///
63    /// An override must return exactly what `sign` returns for the
64    /// concatenated bytes; the shared vectors check that for the shipped
65    /// signers.
66    fn sign_chunks(&self, to_be_signed: &[&[u8]]) -> Option<Result<Vec<u8>, CratestackError>> {
67        let _ = to_be_signed;
68        None
69    }
70}
71
72/// Finds the keys that may have produced a message.
73///
74/// Contract, which the envelope's error mapping depends on:
75///
76/// - **An unknown `kid` is `Ok(vec![])`, never `Err`.** Every `Err` is
77///   treated as a backend outage and becomes a `500`. A resolver that
78///   answered "no such key" with `Err` would turn an unknown `kid` into a
79///   distinguishable response, the oracle §10 forbids.
80/// - **Several candidates are normal.** Eight bytes of thumbprint collide
81///   at about 2³² keys, so a resolver returns every key whose `kid`
82///   matches, and the opener tries each (§3, Q5).
83/// - **Resolve by `(kid, alg)`.** One device may hold a classical key and a
84///   hybrid key side by side during a migration (Q5).
85///
86/// **Returning too much is harmless, but wasteful.** The opener accepts a
87/// candidate only if the candidate's own `kid` (computed from the key) is
88/// the one in the header and the candidate's algorithm is the header's, and
89/// the principal it records is the thumbprint of the key that verified. So
90/// a resolver that ignores `kid` or `alg` (say, one that returns every
91/// active key of a tenant) cannot let one key sign as another, or an HMAC
92/// secret stand in for a public key. It only costs a thumbprint comparison
93/// per extra candidate, and a lookup that should have been indexed.
94#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
95#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
96pub trait CoseVerifierResolver: MaybeSendSync + 'static {
97    /// Every key whose `kid` is `kid` and which verifies `alg`; `Ok(vec![])`
98    /// if there is none. `Err` only for a backend failure.
99    async fn resolve(
100        &self,
101        kid: &[u8],
102        alg: CoseAlg,
103    ) -> Result<Vec<CoseVerifyKey>, CratestackError>;
104}