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}