Skip to main content

cratestack_cose/envelope/
builder.rs

1//! Building a [`CoseEnvelope`].
2
3use std::fmt;
4use std::sync::Arc;
5use std::time::Duration;
6
7use cratestack_core::{CratestackError, NonceStore};
8
9use super::{CoseEnvelope, CoseRole, Inner};
10use crate::alg::CoseMode;
11use crate::keys::{CoseSigner, CoseVerifierResolver};
12use crate::replay::{self, DEFAULT_SKEW_SECS};
13use crate::thumbprint::KID_LEN;
14use cratestack_core::REQUEST_NONCE_LEN;
15
16/// The clock `iat` is read from and checked against: Unix seconds.
17pub(crate) type Clock = Arc<dyn Fn() -> i64 + Send + Sync>;
18
19/// Where a request's `cti` comes from.
20pub(crate) type CtiSource = Arc<dyn Fn() -> Result<Vec<u8>, CratestackError> + Send + Sync>;
21
22/// Where [`CoseEnvelope::request_nonce`] draws its bytes from.
23pub(crate) type NonceSource =
24    Arc<dyn Fn() -> Result<[u8; REQUEST_NONCE_LEN], CratestackError> + Send + Sync>;
25
26/// How far past the build-time clock reading the skew is checked (1000
27/// years), so an envelope that builds never starts failing while it runs.
28const SKEW_CHECK_HORIZON_SECS: u64 = 1000 * 366 * 24 * 60 * 60;
29
30/// Configuration for a [`CoseEnvelope`]; see [`CoseEnvelope::server`] and
31/// [`CoseEnvelope::client`]. Nothing is checked until [`build`](Self::build).
32pub struct CoseEnvelopeBuilder {
33    inner: Inner,
34}
35
36impl CoseEnvelope {
37    /// A server envelope: opens requests (verifying with keys from
38    /// `resolver`, recording `(kid, cti)` in `nonce_store`) and seals
39    /// responses with `signer`.
40    pub fn server(
41        mode: CoseMode,
42        signer: Arc<dyn CoseSigner>,
43        resolver: Arc<dyn CoseVerifierResolver>,
44        nonce_store: Arc<dyn NonceStore>,
45    ) -> CoseEnvelopeBuilder {
46        CoseEnvelopeBuilder::new(CoseRole::Server, mode, signer, resolver, Some(nonce_store))
47    }
48
49    /// A client envelope: seals requests with `signer` and opens responses
50    /// with keys from `resolver` (typically the server keys pinned at
51    /// enrolment, §8).
52    pub fn client(
53        mode: CoseMode,
54        signer: Arc<dyn CoseSigner>,
55        resolver: Arc<dyn CoseVerifierResolver>,
56    ) -> CoseEnvelopeBuilder {
57        CoseEnvelopeBuilder::new(CoseRole::Client, mode, signer, resolver, None)
58    }
59}
60
61impl CoseEnvelopeBuilder {
62    fn new(
63        role: CoseRole,
64        mode: CoseMode,
65        signer: Arc<dyn CoseSigner>,
66        resolver: Arc<dyn CoseVerifierResolver>,
67        nonce_store: Option<Arc<dyn NonceStore>>,
68    ) -> Self {
69        Self {
70            inner: Inner {
71                role,
72                mode,
73                signer,
74                resolver,
75                nonce_store,
76                skew_secs: DEFAULT_SKEW_SECS,
77                clock: Arc::new(replay::system_clock),
78                cti: Arc::new(replay::random_cti),
79                nonce: Arc::new(|| replay::random_request_nonce().map(|nonce| *nonce.as_bytes())),
80            },
81        }
82    }
83
84    /// How far `iat` may be from the opener's clock, either way. Default
85    /// 300 s. Sub-second parts are ignored.
86    ///
87    /// It also sets how long a `(kid, cti)` stays in the nonce store:
88    /// `2·skew + 1` seconds past `iat` (the second `skew` covers clock
89    /// disagreement between replicas and the store; see `replay.rs`), so
90    /// the store's working set is every request of the last `2·skew + 1`
91    /// seconds. [`build`](Self::build) refuses a skew so large that this
92    /// expiry would leave the representable time range.
93    pub fn skew(mut self, skew: Duration) -> Self {
94        self.inner.skew_secs = skew.as_secs();
95        self
96    }
97
98    /// The clock `iat` is read from when sealing and checked against when
99    /// opening, in Unix seconds. Default: the system clock through
100    /// `chrono` (which, unlike `SystemTime`, works on
101    /// `wasm32-unknown-unknown`). Injectable so tests and the shared vectors
102    /// can pin `iat`. A reading before 1970 makes sealing a request fail
103    /// with a `500`.
104    pub fn clock(mut self, clock: impl Fn() -> i64 + Send + Sync + 'static) -> Self {
105        self.inner.clock = Arc::new(clock);
106        self
107    }
108
109    /// Where a request's `cti` comes from. Default: 16 bytes from the
110    /// operating system's CSPRNG (§5, `nonce` mode). Injectable so the
111    /// shared vectors can pin `cti`, in both the 16-byte and the 2-byte
112    /// counter shape. It must return 1 to 4 or 16 bytes; anything else, or
113    /// an `Err`, makes sealing fail with a `500`.
114    pub fn cti_source(
115        mut self,
116        cti: impl Fn() -> Result<Vec<u8>, CratestackError> + Send + Sync + 'static,
117    ) -> Self {
118        self.inner.cti = Arc::new(cti);
119        self
120    }
121
122    /// Where [`CoseEnvelope::request_nonce`] gets its 16 bytes. Default:
123    /// the operating system's CSPRNG. Injectable for tests and vectors.
124    pub fn nonce_source(
125        mut self,
126        nonce: impl Fn() -> Result<[u8; REQUEST_NONCE_LEN], CratestackError> + Send + Sync + 'static,
127    ) -> Self {
128        self.inner.nonce = Arc::new(nonce);
129        self
130    }
131
132    /// A nonce store for a client envelope that also opens requests through
133    /// [`CoseEnvelope::open_request`] (a relay, or a test).
134    pub fn nonce_store(mut self, store: Arc<dyn NonceStore>) -> Self {
135        self.inner.nonce_store = Some(store);
136        self
137    }
138
139    /// Check the configuration. Fails with `CratestackError::Validation`
140    /// when the signer's algorithm belongs to the other mode, its `kid` is
141    /// not 8 bytes, or the skew is so large that a nonce's expiry
142    /// (`iat + 2·skew + 1`, for any `iat` the opener could accept within the
143    /// next thousand years) would not be a representable time. Without that
144    /// last check such an envelope would build, and then answer every valid
145    /// request with a `401`.
146    #[cfg_attr(
147        target_arch = "wasm32",
148        allow(
149            clippy::arc_with_non_send_sync,
150            reason = "on wasm32 a browser signer's future is never `Send` (see `maybe_send.rs`), so `Inner` is not `Send + Sync` there by design; the `Arc` is a cheap shared handle, not a thread hand-off"
151        )
152    )]
153    pub fn build(self) -> Result<CoseEnvelope, CratestackError> {
154        let inner = self.inner;
155        if inner.signer.alg().mode() != inner.mode {
156            return Err(invalid(
157                "the signer's algorithm does not belong to the envelope's mode",
158            ));
159        }
160        if inner.signer.kid().len() != KID_LEN {
161            return Err(invalid(
162                "a COSE kid is the 8-byte RFC 9679 thumbprint prefix",
163            ));
164        }
165        let now = u64::try_from((inner.clock)()).unwrap_or(0);
166        let latest_iat = now
167            .checked_add(SKEW_CHECK_HORIZON_SECS)
168            .and_then(|at| at.checked_add(inner.skew_secs));
169        if latest_iat
170            .and_then(|iat| replay::nonce_expiry(iat, inner.skew_secs))
171            .is_none()
172        {
173            return Err(invalid("clock skew too large"));
174        }
175        Ok(CoseEnvelope {
176            inner: Arc::new(inner),
177        })
178    }
179}
180
181fn invalid(message: &str) -> CratestackError {
182    CratestackError::Validation(message.to_owned())
183}
184
185/// Shows the configuration, not the key material: signers expose only
186/// their algorithm and `kid`, and the injected closures are opaque.
187impl fmt::Debug for CoseEnvelopeBuilder {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        self.inner
190            .debug_fields(&mut f.debug_struct("CoseEnvelopeBuilder"))
191    }
192}