cratestack_cose/envelope/
builder.rs1use 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
16pub(crate) type Clock = Arc<dyn Fn() -> i64 + Send + Sync>;
18
19pub(crate) type CtiSource = Arc<dyn Fn() -> Result<Vec<u8>, CratestackError> + Send + Sync>;
21
22pub(crate) type NonceSource =
24 Arc<dyn Fn() -> Result<[u8; REQUEST_NONCE_LEN], CratestackError> + Send + Sync>;
25
26const SKEW_CHECK_HORIZON_SECS: u64 = 1000 * 366 * 24 * 60 * 60;
29
30pub struct CoseEnvelopeBuilder {
33 inner: Inner,
34}
35
36impl CoseEnvelope {
37 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 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 pub fn skew(mut self, skew: Duration) -> Self {
94 self.inner.skew_secs = skew.as_secs();
95 self
96 }
97
98 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 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 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 pub fn nonce_store(mut self, store: Arc<dyn NonceStore>) -> Self {
135 self.inner.nonce_store = Some(store);
136 self
137 }
138
139 #[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
185impl 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}