1use crate::uri::{normalize_htu, normalize_method};
2use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64;
3use base64::Engine;
4use serde::Deserialize;
5use sha2::{Digest, Sha256};
6use subtle::ConstantTimeEq;
7use time::OffsetDateTime;
8
9use crate::jwk::{thumbprint_ec_p256, verifying_key_from_p256_xy};
10use crate::replay::{ReplayContext, ReplayStore};
11use crate::DpopError;
12use p256::ecdsa::{signature::Verifier, Signature, VerifyingKey};
13
14#[derive(Deserialize)]
15struct DpopHeader {
16 typ: String,
17 alg: String,
18 jwk: Jwk,
19}
20#[derive(Deserialize)]
21struct Jwk {
22 kty: String,
23 crv: String,
24 x: String,
25 y: String,
26}
27
28#[derive(Clone, Debug)]
29pub enum NonceMode {
30 Disabled,
31 RequireEqual {
33 expected_nonce: String, },
35 Hmac {
37 secret: std::sync::Arc<[u8]>, max_age_secs: i64, bind_htu_htm: bool,
40 bind_jkt: bool,
41 },
42}
43
44#[derive(Debug, Clone)]
45pub struct VerifyOptions {
46 pub max_age_secs: i64,
47 pub future_skew_secs: i64,
48 pub nonce_mode: NonceMode,
49}
50impl Default for VerifyOptions {
51 fn default() -> Self {
52 Self {
53 max_age_secs: 300,
54 future_skew_secs: 5,
55 nonce_mode: NonceMode::Disabled,
56 }
57 }
58}
59
60#[derive(Debug)]
61pub struct VerifiedDpop {
62 pub jkt: String,
63 pub jti: String,
64 pub iat: i64,
65}
66
67pub async fn verify_proof<S: ReplayStore + ?Sized>(
69 store: &mut S,
70 dpop_compact_jws: &str,
71 expected_htu: &str,
72 expected_htm: &str,
73 maybe_access_token: Option<&str>,
74 opts: VerifyOptions,
75) -> Result<VerifiedDpop, DpopError> {
76 let mut it = dpop_compact_jws.split('.');
77 let (h_b64, p_b64, s_b64) = match (it.next(), it.next(), it.next()) {
78 (Some(h), Some(p), Some(s)) if it.next().is_none() => (h, p, s),
79 _ => return Err(DpopError::MalformedJws),
80 };
81
82 let hdr: DpopHeader = {
84 let bytes = B64.decode(h_b64).map_err(|_| DpopError::MalformedJws)?;
85 let val: serde_json::Value =
86 serde_json::from_slice(&bytes).map_err(|_| DpopError::MalformedJws)?;
87 if val.get("jwk").and_then(|j| j.get("d")).is_some() {
89 return Err(DpopError::BadJwk("jwk must not include 'd'"));
90 }
91 serde_json::from_value(val).map_err(|_| DpopError::MalformedJws)?
92 };
93
94 if hdr.typ != "dpop+jwt" {
95 return Err(DpopError::MalformedJws);
96 }
97 match hdr.alg.as_str() {
100 "ES256" => { }
101 "none" => return Err(DpopError::InvalidAlg("none".into())),
103 a if a.starts_with("HS") => return Err(DpopError::InvalidAlg(a.into())),
104 other => return Err(DpopError::UnsupportedAlg(other.into())),
105 }
106 if hdr.jwk.kty != "EC" || hdr.jwk.crv != "P-256" {
107 return Err(DpopError::BadJwk("expect EC P-256"));
108 }
109
110 let vk: VerifyingKey = verifying_key_from_p256_xy(&hdr.jwk.x, &hdr.jwk.y)?;
111
112 let signing_input = {
114 let mut s = String::with_capacity(h_b64.len() + 1 + p_b64.len());
115 s.push_str(h_b64);
116 s.push('.');
117 s.push_str(p_b64);
118 s
119 };
120
121 let sig_bytes = B64.decode(s_b64).map_err(|_| DpopError::InvalidSignature)?;
122 if sig_bytes.len() != 64 {
124 return Err(DpopError::InvalidSignature);
125 }
126 let sig = Signature::from_slice(&sig_bytes).map_err(|_| DpopError::InvalidSignature)?;
127 vk.verify(signing_input.as_bytes(), &sig)
128 .map_err(|_| DpopError::InvalidSignature)?;
129
130 let claims: serde_json::Value = {
131 let bytes = B64.decode(p_b64).map_err(|_| DpopError::MalformedJws)?;
132 serde_json::from_slice(&bytes).map_err(|_| DpopError::MalformedJws)?
133 };
134
135 let jti = claims
136 .get("jti")
137 .and_then(|v| v.as_str())
138 .ok_or(DpopError::MissingClaim("jti"))?;
139 if jti.len() > 512 {
140 return Err(DpopError::JtiTooLong);
141 }
142 let iat = claims
143 .get("iat")
144 .and_then(|v| v.as_i64())
145 .ok_or(DpopError::MissingClaim("iat"))?;
146 let htm = claims
147 .get("htm")
148 .and_then(|v| v.as_str())
149 .ok_or(DpopError::MissingClaim("htm"))?;
150 let htu = claims
151 .get("htu")
152 .and_then(|v| v.as_str())
153 .ok_or(DpopError::MissingClaim("htu"))?;
154
155 let want_htm = normalize_method(expected_htm)?; let got_htm = normalize_method(htm)?; if got_htm != want_htm {
159 return Err(DpopError::HtmMismatch);
160 }
161
162 let want_htu = normalize_htu(expected_htu)?; let got_htu = normalize_htu(htu)?;
164 if got_htu != want_htu {
165 return Err(DpopError::HtuMismatch);
166 }
167
168 if let Some(at) = maybe_access_token {
170 let want = Sha256::digest(at.as_bytes());
172 let got_b64 = claims
174 .get("ath")
175 .and_then(|v| v.as_str())
176 .ok_or(DpopError::MissingAth)?;
177 let got = B64
178 .decode(got_b64.as_bytes())
179 .map_err(|_| DpopError::AthMalformed)?;
180 if got.len() != want.len() || !bool::from(got.ct_eq(&want[..])) {
182 return Err(DpopError::AthMismatch);
183 }
184 }
185
186 let now = OffsetDateTime::now_utc().unix_timestamp();
188 if iat > now + opts.future_skew_secs {
189 return Err(DpopError::FutureSkew);
190 }
191 if now - iat > opts.max_age_secs {
192 return Err(DpopError::Stale);
193 }
194
195 let mut hasher = Sha256::new();
197 hasher.update(jti.as_bytes());
198 let mut jti_hash = [0u8; 32];
199 jti_hash.copy_from_slice(&hasher.finalize());
200
201 let jkt = thumbprint_ec_p256(&hdr.jwk.x, &hdr.jwk.y)?;
202
203 let nonce_claim = claims.get("nonce").and_then(|v| v.as_str());
204
205 match &opts.nonce_mode {
206 NonceMode::Disabled => { }
207 NonceMode::RequireEqual { expected_nonce } => {
208 let n = nonce_claim.ok_or(DpopError::MissingNonce)?;
209 if n != expected_nonce {
210 let fresh = expected_nonce.to_string(); return Err(DpopError::UseDpopNonce { nonce: fresh });
212 }
213 }
214 NonceMode::Hmac {
215 secret,
216 max_age_secs,
217 bind_htu_htm,
218 bind_jkt,
219 } => {
220 let n = match nonce_claim {
221 Some(s) => s,
222 None => {
223 let now = time::OffsetDateTime::now_utc().unix_timestamp();
225 let ctx = crate::nonce::NonceCtx {
226 htu: if *bind_htu_htm {
227 Some(want_htu.as_str())
228 } else {
229 None
230 },
231 htm: if *bind_htu_htm {
232 Some(want_htm.as_str())
233 } else {
234 None
235 },
236 jkt: if *bind_jkt { Some(jkt.as_str()) } else { None },
237 };
238 let fresh = crate::nonce::issue_nonce(secret, now, &ctx);
239 return Err(DpopError::UseDpopNonce { nonce: fresh });
240 }
241 };
242 let now = time::OffsetDateTime::now_utc().unix_timestamp();
243 let ctx = crate::nonce::NonceCtx {
244 htu: if *bind_htu_htm {
245 Some(want_htu.as_str())
246 } else {
247 None
248 },
249 htm: if *bind_htu_htm {
250 Some(want_htm.as_str())
251 } else {
252 None
253 },
254 jkt: if *bind_jkt { Some(jkt.as_str()) } else { None },
255 };
256 if let Err(_) = crate::nonce::verify_nonce(secret, n, now, *max_age_secs, &ctx) {
257 let fresh = crate::nonce::issue_nonce(secret, now, &ctx);
259 return Err(DpopError::UseDpopNonce { nonce: fresh });
260 }
261 }
262 }
263
264 let ok = store
265 .insert_once(
266 jti_hash,
267 ReplayContext {
268 jkt: Some(&jkt),
269 htm: Some(htm),
270 htu: Some(htu),
271 iat,
272 },
273 )
274 .await?;
275 if !ok {
276 return Err(DpopError::Replay);
277 }
278
279 Ok(VerifiedDpop {
280 jkt,
281 jti: jti.to_string(),
282 iat,
283 })
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289 use crate::jwk::thumbprint_ec_p256;
290 use crate::nonce::issue_nonce;
291 use p256::ecdsa::{signature::Signer, Signature, SigningKey};
292 use rand_core::OsRng;
293 use std::sync::Arc;
294
295 fn gen_es256_key() -> (SigningKey, String, String) {
298 let sk = SigningKey::random(&mut OsRng);
299 let vk = VerifyingKey::from(&sk);
300 let ep = vk.to_encoded_point(false);
301 let x = B64.encode(ep.x().unwrap());
302 let y = B64.encode(ep.y().unwrap());
303 (sk, x, y)
304 }
305
306 fn make_jws(
307 sk: &SigningKey,
308 header_val: serde_json::Value,
309 claims_val: serde_json::Value,
310 ) -> String {
311 let h = serde_json::to_vec(&header_val).unwrap();
312 let p = serde_json::to_vec(&claims_val).unwrap();
313 let h_b64 = B64.encode(h);
314 let p_b64 = B64.encode(p);
315 let signing_input = format!("{h_b64}.{p_b64}");
316 let sig: Signature = sk.sign(signing_input.as_bytes());
317 let s_b64 = B64.encode(sig.to_bytes());
318 format!("{h_b64}.{p_b64}.{s_b64}")
319 }
320
321 #[derive(Default)]
322 struct MemoryStore(std::collections::HashSet<[u8; 32]>);
323
324 #[async_trait::async_trait]
325 impl ReplayStore for MemoryStore {
326 async fn insert_once(
327 &mut self,
328 jti_hash: [u8; 32],
329 _ctx: ReplayContext<'_>,
330 ) -> Result<bool, DpopError> {
331 Ok(self.0.insert(jti_hash))
332 }
333 }
334 #[test]
336 fn thumbprint_has_expected_length_and_no_padding() {
337 let x = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
339 let y = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
340 let t1 = thumbprint_ec_p256(x, y).expect("thumbprint");
341 let t2 = thumbprint_ec_p256(x, y).expect("thumbprint");
342 assert_eq!(t1, t2);
344 assert_eq!(t1.len(), 43);
345 assert!(!t1.contains('='));
346 }
347
348 #[test]
349 fn decoding_key_rejects_wrong_sizes() {
350 let bad_x = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([0u8; 31]);
352 let good_y = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([0u8; 32]);
353 let res = crate::jwk::verifying_key_from_p256_xy(&bad_x, &good_y);
354 assert!(res.is_err(), "expected error for bad y");
355
356 let good_x = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([0u8; 32]);
358 let bad_y = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([0u8; 33]);
359 let res = crate::jwk::verifying_key_from_p256_xy(&good_x, &bad_y);
360 assert!(res.is_err(), "expected error for bad y");
361 }
362
363 #[tokio::test]
364 async fn replay_store_trait_basic() {
365 use async_trait::async_trait;
366 use std::collections::HashSet;
367
368 struct MemoryStore(HashSet<[u8; 32]>);
369
370 #[async_trait]
371 impl ReplayStore for MemoryStore {
372 async fn insert_once(
373 &mut self,
374 jti_hash: [u8; 32],
375 _ctx: ReplayContext<'_>,
376 ) -> Result<bool, DpopError> {
377 Ok(self.0.insert(jti_hash))
378 }
379 }
380
381 let mut s = MemoryStore(HashSet::new());
382 let first = s
383 .insert_once(
384 [42u8; 32],
385 ReplayContext {
386 jkt: Some("j"),
387 htm: Some("POST"),
388 htu: Some("https://ex"),
389 iat: 0,
390 },
391 )
392 .await
393 .unwrap();
394 let second = s
395 .insert_once(
396 [42u8; 32],
397 ReplayContext {
398 jkt: Some("j"),
399 htm: Some("POST"),
400 htu: Some("https://ex"),
401 iat: 0,
402 },
403 )
404 .await
405 .unwrap();
406 assert!(first);
407 assert!(!second); }
409 #[tokio::test]
410 async fn verify_valid_es256_proof() {
411 let (sk, x, y) = gen_es256_key();
412 let now = OffsetDateTime::now_utc().unix_timestamp();
413 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
414 let p = serde_json::json!({"jti":"j1","iat":now,"htm":"GET","htu":"https://api.example.com/resource"});
415 let jws = make_jws(&sk, h, p);
416
417 let mut store = MemoryStore::default();
418 let res = verify_proof(
419 &mut store,
420 &jws,
421 "https://api.example.com/resource",
422 "GET",
423 None,
424 VerifyOptions::default(),
425 )
426 .await;
427 assert!(res.is_ok(), "{res:?}");
428 }
429
430 #[tokio::test]
431 async fn method_normalization_allows_lowercase_claim() {
432 let (sk, x, y) = gen_es256_key();
433 let now = OffsetDateTime::now_utc().unix_timestamp();
434 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
435 let p = serde_json::json!({"jti":"j2","iat":now,"htm":"get","htu":"https://ex.com/a"});
436 let jws = make_jws(&sk, h, p);
437
438 let mut store = MemoryStore::default();
439 assert!(verify_proof(
440 &mut store,
441 &jws,
442 "https://ex.com/a",
443 "GET",
444 None,
445 VerifyOptions::default()
446 )
447 .await
448 .is_ok());
449 }
450
451 #[tokio::test]
452 async fn htu_normalizes_dot_segments_and_default_ports_and_strips_qf() {
453 let (sk, x, y) = gen_es256_key();
454 let now = OffsetDateTime::now_utc().unix_timestamp();
455 let claim_htu = "https://EX.COM:443/a/../b?q=1#frag";
457 let expect_htu = "https://ex.com/b";
458 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
459 let p = serde_json::json!({"jti":"j3","iat":now,"htm":"GET","htu":claim_htu});
460 let jws = make_jws(&sk, h, p);
461
462 let mut store = MemoryStore::default();
463 assert!(verify_proof(
464 &mut store,
465 &jws,
466 expect_htu,
467 "GET",
468 None,
469 VerifyOptions::default()
470 )
471 .await
472 .is_ok());
473 }
474
475 #[tokio::test]
476 async fn htu_path_case_mismatch_fails() {
477 let (sk, x, y) = gen_es256_key();
478 let now = OffsetDateTime::now_utc().unix_timestamp();
479 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
480 let p = serde_json::json!({"jti":"j4","iat":now,"htm":"GET","htu":"https://ex.com/API"});
481 let jws = make_jws(&sk, h, p);
482
483 let mut store = MemoryStore::default();
484 let err = verify_proof(
485 &mut store,
486 &jws,
487 "https://ex.com/api",
488 "GET",
489 None,
490 VerifyOptions::default(),
491 )
492 .await
493 .unwrap_err();
494 matches!(err, DpopError::HtuMismatch);
495 }
496
497 #[tokio::test]
498 async fn alg_none_rejected() {
499 let (sk, x, y) = gen_es256_key();
500 let now = OffsetDateTime::now_utc().unix_timestamp();
501 let h = serde_json::json!({"typ":"dpop+jwt","alg":"none","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
503 let p = serde_json::json!({"jti":"j5","iat":now,"htm":"GET","htu":"https://ex.com/a"});
504 let jws = make_jws(&sk, h, p);
505
506 let mut store = MemoryStore::default();
507 let err = verify_proof(
508 &mut store,
509 &jws,
510 "https://ex.com/a",
511 "GET",
512 None,
513 VerifyOptions::default(),
514 )
515 .await
516 .unwrap_err();
517 matches!(err, DpopError::InvalidAlg(_));
518 }
519
520 #[tokio::test]
521 async fn alg_hs256_rejected() {
522 let (sk, x, y) = gen_es256_key();
523 let now = OffsetDateTime::now_utc().unix_timestamp();
524 let h = serde_json::json!({"typ":"dpop+jwt","alg":"HS256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
525 let p = serde_json::json!({"jti":"j6","iat":now,"htm":"GET","htu":"https://ex.com/a"});
526 let jws = make_jws(&sk, h, p);
527
528 let mut store = MemoryStore::default();
529 let err = verify_proof(
530 &mut store,
531 &jws,
532 "https://ex.com/a",
533 "GET",
534 None,
535 VerifyOptions::default(),
536 )
537 .await
538 .unwrap_err();
539 matches!(err, DpopError::InvalidAlg(_));
540 }
541
542 #[tokio::test]
543 async fn jwk_with_private_d_rejected() {
544 let (sk, x, y) = gen_es256_key();
545 let now = OffsetDateTime::now_utc().unix_timestamp();
546 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y,"d":"AAAA"}});
548 let p = serde_json::json!({"jti":"j7","iat":now,"htm":"GET","htu":"https://ex.com/a"});
549 let jws = make_jws(&sk, h, p);
550
551 let mut store = MemoryStore::default();
552 let err = verify_proof(
553 &mut store,
554 &jws,
555 "https://ex.com/a",
556 "GET",
557 None,
558 VerifyOptions::default(),
559 )
560 .await
561 .unwrap_err();
562 matches!(err, DpopError::BadJwk(_));
563 }
564
565 #[tokio::test]
566 async fn ath_binding_ok_and_mismatch_and_padded_rejected() {
567 let (sk, x, y) = gen_es256_key();
568 let now = OffsetDateTime::now_utc().unix_timestamp();
569 let at = "access.token.string";
570 let ath = B64.encode(Sha256::digest(at.as_bytes()));
571 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
572
573 let p_ok = serde_json::json!({"jti":"j8","iat":now,"htm":"GET","htu":"https://ex.com/a","ath":ath});
575 let jws_ok = make_jws(&sk, h.clone(), p_ok);
576 let mut store = MemoryStore::default();
577 assert!(verify_proof(
578 &mut store,
579 &jws_ok,
580 "https://ex.com/a",
581 "GET",
582 Some(at),
583 VerifyOptions::default()
584 )
585 .await
586 .is_ok());
587
588 let p_bad = serde_json::json!({"jti":"j9","iat":now,"htm":"GET","htu":"https://ex.com/a","ath":ath});
590 let jws_bad = make_jws(&sk, h.clone(), p_bad);
591 let mut store2 = MemoryStore::default();
592 let err = verify_proof(
593 &mut store2,
594 &jws_bad,
595 "https://ex.com/a",
596 "GET",
597 Some("different.token"),
598 VerifyOptions::default(),
599 )
600 .await
601 .unwrap_err();
602 matches!(err, DpopError::AthMismatch);
603
604 let ath_padded = format!("{ath}==");
606 let p_pad = serde_json::json!({"jti":"j10","iat":now,"htm":"GET","htu":"https://ex.com/a","ath":ath_padded});
607 let jws_pad = make_jws(&sk, h.clone(), p_pad);
608 let mut store3 = MemoryStore::default();
609 let err = verify_proof(
610 &mut store3,
611 &jws_pad,
612 "https://ex.com/a",
613 "GET",
614 Some(at),
615 VerifyOptions::default(),
616 )
617 .await
618 .unwrap_err();
619 matches!(err, DpopError::AthMalformed);
620 }
621
622 #[tokio::test]
623 async fn freshness_future_skew_and_stale() {
624 let (sk, x, y) = gen_es256_key();
625 let now = OffsetDateTime::now_utc().unix_timestamp();
626 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
627
628 let p_future =
630 serde_json::json!({"jti":"jf","iat":now + 6,"htm":"GET","htu":"https://ex.com/a"});
631 let jws_future = make_jws(&sk, h.clone(), p_future);
632 let mut store1 = MemoryStore::default();
633 let opts = VerifyOptions {
634 max_age_secs: 300,
635 future_skew_secs: 5,
636 nonce_mode: NonceMode::Disabled,
637 };
638 let err = verify_proof(
639 &mut store1,
640 &jws_future,
641 "https://ex.com/a",
642 "GET",
643 None,
644 opts,
645 )
646 .await
647 .unwrap_err();
648 matches!(err, DpopError::FutureSkew);
649
650 let p_stale =
652 serde_json::json!({"jti":"js","iat":now - 301,"htm":"GET","htu":"https://ex.com/a"});
653 let jws_stale = make_jws(&sk, h.clone(), p_stale);
654 let mut store2 = MemoryStore::default();
655 let opts = VerifyOptions {
656 max_age_secs: 300,
657 future_skew_secs: 5,
658 nonce_mode: NonceMode::Disabled,
659 };
660 let err = verify_proof(
661 &mut store2,
662 &jws_stale,
663 "https://ex.com/a",
664 "GET",
665 None,
666 opts,
667 )
668 .await
669 .unwrap_err();
670 matches!(err, DpopError::Stale);
671 }
672
673 #[tokio::test]
674 async fn replay_same_jti_is_rejected() {
675 let (sk, x, y) = gen_es256_key();
676 let now = OffsetDateTime::now_utc().unix_timestamp();
677 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
678 let p = serde_json::json!({"jti":"jr","iat":now,"htm":"GET","htu":"https://ex.com/a"});
679 let jws = make_jws(&sk, h, p);
680
681 let mut store = MemoryStore::default();
682 let ok1 = verify_proof(
683 &mut store,
684 &jws,
685 "https://ex.com/a",
686 "GET",
687 None,
688 VerifyOptions::default(),
689 )
690 .await;
691 assert!(ok1.is_ok());
692 let err = verify_proof(
693 &mut store,
694 &jws,
695 "https://ex.com/a",
696 "GET",
697 None,
698 VerifyOptions::default(),
699 )
700 .await
701 .unwrap_err();
702 matches!(err, DpopError::Replay);
703 }
704
705 #[tokio::test]
706 async fn signature_tamper_detected() {
707 let (sk, x, y) = gen_es256_key();
708 let now = OffsetDateTime::now_utc().unix_timestamp();
709 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
710 let p = serde_json::json!({"jti":"jt","iat":now,"htm":"GET","htu":"https://ex.com/a"});
711 let mut jws = make_jws(&sk, h, p);
712
713 let bytes = unsafe { jws.as_bytes_mut() }; let mut dot_count = 0usize;
717 for i in 0..bytes.len() {
718 if bytes[i] == b'.' {
719 dot_count += 1;
720 if dot_count == 2 && i > 10 {
721 bytes[i - 5] ^= 0x01; break;
723 }
724 }
725 }
726
727 let mut store = MemoryStore::default();
728 let err = verify_proof(
729 &mut store,
730 &jws,
731 "https://ex.com/a",
732 "GET",
733 None,
734 VerifyOptions::default(),
735 )
736 .await
737 .unwrap_err();
738 matches!(err, DpopError::InvalidSignature);
739 }
740
741 #[tokio::test]
742 async fn method_mismatch_rejected() {
743 let (sk, x, y) = gen_es256_key();
744 let now = OffsetDateTime::now_utc().unix_timestamp();
745 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
746 let p = serde_json::json!({"jti":"jm","iat":now,"htm":"POST","htu":"https://ex.com/a"});
747 let jws = make_jws(&sk, h, p);
748
749 let mut store = MemoryStore::default();
750 let err = verify_proof(
751 &mut store,
752 &jws,
753 "https://ex.com/a",
754 "GET",
755 None,
756 VerifyOptions::default(),
757 )
758 .await
759 .unwrap_err();
760 matches!(err, DpopError::HtmMismatch);
761 }
762
763 #[test]
764 fn normalize_helpers_examples() {
765 assert_eq!(
767 normalize_htu("https://EX.com:443/a/./b/../c?x=1#frag").unwrap(),
768 "https://ex.com/a/c"
769 );
770 assert_eq!(normalize_method("get").unwrap(), "GET");
771 assert!(normalize_method("CUSTOM").is_err());
772 }
773
774 #[tokio::test]
775 async fn jti_too_long_rejected() {
776 let (sk, x, y) = gen_es256_key();
777 let now = OffsetDateTime::now_utc().unix_timestamp();
778 let too_long = "x".repeat(513);
779 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
780 let p = serde_json::json!({"jti":too_long,"iat":now,"htm":"GET","htu":"https://ex.com/a"});
781 let jws = make_jws(&sk, h, p);
782
783 let mut store = MemoryStore::default();
784 let err = verify_proof(
785 &mut store,
786 &jws,
787 "https://ex.com/a",
788 "GET",
789 None,
790 VerifyOptions::default(),
791 )
792 .await
793 .unwrap_err();
794 matches!(err, DpopError::JtiTooLong);
795 }
796 #[tokio::test]
799 async fn nonce_require_equal_ok() {
800 let (sk, x, y) = gen_es256_key();
801 let now = OffsetDateTime::now_utc().unix_timestamp();
802 let expected_htu = "https://ex.com/a";
803 let expected_htm = "GET";
804
805 let expected_nonce = "nonce-123";
806 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
807 let p = serde_json::json!({
808 "jti":"n-reqeq-ok",
809 "iat":now,
810 "htm":expected_htm,
811 "htu":expected_htu,
812 "nonce": expected_nonce
813 });
814 let jws = make_jws(&sk, h, p);
815
816 let mut store = MemoryStore::default();
817 let opts = VerifyOptions {
818 max_age_secs: 300,
819 future_skew_secs: 5,
820 nonce_mode: NonceMode::RequireEqual {
821 expected_nonce: expected_nonce.to_string(),
822 },
823 };
824 assert!(
825 verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
826 .await
827 .is_ok()
828 );
829 }
830
831 #[tokio::test]
832 async fn nonce_require_equal_missing_claim() {
833 let (sk, x, y) = gen_es256_key();
834 let now = OffsetDateTime::now_utc().unix_timestamp();
835 let expected_htu = "https://ex.com/a";
836 let expected_htm = "GET";
837
838 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
839 let p = serde_json::json!({
840 "jti":"n-reqeq-miss",
841 "iat":now,
842 "htm":expected_htm,
843 "htu":expected_htu
844 });
845 let jws = make_jws(&sk, h, p);
846
847 let mut store = MemoryStore::default();
848 let opts = VerifyOptions {
849 max_age_secs: 300,
850 future_skew_secs: 5,
851 nonce_mode: NonceMode::RequireEqual {
852 expected_nonce: "x".into(),
853 },
854 };
855 let err = verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
856 .await
857 .unwrap_err();
858 matches!(err, DpopError::MissingNonce);
859 }
860
861 #[tokio::test]
862 async fn nonce_require_equal_mismatch_yields_usedpopnonce() {
863 let (sk, x, y) = gen_es256_key();
864 let now = OffsetDateTime::now_utc().unix_timestamp();
865 let expected_htu = "https://ex.com/a";
866 let expected_htm = "GET";
867
868 let claim_nonce = "client-value";
869 let expected_nonce = "server-expected";
870 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
871 let p = serde_json::json!({
872 "jti":"n-reqeq-mis",
873 "iat":now,
874 "htm":expected_htm,
875 "htu":expected_htu,
876 "nonce": claim_nonce
877 });
878 let jws = make_jws(&sk, h, p);
879
880 let mut store = MemoryStore::default();
881 let opts = VerifyOptions {
882 max_age_secs: 300,
883 future_skew_secs: 5,
884 nonce_mode: NonceMode::RequireEqual {
885 expected_nonce: expected_nonce.into(),
886 },
887 };
888 let err = verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
889 .await
890 .unwrap_err();
891 if let DpopError::UseDpopNonce { nonce } = err {
893 assert_eq!(nonce, expected_nonce);
894 } else {
895 panic!("expected UseDpopNonce, got {err:?}");
896 }
897 }
898
899 #[tokio::test]
902 async fn nonce_hmac_ok_bound_all() {
903 let (sk, x, y) = gen_es256_key();
904 let now = OffsetDateTime::now_utc().unix_timestamp();
905 let expected_htu = "https://ex.com/a";
906 let expected_htm = "GET";
907
908 let jkt = thumbprint_ec_p256(&x, &y).unwrap();
910
911 let secret: Arc<[u8]> = Arc::from(&b"supersecret"[..]);
912 let ctx = crate::nonce::NonceCtx {
913 htu: Some(expected_htu),
914 htm: Some(expected_htm),
915 jkt: Some(&jkt),
916 };
917 let nonce = issue_nonce(&secret, now, &ctx);
918
919 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
920 let p = serde_json::json!({
921 "jti":"n-hmac-ok",
922 "iat":now,
923 "htm":expected_htm,
924 "htu":expected_htu,
925 "nonce": nonce
926 });
927 let jws = make_jws(&sk, h, p);
928
929 let mut store = MemoryStore::default();
930 let opts = VerifyOptions {
931 max_age_secs: 300,
932 future_skew_secs: 5,
933 nonce_mode: NonceMode::Hmac {
934 secret: secret.clone(),
935 max_age_secs: 300,
936 bind_htu_htm: true,
937 bind_jkt: true,
938 },
939 };
940 assert!(
941 verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
942 .await
943 .is_ok()
944 );
945 }
946
947 #[tokio::test]
948 async fn nonce_hmac_missing_claim_prompts_use_dpop_nonce() {
949 let (sk, x, y) = gen_es256_key();
950 let now = OffsetDateTime::now_utc().unix_timestamp();
951 let expected_htu = "https://ex.com/a";
952 let expected_htm = "GET";
953
954 let secret: Arc<[u8]> = Arc::from(&b"supersecret"[..]);
955
956 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
957 let p = serde_json::json!({
958 "jti":"n-hmac-miss",
959 "iat":now,
960 "htm":expected_htm,
961 "htu":expected_htu
962 });
963 let jws = make_jws(&sk, h, p);
964
965 let mut store = MemoryStore::default();
966 let opts = VerifyOptions {
967 max_age_secs: 300,
968 future_skew_secs: 5,
969 nonce_mode: NonceMode::Hmac {
970 secret: secret.clone(),
971 max_age_secs: 300,
972 bind_htu_htm: true,
973 bind_jkt: true,
974 },
975 };
976 let err = verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
977 .await
978 .unwrap_err();
979 matches!(err, DpopError::UseDpopNonce { .. });
980 }
981
982 #[tokio::test]
983 async fn nonce_hmac_wrong_htu_prompts_use_dpop_nonce() {
984 let (sk, x, y) = gen_es256_key();
985 let now = OffsetDateTime::now_utc().unix_timestamp();
986 let expected_htm = "GET";
987 let expected_htu = "https://ex.com/correct";
988
989 let jkt = thumbprint_ec_p256(&x, &y).unwrap();
991 let secret: Arc<[u8]> = Arc::from(&b"k"[..]);
992 let ctx_wrong = crate::nonce::NonceCtx {
993 htu: Some("https://ex.com/wrong"),
994 htm: Some(expected_htm),
995 jkt: Some(&jkt),
996 };
997 let nonce = issue_nonce(&secret, now, &ctx_wrong);
998
999 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1000 let p = serde_json::json!({
1001 "jti":"n-hmac-htu-mis",
1002 "iat":now,
1003 "htm":expected_htm,
1004 "htu":expected_htu,
1005 "nonce": nonce
1006 });
1007 let jws = make_jws(&sk, h, p);
1008
1009 let mut store = MemoryStore::default();
1010 let opts = VerifyOptions {
1011 max_age_secs: 300,
1012 future_skew_secs: 5,
1013 nonce_mode: NonceMode::Hmac {
1014 secret: secret.clone(),
1015 max_age_secs: 300,
1016 bind_htu_htm: true,
1017 bind_jkt: true,
1018 },
1019 };
1020 let err = verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1021 .await
1022 .unwrap_err();
1023 matches!(err, DpopError::UseDpopNonce { .. });
1024 }
1025
1026 #[tokio::test]
1027 async fn nonce_hmac_wrong_jkt_prompts_use_dpop_nonce() {
1028 let (_sk_a, x_a, y_a) = gen_es256_key();
1030 let (sk_b, x_b, y_b) = gen_es256_key();
1031 let now = OffsetDateTime::now_utc().unix_timestamp();
1032 let expected_htu = "https://ex.com/a";
1033 let expected_htm = "GET";
1034
1035 let jkt_a = thumbprint_ec_p256(&x_a, &y_a).unwrap();
1036 let secret: Arc<[u8]> = Arc::from(&b"secret-2"[..]);
1037 let ctx = crate::nonce::NonceCtx {
1038 htu: Some(expected_htu),
1039 htm: Some(expected_htm),
1040 jkt: Some(&jkt_a), };
1042 let nonce = issue_nonce(&secret, now, &ctx);
1043
1044 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x_b,"y":y_b}});
1046 let p = serde_json::json!({
1047 "jti":"n-hmac-jkt-mis",
1048 "iat":now,
1049 "htm":expected_htm,
1050 "htu":expected_htu,
1051 "nonce": nonce
1052 });
1053 let jws = make_jws(&sk_b, h, p);
1054
1055 let mut store = MemoryStore::default();
1056 let opts = VerifyOptions {
1057 max_age_secs: 300,
1058 future_skew_secs: 5,
1059 nonce_mode: NonceMode::Hmac {
1060 secret: secret.clone(),
1061 max_age_secs: 300,
1062 bind_htu_htm: true,
1063 bind_jkt: true,
1064 },
1065 };
1066 let err = verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1067 .await
1068 .unwrap_err();
1069 matches!(err, DpopError::UseDpopNonce { .. });
1070 }
1071
1072 #[tokio::test]
1073 async fn nonce_hmac_stale_prompts_use_dpop_nonce() {
1074 let (sk, x, y) = gen_es256_key();
1075 let now = OffsetDateTime::now_utc().unix_timestamp();
1076 let expected_htu = "https://ex.com/a";
1077 let expected_htm = "GET";
1078
1079 let jkt = thumbprint_ec_p256(&x, &y).unwrap();
1080 let secret: Arc<[u8]> = Arc::from(&b"secret-3"[..]);
1081 let issued_ts = now - 400;
1083 let nonce = issue_nonce(
1084 &secret,
1085 issued_ts,
1086 &crate::nonce::NonceCtx {
1087 htu: Some(expected_htu),
1088 htm: Some(expected_htm),
1089 jkt: Some(&jkt),
1090 },
1091 );
1092
1093 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1094 let p = serde_json::json!({
1095 "jti":"n-hmac-stale",
1096 "iat":now,
1097 "htm":expected_htm,
1098 "htu":expected_htu,
1099 "nonce": nonce
1100 });
1101 let jws = make_jws(&sk, h, p);
1102
1103 let mut store = MemoryStore::default();
1104 let opts = VerifyOptions {
1105 max_age_secs: 300,
1106 future_skew_secs: 5,
1107 nonce_mode: NonceMode::Hmac {
1108 secret: secret.clone(),
1109 max_age_secs: 300,
1110 bind_htu_htm: true,
1111 bind_jkt: true,
1112 },
1113 };
1114 let err = verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1115 .await
1116 .unwrap_err();
1117 matches!(err, DpopError::UseDpopNonce { .. });
1118 }
1119
1120 #[tokio::test]
1121 async fn nonce_hmac_future_skew_prompts_use_dpop_nonce() {
1122 let (sk, x, y) = gen_es256_key();
1123 let now = OffsetDateTime::now_utc().unix_timestamp();
1124 let expected_htu = "https://ex.com/a";
1125 let expected_htm = "GET";
1126
1127 let jkt = thumbprint_ec_p256(&x, &y).unwrap();
1128 let secret: Arc<[u8]> = Arc::from(&b"secret-4"[..]);
1129 let issued_ts = now + 10;
1131 let nonce = issue_nonce(
1132 &secret,
1133 issued_ts,
1134 &crate::nonce::NonceCtx {
1135 htu: Some(expected_htu),
1136 htm: Some(expected_htm),
1137 jkt: Some(&jkt),
1138 },
1139 );
1140
1141 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1142 let p = serde_json::json!({
1143 "jti":"n-hmac-future",
1144 "iat":now,
1145 "htm":expected_htm,
1146 "htu":expected_htu,
1147 "nonce": nonce
1148 });
1149 let jws = make_jws(&sk, h, p);
1150
1151 let mut store = MemoryStore::default();
1152 let opts = VerifyOptions {
1153 max_age_secs: 300,
1154 future_skew_secs: 5,
1155 nonce_mode: NonceMode::Hmac {
1156 secret: secret.clone(),
1157 max_age_secs: 300,
1158 bind_htu_htm: true,
1159 bind_jkt: true,
1160 },
1161 };
1162 let err = verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1163 .await
1164 .unwrap_err();
1165 matches!(err, DpopError::UseDpopNonce { .. });
1166 }
1167}