1use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64;
13use base64::Engine;
14use hmac::{Hmac, Mac};
15use rand_core::{OsRng, RngCore};
16use secrecy::{ExposeSecret, SecretBox};
17use sha2::Sha256;
18use subtle::ConstantTimeEq;
19
20use crate::DpopError;
21
22pub trait IntoSecretBox {
25 fn into_secret_box(self) -> SecretBox<[u8]>;
26}
27
28impl IntoSecretBox for SecretBox<[u8]> {
29 fn into_secret_box(self) -> SecretBox<[u8]> {
30 self
31 }
32}
33
34impl IntoSecretBox for &SecretBox<[u8]> {
35 fn into_secret_box(self) -> SecretBox<[u8]> {
36 self.clone()
37 }
38}
39
40impl IntoSecretBox for &[u8] {
41 fn into_secret_box(self) -> SecretBox<[u8]> {
42 SecretBox::from(self.to_vec())
43 }
44}
45
46impl<const N: usize> IntoSecretBox for &[u8; N] {
47 fn into_secret_box(self) -> SecretBox<[u8]> {
48 SecretBox::from(self.to_vec())
49 }
50}
51
52impl IntoSecretBox for Vec<u8> {
53 fn into_secret_box(self) -> SecretBox<[u8]> {
54 SecretBox::from(self)
55 }
56}
57
58impl IntoSecretBox for &Vec<u8> {
59 fn into_secret_box(self) -> SecretBox<[u8]> {
60 SecretBox::from(self.clone())
61 }
62}
63
64type HmacSha256 = Hmac<Sha256>;
65
66const NONCE_VERSION: u8 = 1;
67const NONCE_RANDOM_LENGTH: usize = 16;
68const NONCE_MAC_LENGTH: usize = 16; const NONCE_TOTAL_LENGTH: usize = 1 + 8 + 16 + 16; const NONCE_FUTURE_SKEW_SECS: i64 = 5;
71
72pub struct NonceCtx<'a> {
74 pub htu: Option<&'a str>,
75 pub htm: Option<&'a str>,
76 pub jkt: Option<&'a str>,
77 pub client: Option<&'a str>,
78}
79
80fn ctx_bytes(ctx: &NonceCtx<'_>) -> Vec<u8> {
81 let mut context_bytes = Vec::new();
82 if let Some(htu) = ctx.htu {
83 context_bytes.extend_from_slice(b"HTU\0");
84 context_bytes.extend_from_slice(htu.as_bytes());
85 context_bytes.push(0);
86 }
87 if let Some(htm) = ctx.htm {
88 context_bytes.extend_from_slice(b"HTM\0");
89 context_bytes.extend_from_slice(htm.as_bytes());
90 context_bytes.push(0);
91 }
92 if let Some(jkt) = ctx.jkt {
93 context_bytes.extend_from_slice(b"JKT\0");
94 context_bytes.extend_from_slice(jkt.as_bytes());
95 context_bytes.push(0);
96 }
97 if let Some(client_id) = ctx.client {
98 context_bytes.extend_from_slice(b"CID\0");
99 context_bytes.extend_from_slice(client_id.as_bytes());
100 context_bytes.push(0);
101 }
102 context_bytes
103}
104
105pub fn issue_nonce<S>(secret: S, now_unix: i64, ctx: &NonceCtx<'_>) -> Result<String, DpopError>
110where
111 S: IntoSecretBox,
112{
113 let secret_box = secret.into_secret_box();
114 let version_bytes = [NONCE_VERSION];
115 let timestamp_bytes = now_unix.to_be_bytes();
116
117 let mut random_bytes = [0u8; NONCE_RANDOM_LENGTH];
118 OsRng.fill_bytes(&mut random_bytes);
119
120 let mut hmac = HmacSha256::new_from_slice(secret_box.expose_secret()).map_err(|_| DpopError::InvalidHmacConfig)?;
122
123 hmac.update(&version_bytes);
124 hmac.update(×tamp_bytes);
125 hmac.update(&random_bytes);
126 hmac.update(&ctx_bytes(ctx));
127 let mac_tag = hmac.finalize().into_bytes();
128
129 let mut nonce_bytes = Vec::with_capacity(NONCE_TOTAL_LENGTH);
130 nonce_bytes.extend_from_slice(&version_bytes);
131 nonce_bytes.extend_from_slice(×tamp_bytes);
132 nonce_bytes.extend_from_slice(&random_bytes);
133 nonce_bytes.extend_from_slice(&mac_tag[..NONCE_MAC_LENGTH]);
134
135 Ok(B64.encode(nonce_bytes))
136}
137
138pub fn verify_nonce<S>(
144 secret: S,
145 nonce_b64: &str,
146 now_unix: i64,
147 max_age_secs: i64,
148 ctx: &NonceCtx<'_>,
149) -> Result<(), DpopError>
150where
151 S: IntoSecretBox,
152{
153 let secret_box = secret.into_secret_box();
154 let nonce_bytes = B64
155 .decode(nonce_b64.as_bytes())
156 .map_err(|_| DpopError::NonceMismatch)?;
157
158 if nonce_bytes.len() != NONCE_TOTAL_LENGTH {
159 return Err(DpopError::NonceMismatch);
160 }
161
162 let version = nonce_bytes[0];
163 if version != NONCE_VERSION {
164 return Err(DpopError::NonceMismatch);
165 }
166
167 let timestamp_bytes: [u8; 8] = nonce_bytes
169 .get(1..9)
170 .and_then(|slice| slice.try_into().ok())
171 .ok_or(DpopError::NonceMismatch)?;
172 let timestamp = i64::from_be_bytes(timestamp_bytes);
173
174 let random_bytes = nonce_bytes
176 .get(9..9 + NONCE_RANDOM_LENGTH)
177 .ok_or(DpopError::NonceMismatch)?;
178 let mac_from_nonce = nonce_bytes
179 .get(9 + NONCE_RANDOM_LENGTH..)
180 .ok_or(DpopError::NonceMismatch)?;
181
182 if now_unix - timestamp > max_age_secs {
184 return Err(DpopError::NonceStale);
185 }
186 if timestamp - now_unix > NONCE_FUTURE_SKEW_SECS {
187 return Err(DpopError::FutureSkew);
188 }
189
190 let mut hmac = HmacSha256::new_from_slice(secret_box.expose_secret()).map_err(|_| DpopError::InvalidHmacConfig)?;
193
194 hmac.update(&[version]);
195 hmac.update(×tamp.to_be_bytes());
196 hmac.update(random_bytes);
197 hmac.update(&ctx_bytes(ctx));
198 let computed_mac = hmac.finalize().into_bytes();
199
200 if bool::from(mac_from_nonce.ct_eq(&computed_mac[..NONCE_MAC_LENGTH])) {
201 Ok(())
202 } else {
203 Err(DpopError::NonceMismatch)
204 }
205}
206
207pub fn verify_nonce_with_any<S>(
212 secrets: &[S],
213 nonce_b64: &str,
214 now_unix: i64,
215 max_age_secs: i64,
216 ctx: &NonceCtx<'_>,
217) -> Result<(), DpopError>
218where
219 S: IntoSecretBox + Clone,
220{
221 let mut last_error = DpopError::NonceMismatch;
222 for secret in secrets {
223 match verify_nonce(secret.clone(), nonce_b64, now_unix, max_age_secs, ctx) {
224 Ok(()) => return Ok(()),
225 Err(error @ DpopError::FutureSkew)
226 | Err(error @ DpopError::NonceStale)
227 | Err(error @ DpopError::NonceMismatch) => last_error = error,
228 Err(error) => return Err(error),
229 }
230 }
231 Err(last_error)
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use time::OffsetDateTime;
238
239 #[test]
240 fn roundtrip_ok_with_binding() {
241 let secret = SecretBox::from(b"supersecretkey".to_vec());
242 let current_time = OffsetDateTime::now_utc().unix_timestamp();
243 let context = NonceCtx {
244 htu: Some("https://ex.com/a"),
245 htm: Some("POST"),
246 jkt: Some("thumb"),
247 client: None,
248 };
249
250 let nonce = issue_nonce(&secret, current_time, &context).expect("issue_nonce");
251 assert!(verify_nonce(&secret, &nonce, current_time, 300, &context).is_ok());
252 }
253
254 #[test]
255 fn bad_ctx_fails() {
256 let secret = SecretBox::from(b"k".to_vec());
257 let current_time = OffsetDateTime::now_utc().unix_timestamp();
258 let original_context = NonceCtx {
259 htu: Some("https://ex.com/a"),
260 htm: Some("GET"),
261 jkt: Some("t"),
262 client: None,
263 };
264 let nonce = issue_nonce(&secret, current_time, &original_context).expect("issue_nonce");
265
266 let different_context = NonceCtx {
268 htu: Some("https://ex.com/b"),
269 htm: Some("GET"),
270 jkt: Some("t"),
271 client: None,
272 };
273 assert!(matches!(
274 verify_nonce(&secret, &nonce, current_time, 300, &different_context),
275 Err(DpopError::NonceMismatch)
276 ));
277 }
278
279 #[test]
280 fn stale_and_future_skew() {
281 let secret = SecretBox::from(b"k2".to_vec());
282 let current_time = OffsetDateTime::now_utc().unix_timestamp();
283 let empty_context = NonceCtx {
284 htu: None,
285 htm: None,
286 jkt: None,
287 client: None,
288 };
289
290 let future_nonce =
291 issue_nonce(&secret, current_time + 10, &empty_context).expect("issue_nonce");
292 assert!(matches!(
293 verify_nonce(&secret, &future_nonce, current_time, 300, &empty_context),
294 Err(DpopError::FutureSkew)
295 ));
296
297 let stale_nonce =
298 issue_nonce(&secret, current_time - 301, &empty_context).expect("issue_nonce");
299 assert!(matches!(
300 verify_nonce(&secret, &stale_nonce, current_time, 300, &empty_context),
301 Err(DpopError::NonceStale)
302 ));
303 }
304
305 #[test]
306 fn rotation_any_secret() {
307 let current_secret = SecretBox::from(b"current".to_vec());
308 let previous_secret = SecretBox::from(b"previous".to_vec());
309 let current_time = OffsetDateTime::now_utc().unix_timestamp();
310 let context = NonceCtx {
311 htu: Some("u"),
312 htm: Some("M"),
313 jkt: None,
314 client: None,
315 };
316
317 let nonce_from_previous =
318 issue_nonce(&previous_secret, current_time, &context).expect("issue_nonce");
319 assert!(verify_nonce_with_any(
320 &[¤t_secret, &previous_secret],
321 &nonce_from_previous,
322 current_time,
323 300,
324 &context
325 )
326 .is_ok());
327
328 let nonce_from_current =
329 issue_nonce(¤t_secret, current_time, &context).expect("issue_nonce");
330 assert!(verify_nonce_with_any(
331 &[¤t_secret, &previous_secret],
332 &nonce_from_current,
333 current_time,
334 300,
335 &context
336 )
337 .is_ok());
338 }
339
340 #[test]
341 fn accepts_non_boxed_types() {
342 let secret_bytes = b"test-secret";
344 let current_time = OffsetDateTime::now_utc().unix_timestamp();
345 let context = NonceCtx {
346 htu: Some("https://ex.com/a"),
347 htm: Some("GET"),
348 jkt: Some("thumb"),
349 client: None,
350 };
351
352 let nonce = issue_nonce(secret_bytes.as_slice(), current_time, &context).expect("issue_nonce");
353 assert!(verify_nonce(secret_bytes.as_slice(), &nonce, current_time, 300, &context).is_ok());
354
355 let secret_vec = b"test-secret-vec".to_vec();
357 let nonce2 = issue_nonce(&secret_vec, current_time, &context).expect("issue_nonce");
358 assert!(verify_nonce(&secret_vec, &nonce2, current_time, 300, &context).is_ok());
359 }
360}