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, VerifyingKey};
13
14const ECDSA_P256_SIGNATURE_LENGTH: usize = 64;
16#[cfg(feature = "eddsa")]
17const ED25519_SIGNATURE_LENGTH: usize = 64;
18const JTI_HASH_LENGTH: usize = 32;
19const JTI_MAX_LENGTH: usize = 512;
20
21#[derive(Deserialize)]
22struct DpopHeader {
23 typ: String,
24 alg: String,
25 jwk: Jwk,
26}
27
28#[derive(Deserialize)]
29#[serde(untagged)]
30enum Jwk {
31 EcP256 {
32 kty: String,
33 crv: String,
34 x: String,
35 y: String,
36 },
37 #[cfg(feature = "eddsa")]
38 OkpEd25519 { kty: String, crv: String, x: String },
39}
40
41#[derive(Clone, Debug)]
42pub enum NonceMode {
43 Disabled,
44 RequireEqual {
46 expected_nonce: String, },
48 Hmac {
50 secret: secrecy::SecretBox<[u8]>,
51 max_age_seconds: i64,
52 bind_htu_htm: bool,
53 bind_jkt: bool,
54 bind_client: bool,
55 },
56}
57
58#[derive(Debug, Clone)]
59pub struct VerifyOptions {
60 pub max_age_seconds: i64,
61 pub future_skew_seconds: i64,
62 pub nonce_mode: NonceMode,
63 pub client_binding: Option<ClientBinding>,
64}
65impl Default for VerifyOptions {
66 fn default() -> Self {
67 Self {
68 max_age_seconds: 300,
69 future_skew_seconds: 5,
70 nonce_mode: NonceMode::Disabled,
71 client_binding: None,
72 }
73 }
74}
75
76#[derive(Debug, Clone)]
77pub struct ClientBinding {
78 pub client_id: String,
79}
80
81impl ClientBinding {
82 pub fn new(client_id: impl Into<String>) -> Self {
83 Self {
84 client_id: client_id.into(),
85 }
86 }
87}
88
89#[derive(Debug)]
90pub struct VerifiedDpop {
91 pub jkt: String,
92 pub jti: String,
93 pub iat: i64,
94}
95
96struct JtiHash([u8; JTI_HASH_LENGTH]);
98
99impl JtiHash {
100 fn from_jti(jti: &str) -> Self {
102 let mut hasher = Sha256::new();
103 hasher.update(jti.as_bytes());
104 let digest = hasher.finalize();
105 let mut hash = [0u8; JTI_HASH_LENGTH];
106 hash.copy_from_slice(&digest[..JTI_HASH_LENGTH]);
107 JtiHash(hash)
108 }
109
110 fn as_array(&self) -> [u8; JTI_HASH_LENGTH] {
112 self.0
113 }
114}
115
116struct DpopToken {
118 header: DpopHeader,
119 payload_b64: String,
120 signature_bytes: Vec<u8>,
121 signing_input: String,
122}
123
124#[derive(Deserialize)]
126struct DpopClaims {
127 jti: String,
128 iat: i64,
129 htm: String,
130 htu: String,
131 #[serde(default)]
132 ath: Option<String>,
133 #[serde(default)]
134 nonce: Option<String>,
135}
136
137pub struct DpopVerifier {
139 options: VerifyOptions,
140}
141
142impl DpopVerifier {
143 pub fn new() -> Self {
145 Self {
146 options: VerifyOptions::default(),
147 }
148 }
149
150 pub fn with_max_age_seconds(mut self, max_age_seconds: i64) -> Self {
152 self.options.max_age_seconds = max_age_seconds;
153 self
154 }
155
156 pub fn with_future_skew_seconds(mut self, future_skew_seconds: i64) -> Self {
158 self.options.future_skew_seconds = future_skew_seconds;
159 self
160 }
161
162 pub fn with_nonce_mode(mut self, nonce_mode: NonceMode) -> Self {
164 self.options.nonce_mode = nonce_mode;
165 self
166 }
167
168 pub fn with_client_binding(mut self, client_id: impl Into<String>) -> Self {
170 self.options.client_binding = Some(ClientBinding {
171 client_id: client_id.into(),
172 });
173 self
174 }
175
176 pub fn without_client_binding(mut self) -> Self {
178 self.options.client_binding = None;
179 self
180 }
181
182 pub async fn verify<S: ReplayStore + ?Sized>(
184 &self,
185 store: &mut S,
186 dpop_compact_jws: &str,
187 expected_htu: &str,
188 expected_htm: &str,
189 access_token: Option<&str>,
190 ) -> Result<VerifiedDpop, DpopError> {
191 let token = self.parse_token(dpop_compact_jws)?;
193
194 self.validate_header(&token.header)?;
196
197 let jkt = self.verify_signature_and_compute_jkt(&token)?;
199
200 let claims: DpopClaims = {
202 let bytes = B64
203 .decode(&token.payload_b64)
204 .map_err(|_| DpopError::MalformedJws)?;
205 serde_json::from_slice(&bytes).map_err(|_| DpopError::MalformedJws)?
206 };
207
208 if claims.jti.len() > JTI_MAX_LENGTH {
210 return Err(DpopError::JtiTooLong);
211 }
212
213 let (expected_htm_normalized, expected_htu_normalized) =
215 self.validate_http_binding(&claims, expected_htm, expected_htu)?;
216
217 if let Some(token) = access_token {
219 self.validate_access_token_binding(&claims, token)?;
220 }
221
222 self.check_timestamp_freshness(claims.iat)?;
224
225 let client_binding = self
226 .options
227 .client_binding
228 .as_ref()
229 .map(|binding| binding.client_id.as_str());
230
231 self.validate_nonce_if_required(
233 &claims,
234 &expected_htu_normalized,
235 &expected_htm_normalized,
236 &jkt,
237 client_binding,
238 )?;
239
240 let jti_hash = JtiHash::from_jti(&claims.jti);
242 self.prevent_replay(store, jti_hash, &claims, &jkt, client_binding)
243 .await?;
244
245 Ok(VerifiedDpop {
246 jkt,
247 jti: claims.jti,
248 iat: claims.iat,
249 })
250 }
251
252 fn parse_token(&self, dpop_compact_jws: &str) -> Result<DpopToken, DpopError> {
254 let mut jws_parts = dpop_compact_jws.split('.');
255 let (header_b64, payload_b64, signature_b64) =
256 match (jws_parts.next(), jws_parts.next(), jws_parts.next()) {
257 (Some(h), Some(p), Some(s)) if jws_parts.next().is_none() => (h, p, s),
258 _ => return Err(DpopError::MalformedJws),
259 };
260
261 let header: DpopHeader = {
263 let bytes = B64
264 .decode(header_b64)
265 .map_err(|_| DpopError::MalformedJws)?;
266 let val: serde_json::Value =
267 serde_json::from_slice(&bytes).map_err(|_| DpopError::MalformedJws)?;
268 if val.get("jwk").and_then(|j| j.get("d")).is_some() {
270 return Err(DpopError::BadJwk("jwk must not include 'd'"));
271 }
272 serde_json::from_value(val).map_err(|_| DpopError::MalformedJws)?
273 };
274
275 let signing_input = format!("{}.{}", header_b64, payload_b64);
276 let signature_bytes = B64
277 .decode(signature_b64)
278 .map_err(|_| DpopError::InvalidSignature)?;
279
280 Ok(DpopToken {
281 header,
282 payload_b64: payload_b64.to_string(),
283 signature_bytes,
284 signing_input,
285 })
286 }
287
288 fn validate_header(&self, header: &DpopHeader) -> Result<(), DpopError> {
290 if header.typ != "dpop+jwt" {
291 return Err(DpopError::MalformedJws);
292 }
293 Ok(())
294 }
295
296 fn verify_signature_and_compute_jkt(&self, token: &DpopToken) -> Result<String, DpopError> {
298 let jkt = match (token.header.alg.as_str(), &token.header.jwk) {
299 ("ES256", Jwk::EcP256 { kty, crv, x, y }) if kty == "EC" && crv == "P-256" => {
300 if token.signature_bytes.len() != ECDSA_P256_SIGNATURE_LENGTH {
301 return Err(DpopError::InvalidSignature);
302 }
303
304 let verifying_key: VerifyingKey = verifying_key_from_p256_xy(x, y)?;
305 let signature = p256::ecdsa::Signature::from_slice(&token.signature_bytes)
306 .map_err(|_| DpopError::InvalidSignature)?;
307 verifying_key
308 .verify(token.signing_input.as_bytes(), &signature)
309 .map_err(|_| DpopError::InvalidSignature)?;
310 thumbprint_ec_p256(x, y)?
312 }
313
314 #[cfg(feature = "eddsa")]
315 ("EdDSA", Jwk::OkpEd25519 { kty, crv, x }) if kty == "OKP" && crv == "Ed25519" => {
316 use ed25519_dalek::{Signature as EdSig, VerifyingKey as EdVk};
317 use signature::Verifier as _;
318
319 if token.signature_bytes.len() != ED25519_SIGNATURE_LENGTH {
320 return Err(DpopError::InvalidSignature);
321 }
322
323 let verifying_key: EdVk = crate::jwk::verifying_key_from_okp_ed25519(x)?;
324 let signature = EdSig::from_slice(&token.signature_bytes)
325 .map_err(|_| DpopError::InvalidSignature)?;
326 verifying_key
327 .verify(token.signing_input.as_bytes(), &signature)
328 .map_err(|_| DpopError::InvalidSignature)?;
329 crate::jwk::thumbprint_okp_ed25519(x)?
330 }
331
332 ("EdDSA", _) => return Err(DpopError::BadJwk("expect OKP/Ed25519 for EdDSA")),
333 ("ES256", _) => return Err(DpopError::BadJwk("expect EC/P-256 for ES256")),
334 ("none", _) => return Err(DpopError::InvalidAlg("none".into())),
335 (a, _) if a.starts_with("HS") => return Err(DpopError::InvalidAlg(a.into())),
336 (other, _) => return Err(DpopError::UnsupportedAlg(other.into())),
337 };
338
339 Ok(jkt)
340 }
341
342 fn validate_http_binding(
344 &self,
345 claims: &DpopClaims,
346 expected_htm: &str,
347 expected_htu: &str,
348 ) -> Result<(String, String), DpopError> {
349 let expected_htm_normalized = normalize_method(expected_htm)?;
351 let actual_htm_normalized = normalize_method(&claims.htm)?;
352 if actual_htm_normalized != expected_htm_normalized {
353 return Err(DpopError::HtmMismatch);
354 }
355
356 let expected_htu_normalized = normalize_htu(expected_htu)?;
357 let actual_htu_normalized = normalize_htu(&claims.htu)?;
358 if actual_htu_normalized != expected_htu_normalized {
359 return Err(DpopError::HtuMismatch);
360 }
361
362 Ok((expected_htm_normalized, expected_htu_normalized))
363 }
364
365 fn validate_access_token_binding(
367 &self,
368 claims: &DpopClaims,
369 access_token: &str,
370 ) -> Result<(), DpopError> {
371 let expected_hash = Sha256::digest(access_token.as_bytes());
373
374 let ath_b64 = claims.ath.as_ref().ok_or(DpopError::MissingAth)?;
376 let actual_hash = B64
377 .decode(ath_b64.as_bytes())
378 .map_err(|_| DpopError::AthMalformed)?;
379
380 if actual_hash.len() != expected_hash.len()
382 || !bool::from(actual_hash.ct_eq(&expected_hash[..]))
383 {
384 return Err(DpopError::AthMismatch);
385 }
386
387 Ok(())
388 }
389
390 fn check_timestamp_freshness(&self, iat: i64) -> Result<(), DpopError> {
392 let current_time = OffsetDateTime::now_utc().unix_timestamp();
393 if iat > current_time + self.options.future_skew_seconds {
394 return Err(DpopError::FutureSkew);
395 }
396 if current_time - iat > self.options.max_age_seconds {
397 return Err(DpopError::Stale);
398 }
399 Ok(())
400 }
401
402 fn validate_nonce_if_required(
404 &self,
405 claims: &DpopClaims,
406 expected_htu_normalized: &str,
407 expected_htm_normalized: &str,
408 jkt: &str,
409 client_binding: Option<&str>,
410 ) -> Result<(), DpopError> {
411 match &self.options.nonce_mode {
412 NonceMode::Disabled => { }
413 NonceMode::RequireEqual { expected_nonce } => {
414 let nonce_value = claims.nonce.as_ref().ok_or(DpopError::MissingNonce)?;
415 if nonce_value != expected_nonce {
416 let fresh_nonce = expected_nonce.to_string();
417 return Err(DpopError::UseDpopNonce { nonce: fresh_nonce });
418 }
419 }
420 NonceMode::Hmac {
421 secret,
422 max_age_seconds,
423 bind_htu_htm,
424 bind_jkt,
425 bind_client,
426 } => {
427 let nonce_value = match &claims.nonce {
428 Some(s) => s.as_str(),
429 None => {
430 let current_time = time::OffsetDateTime::now_utc().unix_timestamp();
432 let nonce_ctx = crate::nonce::NonceCtx {
433 htu: if *bind_htu_htm {
434 Some(expected_htu_normalized)
435 } else {
436 None
437 },
438 htm: if *bind_htu_htm {
439 Some(expected_htm_normalized)
440 } else {
441 None
442 },
443 jkt: if *bind_jkt { Some(jkt) } else { None },
444 client: if *bind_client { client_binding } else { None },
445 };
446 let fresh_nonce =
447 crate::nonce::issue_nonce(secret, current_time, &nonce_ctx)?;
448 return Err(DpopError::UseDpopNonce { nonce: fresh_nonce });
449 }
450 };
451
452 let current_time = time::OffsetDateTime::now_utc().unix_timestamp();
453 let nonce_ctx = crate::nonce::NonceCtx {
454 htu: if *bind_htu_htm {
455 Some(expected_htu_normalized)
456 } else {
457 None
458 },
459 htm: if *bind_htu_htm {
460 Some(expected_htm_normalized)
461 } else {
462 None
463 },
464 jkt: if *bind_jkt { Some(jkt) } else { None },
465 client: if *bind_client { client_binding } else { None },
466 };
467
468 if crate::nonce::verify_nonce(
469 secret,
470 nonce_value,
471 current_time,
472 *max_age_seconds,
473 &nonce_ctx,
474 )
475 .is_err()
476 {
477 let fresh_nonce = crate::nonce::issue_nonce(secret, current_time, &nonce_ctx)?;
479 return Err(DpopError::UseDpopNonce { nonce: fresh_nonce });
480 }
481 }
482 }
483 Ok(())
484 }
485
486 async fn prevent_replay<S: ReplayStore + ?Sized>(
488 &self,
489 store: &mut S,
490 jti_hash: JtiHash,
491 claims: &DpopClaims,
492 jkt: &str,
493 client_binding: Option<&str>,
494 ) -> Result<(), DpopError> {
495 let is_first_use = store
496 .insert_once(
497 jti_hash.as_array(),
498 ReplayContext {
499 jkt: Some(jkt),
500 htm: Some(&claims.htm),
501 htu: Some(&claims.htu),
502 client_id: client_binding,
503 iat: claims.iat,
504 },
505 )
506 .await?;
507
508 if !is_first_use {
509 return Err(DpopError::Replay);
510 }
511
512 Ok(())
513 }
514}
515
516impl Default for DpopVerifier {
517 fn default() -> Self {
518 Self::new()
519 }
520}
521
522#[deprecated(since = "2.0.0", note = "Use DpopVerifier instead")]
528pub async fn verify_proof<S: ReplayStore + ?Sized>(
529 store: &mut S,
530 dpop_compact_jws: &str,
531 expected_htu: &str,
532 expected_htm: &str,
533 access_token: Option<&str>,
534 opts: VerifyOptions,
535) -> Result<VerifiedDpop, DpopError> {
536 let verifier = DpopVerifier { options: opts };
537 verifier
538 .verify(
539 store,
540 dpop_compact_jws,
541 expected_htu,
542 expected_htm,
543 access_token,
544 )
545 .await
546}
547
548#[cfg(test)]
549mod tests {
550 use super::*;
551 use crate::jwk::thumbprint_ec_p256;
552 use crate::nonce::issue_nonce;
553 use p256::ecdsa::{signature::Signer, Signature, SigningKey};
554 use rand_core::OsRng;
555 use secrecy::SecretBox;
556
557 fn gen_es256_key() -> (SigningKey, String, String) {
560 let signing_key = SigningKey::random(&mut OsRng);
561 let verifying_key = VerifyingKey::from(&signing_key);
562 let encoded_point = verifying_key.to_encoded_point(false);
563 let x_coordinate = B64.encode(encoded_point.x().unwrap());
564 let y_coordinate = B64.encode(encoded_point.y().unwrap());
565 (signing_key, x_coordinate, y_coordinate)
566 }
567
568 fn make_jws(
569 signing_key: &SigningKey,
570 header_json: serde_json::Value,
571 claims_json: serde_json::Value,
572 ) -> String {
573 let header_bytes = serde_json::to_vec(&header_json).unwrap();
574 let payload_bytes = serde_json::to_vec(&claims_json).unwrap();
575 let header_b64 = B64.encode(header_bytes);
576 let payload_b64 = B64.encode(payload_bytes);
577 let signing_input = format!("{header_b64}.{payload_b64}");
578 let signature: Signature = signing_key.sign(signing_input.as_bytes());
579 let signature_b64 = B64.encode(signature.to_bytes());
580 format!("{header_b64}.{payload_b64}.{signature_b64}")
581 }
582
583 #[derive(Default)]
584 struct MemoryStore(std::collections::HashSet<[u8; 32]>);
585
586 #[async_trait::async_trait]
587 impl ReplayStore for MemoryStore {
588 async fn insert_once(
589 &mut self,
590 jti_hash: [u8; 32],
591 _ctx: ReplayContext<'_>,
592 ) -> Result<bool, DpopError> {
593 Ok(self.0.insert(jti_hash))
594 }
595 }
596 #[test]
598 fn thumbprint_has_expected_length_and_no_padding() {
599 let x = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
601 let y = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
602 let t1 = thumbprint_ec_p256(x, y).expect("thumbprint");
603 let t2 = thumbprint_ec_p256(x, y).expect("thumbprint");
604 assert_eq!(t1, t2);
606 assert_eq!(t1.len(), 43);
607 assert!(!t1.contains('='));
608 }
609
610 #[test]
611 fn decoding_key_rejects_wrong_sizes() {
612 let bad_x = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([0u8; 31]);
614 let good_y = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([0u8; 32]);
615 let res = crate::jwk::verifying_key_from_p256_xy(&bad_x, &good_y);
616 assert!(res.is_err(), "expected error for bad y");
617
618 let good_x = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([0u8; 32]);
620 let bad_y = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([0u8; 33]);
621 let res = crate::jwk::verifying_key_from_p256_xy(&good_x, &bad_y);
622 assert!(res.is_err(), "expected error for bad y");
623 }
624
625 #[tokio::test]
626 async fn replay_store_trait_basic() {
627 use async_trait::async_trait;
628 use std::collections::HashSet;
629
630 struct MemoryStore(HashSet<[u8; 32]>);
631
632 #[async_trait]
633 impl ReplayStore for MemoryStore {
634 async fn insert_once(
635 &mut self,
636 jti_hash: [u8; 32],
637 _ctx: ReplayContext<'_>,
638 ) -> Result<bool, DpopError> {
639 Ok(self.0.insert(jti_hash))
640 }
641 }
642
643 let mut s = MemoryStore(HashSet::new());
644 let first = s
645 .insert_once(
646 [42u8; 32],
647 ReplayContext {
648 jkt: Some("j"),
649 htm: Some("POST"),
650 htu: Some("https://ex"),
651 client_id: None,
652 iat: 0,
653 },
654 )
655 .await
656 .unwrap();
657 let second = s
658 .insert_once(
659 [42u8; 32],
660 ReplayContext {
661 jkt: Some("j"),
662 htm: Some("POST"),
663 htu: Some("https://ex"),
664 client_id: None,
665 iat: 0,
666 },
667 )
668 .await
669 .unwrap();
670 assert!(first);
671 assert!(!second); }
673 #[tokio::test]
674 async fn verify_valid_es256_proof() {
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":"j1","iat":now,"htm":"GET","htu":"https://api.example.com/resource"});
679 let jws = make_jws(&sk, h, p);
680
681 let mut store = MemoryStore::default();
682 let res = verify_proof(
683 &mut store,
684 &jws,
685 "https://api.example.com/resource",
686 "GET",
687 None,
688 VerifyOptions::default(),
689 )
690 .await;
691 assert!(res.is_ok(), "{res:?}");
692 }
693
694 #[tokio::test]
695 async fn method_normalization_allows_lowercase_claim() {
696 let (sk, x, y) = gen_es256_key();
697 let now = OffsetDateTime::now_utc().unix_timestamp();
698 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
699 let p = serde_json::json!({"jti":"j2","iat":now,"htm":"get","htu":"https://ex.com/a"});
700 let jws = make_jws(&sk, h, p);
701
702 let mut store = MemoryStore::default();
703 assert!(verify_proof(
704 &mut store,
705 &jws,
706 "https://ex.com/a",
707 "GET",
708 None,
709 VerifyOptions::default()
710 )
711 .await
712 .is_ok());
713 }
714
715 #[tokio::test]
716 async fn htu_normalizes_dot_segments_and_default_ports_and_strips_qf() {
717 let (sk, x, y) = gen_es256_key();
718 let now = OffsetDateTime::now_utc().unix_timestamp();
719 let claim_htu = "https://EX.COM:443/a/../b?q=1#frag";
721 let expect_htu = "https://ex.com/b";
722 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
723 let p = serde_json::json!({"jti":"j3","iat":now,"htm":"GET","htu":claim_htu});
724 let jws = make_jws(&sk, h, p);
725
726 let mut store = MemoryStore::default();
727 assert!(verify_proof(
728 &mut store,
729 &jws,
730 expect_htu,
731 "GET",
732 None,
733 VerifyOptions::default()
734 )
735 .await
736 .is_ok());
737 }
738
739 #[tokio::test]
740 async fn htu_path_case_mismatch_fails() {
741 let (sk, x, y) = gen_es256_key();
742 let now = OffsetDateTime::now_utc().unix_timestamp();
743 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
744 let p = serde_json::json!({"jti":"j4","iat":now,"htm":"GET","htu":"https://ex.com/API"});
745 let jws = make_jws(&sk, h, p);
746
747 let mut store = MemoryStore::default();
748 let err = verify_proof(
749 &mut store,
750 &jws,
751 "https://ex.com/api",
752 "GET",
753 None,
754 VerifyOptions::default(),
755 )
756 .await
757 .unwrap_err();
758 matches!(err, DpopError::HtuMismatch);
759 }
760
761 #[tokio::test]
762 async fn alg_none_rejected() {
763 let (sk, x, y) = gen_es256_key();
764 let now = OffsetDateTime::now_utc().unix_timestamp();
765 let h = serde_json::json!({"typ":"dpop+jwt","alg":"none","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
767 let p = serde_json::json!({"jti":"j5","iat":now,"htm":"GET","htu":"https://ex.com/a"});
768 let jws = make_jws(&sk, h, p);
769
770 let mut store = MemoryStore::default();
771 let err = verify_proof(
772 &mut store,
773 &jws,
774 "https://ex.com/a",
775 "GET",
776 None,
777 VerifyOptions::default(),
778 )
779 .await
780 .unwrap_err();
781 matches!(err, DpopError::InvalidAlg(_));
782 }
783
784 #[tokio::test]
785 async fn alg_hs256_rejected() {
786 let (sk, x, y) = gen_es256_key();
787 let now = OffsetDateTime::now_utc().unix_timestamp();
788 let h = serde_json::json!({"typ":"dpop+jwt","alg":"HS256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
789 let p = serde_json::json!({"jti":"j6","iat":now,"htm":"GET","htu":"https://ex.com/a"});
790 let jws = make_jws(&sk, h, p);
791
792 let mut store = MemoryStore::default();
793 let err = verify_proof(
794 &mut store,
795 &jws,
796 "https://ex.com/a",
797 "GET",
798 None,
799 VerifyOptions::default(),
800 )
801 .await
802 .unwrap_err();
803 matches!(err, DpopError::InvalidAlg(_));
804 }
805
806 #[tokio::test]
807 async fn jwk_with_private_d_rejected() {
808 let (sk, x, y) = gen_es256_key();
809 let now = OffsetDateTime::now_utc().unix_timestamp();
810 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y,"d":"AAAA"}});
812 let p = serde_json::json!({"jti":"j7","iat":now,"htm":"GET","htu":"https://ex.com/a"});
813 let jws = make_jws(&sk, h, p);
814
815 let mut store = MemoryStore::default();
816 let err = verify_proof(
817 &mut store,
818 &jws,
819 "https://ex.com/a",
820 "GET",
821 None,
822 VerifyOptions::default(),
823 )
824 .await
825 .unwrap_err();
826 matches!(err, DpopError::BadJwk(_));
827 }
828
829 #[tokio::test]
830 async fn ath_binding_ok_and_mismatch_and_padded_rejected() {
831 let (sk, x, y) = gen_es256_key();
832 let now = OffsetDateTime::now_utc().unix_timestamp();
833 let at = "access.token.string";
834 let ath = B64.encode(Sha256::digest(at.as_bytes()));
835 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
836
837 let p_ok = serde_json::json!({"jti":"j8","iat":now,"htm":"GET","htu":"https://ex.com/a","ath":ath});
839 let jws_ok = make_jws(&sk, h.clone(), p_ok);
840 let mut store = MemoryStore::default();
841 assert!(verify_proof(
842 &mut store,
843 &jws_ok,
844 "https://ex.com/a",
845 "GET",
846 Some(at),
847 VerifyOptions::default()
848 )
849 .await
850 .is_ok());
851
852 let p_bad = serde_json::json!({"jti":"j9","iat":now,"htm":"GET","htu":"https://ex.com/a","ath":ath});
854 let jws_bad = make_jws(&sk, h.clone(), p_bad);
855 let mut store2 = MemoryStore::default();
856 let err = verify_proof(
857 &mut store2,
858 &jws_bad,
859 "https://ex.com/a",
860 "GET",
861 Some("different.token"),
862 VerifyOptions::default(),
863 )
864 .await
865 .unwrap_err();
866 matches!(err, DpopError::AthMismatch);
867
868 let ath_padded = format!("{ath}==");
870 let p_pad = serde_json::json!({"jti":"j10","iat":now,"htm":"GET","htu":"https://ex.com/a","ath":ath_padded});
871 let jws_pad = make_jws(&sk, h.clone(), p_pad);
872 let mut store3 = MemoryStore::default();
873 let err = verify_proof(
874 &mut store3,
875 &jws_pad,
876 "https://ex.com/a",
877 "GET",
878 Some(at),
879 VerifyOptions::default(),
880 )
881 .await
882 .unwrap_err();
883 matches!(err, DpopError::AthMalformed);
884 }
885
886 #[tokio::test]
887 async fn freshness_future_skew_and_stale() {
888 let (sk, x, y) = gen_es256_key();
889 let now = OffsetDateTime::now_utc().unix_timestamp();
890 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
891
892 let future_skew_seconds = 5;
894 let p_future = serde_json::json!({
895 "jti":"jf",
896 "iat":now + future_skew_seconds + 5,
897 "htm":"GET",
898 "htu":"https://ex.com/a"
899 });
900 let jws_future = make_jws(&sk, h.clone(), p_future);
901 let mut store1 = MemoryStore::default();
902 let opts = VerifyOptions {
903 max_age_seconds: 300,
904 future_skew_seconds,
905 nonce_mode: NonceMode::Disabled,
906 client_binding: None,
907 };
908 let err = verify_proof(
909 &mut store1,
910 &jws_future,
911 "https://ex.com/a",
912 "GET",
913 None,
914 opts,
915 )
916 .await
917 .unwrap_err();
918 matches!(err, DpopError::FutureSkew);
919
920 let p_stale =
922 serde_json::json!({"jti":"js","iat":now - 301,"htm":"GET","htu":"https://ex.com/a"});
923 let jws_stale = make_jws(&sk, h.clone(), p_stale);
924 let mut store2 = MemoryStore::default();
925 let opts = VerifyOptions {
926 max_age_seconds: 300,
927 future_skew_seconds,
928 nonce_mode: NonceMode::Disabled,
929 client_binding: None,
930 };
931 let err = verify_proof(
932 &mut store2,
933 &jws_stale,
934 "https://ex.com/a",
935 "GET",
936 None,
937 opts,
938 )
939 .await
940 .unwrap_err();
941 matches!(err, DpopError::Stale);
942 }
943
944 #[tokio::test]
945 async fn replay_same_jti_is_rejected() {
946 let (sk, x, y) = gen_es256_key();
947 let now = OffsetDateTime::now_utc().unix_timestamp();
948 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
949 let p = serde_json::json!({"jti":"jr","iat":now,"htm":"GET","htu":"https://ex.com/a"});
950 let jws = make_jws(&sk, h, p);
951
952 let mut store = MemoryStore::default();
953 let ok1 = verify_proof(
954 &mut store,
955 &jws,
956 "https://ex.com/a",
957 "GET",
958 None,
959 VerifyOptions::default(),
960 )
961 .await;
962 assert!(ok1.is_ok());
963 let err = verify_proof(
964 &mut store,
965 &jws,
966 "https://ex.com/a",
967 "GET",
968 None,
969 VerifyOptions::default(),
970 )
971 .await
972 .unwrap_err();
973 matches!(err, DpopError::Replay);
974 }
975
976 #[tokio::test]
977 async fn signature_tamper_detected() {
978 let (sk, x, y) = gen_es256_key();
979 let now = OffsetDateTime::now_utc().unix_timestamp();
980 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
981 let p = serde_json::json!({"jti":"jt","iat":now,"htm":"GET","htu":"https://ex.com/a"});
982 let mut jws = make_jws(&sk, h, p);
983
984 let bytes = unsafe { jws.as_bytes_mut() }; let mut dot_count = 0usize;
988 for i in 0..bytes.len() {
989 if bytes[i] == b'.' {
990 dot_count += 1;
991 if dot_count == 2 && i > 10 {
992 bytes[i - 5] ^= 0x01; break;
994 }
995 }
996 }
997
998 let mut store = MemoryStore::default();
999 let err = verify_proof(
1000 &mut store,
1001 &jws,
1002 "https://ex.com/a",
1003 "GET",
1004 None,
1005 VerifyOptions::default(),
1006 )
1007 .await
1008 .unwrap_err();
1009 matches!(err, DpopError::InvalidSignature);
1010 }
1011
1012 #[tokio::test]
1013 async fn method_mismatch_rejected() {
1014 let (sk, x, y) = gen_es256_key();
1015 let now = OffsetDateTime::now_utc().unix_timestamp();
1016 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1017 let p = serde_json::json!({"jti":"jm","iat":now,"htm":"POST","htu":"https://ex.com/a"});
1018 let jws = make_jws(&sk, h, p);
1019
1020 let mut store = MemoryStore::default();
1021 let err = verify_proof(
1022 &mut store,
1023 &jws,
1024 "https://ex.com/a",
1025 "GET",
1026 None,
1027 VerifyOptions::default(),
1028 )
1029 .await
1030 .unwrap_err();
1031 matches!(err, DpopError::HtmMismatch);
1032 }
1033
1034 #[test]
1035 fn normalize_helpers_examples() {
1036 assert_eq!(
1038 normalize_htu("https://EX.com:443/a/./b/../c?x=1#frag").unwrap(),
1039 "https://ex.com/a/c"
1040 );
1041 assert_eq!(normalize_method("get").unwrap(), "GET");
1042 assert!(normalize_method("CUSTOM").is_err());
1043 }
1044
1045 #[tokio::test]
1046 async fn jti_too_long_rejected() {
1047 let (sk, x, y) = gen_es256_key();
1048 let now = OffsetDateTime::now_utc().unix_timestamp();
1049 let too_long = "x".repeat(513);
1050 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1051 let p = serde_json::json!({"jti":too_long,"iat":now,"htm":"GET","htu":"https://ex.com/a"});
1052 let jws = make_jws(&sk, h, p);
1053
1054 let mut store = MemoryStore::default();
1055 let err = verify_proof(
1056 &mut store,
1057 &jws,
1058 "https://ex.com/a",
1059 "GET",
1060 None,
1061 VerifyOptions::default(),
1062 )
1063 .await
1064 .unwrap_err();
1065 matches!(err, DpopError::JtiTooLong);
1066 }
1067 #[tokio::test]
1070 async fn nonce_require_equal_ok() {
1071 let (sk, x, y) = gen_es256_key();
1072 let now = OffsetDateTime::now_utc().unix_timestamp();
1073 let expected_htu = "https://ex.com/a";
1074 let expected_htm = "GET";
1075
1076 let expected_nonce = "nonce-123";
1077 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1078 let p = serde_json::json!({
1079 "jti":"n-reqeq-ok",
1080 "iat":now,
1081 "htm":expected_htm,
1082 "htu":expected_htu,
1083 "nonce": expected_nonce
1084 });
1085 let jws = make_jws(&sk, h, p);
1086
1087 let mut store = MemoryStore::default();
1088 let opts = VerifyOptions {
1089 max_age_seconds: 300,
1090 future_skew_seconds: 5,
1091 nonce_mode: NonceMode::RequireEqual {
1092 expected_nonce: expected_nonce.to_string(),
1093 },
1094 client_binding: None,
1095 };
1096 assert!(
1097 verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1098 .await
1099 .is_ok()
1100 );
1101 }
1102
1103 #[tokio::test]
1104 async fn nonce_require_equal_missing_claim() {
1105 let (sk, x, y) = gen_es256_key();
1106 let now = OffsetDateTime::now_utc().unix_timestamp();
1107 let expected_htu = "https://ex.com/a";
1108 let expected_htm = "GET";
1109
1110 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1111 let p = serde_json::json!({
1112 "jti":"n-reqeq-miss",
1113 "iat":now,
1114 "htm":expected_htm,
1115 "htu":expected_htu
1116 });
1117 let jws = make_jws(&sk, h, p);
1118
1119 let mut store = MemoryStore::default();
1120 let opts = VerifyOptions {
1121 max_age_seconds: 300,
1122 future_skew_seconds: 5,
1123 nonce_mode: NonceMode::RequireEqual {
1124 expected_nonce: "x".into(),
1125 },
1126 client_binding: None,
1127 };
1128 let err = verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1129 .await
1130 .unwrap_err();
1131 matches!(err, DpopError::MissingNonce);
1132 }
1133
1134 #[tokio::test]
1135 async fn nonce_require_equal_mismatch_yields_usedpopnonce() {
1136 let (sk, x, y) = gen_es256_key();
1137 let now = OffsetDateTime::now_utc().unix_timestamp();
1138 let expected_htu = "https://ex.com/a";
1139 let expected_htm = "GET";
1140
1141 let claim_nonce = "client-value";
1142 let expected_nonce = "server-expected";
1143 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1144 let p = serde_json::json!({
1145 "jti":"n-reqeq-mis",
1146 "iat":now,
1147 "htm":expected_htm,
1148 "htu":expected_htu,
1149 "nonce": claim_nonce
1150 });
1151 let jws = make_jws(&sk, h, p);
1152
1153 let mut store = MemoryStore::default();
1154 let opts = VerifyOptions {
1155 max_age_seconds: 300,
1156 future_skew_seconds: 5,
1157 nonce_mode: NonceMode::RequireEqual {
1158 expected_nonce: expected_nonce.into(),
1159 },
1160 client_binding: None,
1161 };
1162 let err = verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1163 .await
1164 .unwrap_err();
1165 if let DpopError::UseDpopNonce { nonce } = err {
1167 assert_eq!(nonce, expected_nonce);
1168 } else {
1169 panic!("expected UseDpopNonce, got {err:?}");
1170 }
1171 }
1172
1173 #[tokio::test]
1176 async fn nonce_hmac_ok_bound_all() {
1177 let (sk, x, y) = gen_es256_key();
1178 let now = OffsetDateTime::now_utc().unix_timestamp();
1179 let expected_htu = "https://ex.com/a";
1180 let expected_htm = "GET";
1181
1182 let jkt = thumbprint_ec_p256(&x, &y).unwrap();
1184
1185 let secret = SecretBox::from(b"supersecret".to_vec());
1186 let ctx = crate::nonce::NonceCtx {
1187 htu: Some(expected_htu),
1188 htm: Some(expected_htm),
1189 jkt: Some(&jkt),
1190 client: None,
1191 };
1192 let nonce = issue_nonce(&secret, now, &ctx).expect("issue_nonce");
1193
1194 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1195 let p = serde_json::json!({
1196 "jti":"n-hmac-ok",
1197 "iat":now,
1198 "htm":expected_htm,
1199 "htu":expected_htu,
1200 "nonce": nonce
1201 });
1202 let jws = make_jws(&sk, h, p);
1203
1204 let mut store = MemoryStore::default();
1205 let opts = VerifyOptions {
1206 max_age_seconds: 300,
1207 future_skew_seconds: 5,
1208 nonce_mode: NonceMode::Hmac {
1209 secret: secret.clone(),
1210 max_age_seconds: 300,
1211 bind_htu_htm: true,
1212 bind_jkt: true,
1213 bind_client: false,
1214 },
1215 client_binding: None,
1216 };
1217 assert!(
1218 verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1219 .await
1220 .is_ok()
1221 );
1222 }
1223
1224 #[tokio::test]
1225 async fn nonce_hmac_missing_claim_prompts_use_dpop_nonce() {
1226 let (sk, x, y) = gen_es256_key();
1227 let now = OffsetDateTime::now_utc().unix_timestamp();
1228 let expected_htu = "https://ex.com/a";
1229 let expected_htm = "GET";
1230
1231 let secret = SecretBox::from(b"supersecret".to_vec());
1232
1233 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1234 let p = serde_json::json!({
1235 "jti":"n-hmac-miss",
1236 "iat":now,
1237 "htm":expected_htm,
1238 "htu":expected_htu
1239 });
1240 let jws = make_jws(&sk, h, p);
1241
1242 let mut store = MemoryStore::default();
1243 let opts = VerifyOptions {
1244 max_age_seconds: 300,
1245 future_skew_seconds: 5,
1246 nonce_mode: NonceMode::Hmac {
1247 secret: secret.clone(),
1248 max_age_seconds: 300,
1249 bind_htu_htm: true,
1250 bind_jkt: true,
1251 bind_client: false,
1252 },
1253 client_binding: None,
1254 };
1255 let err = verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1256 .await
1257 .unwrap_err();
1258 matches!(err, DpopError::UseDpopNonce { .. });
1259 }
1260
1261 #[tokio::test]
1262 async fn nonce_hmac_wrong_htu_prompts_use_dpop_nonce() {
1263 let (sk, x, y) = gen_es256_key();
1264 let now = OffsetDateTime::now_utc().unix_timestamp();
1265 let expected_htm = "GET";
1266 let expected_htu = "https://ex.com/correct";
1267
1268 let jkt = thumbprint_ec_p256(&x, &y).unwrap();
1270 let secret = SecretBox::from(b"k".to_vec());
1271 let ctx_wrong = crate::nonce::NonceCtx {
1272 htu: Some("https://ex.com/wrong"),
1273 htm: Some(expected_htm),
1274 jkt: Some(&jkt),
1275 client: None,
1276 };
1277 let nonce = issue_nonce(&secret, now, &ctx_wrong).expect("issue_nonce");
1278
1279 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1280 let p = serde_json::json!({
1281 "jti":"n-hmac-htu-mis",
1282 "iat":now,
1283 "htm":expected_htm,
1284 "htu":expected_htu,
1285 "nonce": nonce
1286 });
1287 let jws = make_jws(&sk, h, p);
1288
1289 let mut store = MemoryStore::default();
1290 let opts = VerifyOptions {
1291 max_age_seconds: 300,
1292 future_skew_seconds: 5,
1293 nonce_mode: NonceMode::Hmac {
1294 secret: secret.clone(),
1295 max_age_seconds: 300,
1296 bind_htu_htm: true,
1297 bind_jkt: true,
1298 bind_client: false,
1299 },
1300 client_binding: None,
1301 };
1302 let err = verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1303 .await
1304 .unwrap_err();
1305 matches!(err, DpopError::UseDpopNonce { .. });
1306 }
1307
1308 #[tokio::test]
1309 async fn nonce_hmac_wrong_jkt_prompts_use_dpop_nonce() {
1310 let (_sk_a, x_a, y_a) = gen_es256_key();
1312 let (sk_b, x_b, y_b) = gen_es256_key();
1313 let now = OffsetDateTime::now_utc().unix_timestamp();
1314 let expected_htu = "https://ex.com/a";
1315 let expected_htm = "GET";
1316
1317 let jkt_a = thumbprint_ec_p256(&x_a, &y_a).unwrap();
1318 let secret = SecretBox::from(b"secret-2".to_vec());
1319 let ctx = crate::nonce::NonceCtx {
1320 htu: Some(expected_htu),
1321 htm: Some(expected_htm),
1322 jkt: Some(&jkt_a), client: None,
1324 };
1325 let nonce = issue_nonce(&secret, now, &ctx).expect("issue_nonce");
1326
1327 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x_b,"y":y_b}});
1329 let p = serde_json::json!({
1330 "jti":"n-hmac-jkt-mis",
1331 "iat":now,
1332 "htm":expected_htm,
1333 "htu":expected_htu,
1334 "nonce": nonce
1335 });
1336 let jws = make_jws(&sk_b, h, p);
1337
1338 let mut store = MemoryStore::default();
1339 let opts = VerifyOptions {
1340 max_age_seconds: 300,
1341 future_skew_seconds: 5,
1342 nonce_mode: NonceMode::Hmac {
1343 secret: secret.clone(),
1344 max_age_seconds: 300,
1345 bind_htu_htm: true,
1346 bind_jkt: true,
1347 bind_client: false,
1348 },
1349 client_binding: None,
1350 };
1351 let err = verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1352 .await
1353 .unwrap_err();
1354 matches!(err, DpopError::UseDpopNonce { .. });
1355 }
1356
1357 #[tokio::test]
1358 async fn nonce_hmac_stale_prompts_use_dpop_nonce() {
1359 let (sk, x, y) = gen_es256_key();
1360 let now = OffsetDateTime::now_utc().unix_timestamp();
1361 let expected_htu = "https://ex.com/a";
1362 let expected_htm = "GET";
1363
1364 let jkt = thumbprint_ec_p256(&x, &y).unwrap();
1365 let secret = SecretBox::from(b"secret-3".to_vec());
1366 let issued_ts = now - 400;
1368 let nonce = issue_nonce(
1369 &secret,
1370 issued_ts,
1371 &crate::nonce::NonceCtx {
1372 htu: Some(expected_htu),
1373 htm: Some(expected_htm),
1374 jkt: Some(&jkt),
1375 client: None,
1376 },
1377 )
1378 .expect("issue_nonce");
1379
1380 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1381 let p = serde_json::json!({
1382 "jti":"n-hmac-stale",
1383 "iat":now,
1384 "htm":expected_htm,
1385 "htu":expected_htu,
1386 "nonce": nonce
1387 });
1388 let jws = make_jws(&sk, h, p);
1389
1390 let mut store = MemoryStore::default();
1391 let opts = VerifyOptions {
1392 max_age_seconds: 300,
1393 future_skew_seconds: 5,
1394 nonce_mode: NonceMode::Hmac {
1395 secret: secret.clone(),
1396 max_age_seconds: 300,
1397 bind_htu_htm: true,
1398 bind_jkt: true,
1399 bind_client: false,
1400 },
1401 client_binding: None,
1402 };
1403 let err = verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1404 .await
1405 .unwrap_err();
1406 matches!(err, DpopError::UseDpopNonce { .. });
1407 }
1408
1409 #[tokio::test]
1410 async fn nonce_hmac_future_skew_prompts_use_dpop_nonce() {
1411 let (sk, x, y) = gen_es256_key();
1412 let now = OffsetDateTime::now_utc().unix_timestamp();
1413 let expected_htu = "https://ex.com/a";
1414 let expected_htm = "GET";
1415
1416 let jkt = thumbprint_ec_p256(&x, &y).unwrap();
1417 let secret = SecretBox::from(b"secret-4".to_vec());
1418 let issued_ts = now + 10;
1420 let nonce = issue_nonce(
1421 &secret,
1422 issued_ts,
1423 &crate::nonce::NonceCtx {
1424 htu: Some(expected_htu),
1425 htm: Some(expected_htm),
1426 jkt: Some(&jkt),
1427 client: None,
1428 },
1429 )
1430 .expect("issue_nonce");
1431
1432 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1433 let p = serde_json::json!({
1434 "jti":"n-hmac-future",
1435 "iat":now,
1436 "htm":expected_htm,
1437 "htu":expected_htu,
1438 "nonce": nonce
1439 });
1440 let jws = make_jws(&sk, h, p);
1441
1442 let mut store = MemoryStore::default();
1443 let opts = VerifyOptions {
1444 max_age_seconds: 300,
1445 future_skew_seconds: 5,
1446 nonce_mode: NonceMode::Hmac {
1447 secret: secret.clone(),
1448 max_age_seconds: 300,
1449 bind_htu_htm: true,
1450 bind_jkt: true,
1451 bind_client: false,
1452 },
1453 client_binding: None,
1454 };
1455 let err = verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1456 .await
1457 .unwrap_err();
1458 matches!(err, DpopError::UseDpopNonce { .. });
1459 }
1460
1461 #[tokio::test]
1462 async fn nonce_hmac_client_binding_ok() {
1463 let (sk, x, y) = gen_es256_key();
1464 let now = OffsetDateTime::now_utc().unix_timestamp();
1465 let expected_htu = "https://ex.com/a";
1466 let expected_htm = "GET";
1467 let client_id = "client-123";
1468
1469 let jkt = thumbprint_ec_p256(&x, &y).unwrap();
1470 let secret = SecretBox::from(b"secret-client".to_vec());
1471 let ctx = crate::nonce::NonceCtx {
1472 htu: Some(expected_htu),
1473 htm: Some(expected_htm),
1474 jkt: Some(&jkt),
1475 client: Some(client_id),
1476 };
1477 let nonce = issue_nonce(&secret, now, &ctx).expect("issue_nonce");
1478
1479 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1480 let p = serde_json::json!({
1481 "jti":"n-hmac-client-ok",
1482 "iat":now,
1483 "htm":expected_htm,
1484 "htu":expected_htu,
1485 "nonce": nonce
1486 });
1487 let jws = make_jws(&sk, h, p);
1488
1489 let mut store = MemoryStore::default();
1490 let opts = VerifyOptions {
1491 max_age_seconds: 300,
1492 future_skew_seconds: 5,
1493 nonce_mode: NonceMode::Hmac {
1494 secret: secret.clone(),
1495 max_age_seconds: 300,
1496 bind_htu_htm: true,
1497 bind_jkt: true,
1498 bind_client: true,
1499 },
1500 client_binding: Some(ClientBinding::new(client_id)),
1501 };
1502 assert!(
1503 verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1504 .await
1505 .is_ok()
1506 );
1507 }
1508
1509 #[tokio::test]
1510 async fn nonce_hmac_client_binding_mismatch_prompts_use_dpop_nonce() {
1511 let (sk, x, y) = gen_es256_key();
1512 let now = OffsetDateTime::now_utc().unix_timestamp();
1513 let expected_htu = "https://ex.com/a";
1514 let expected_htm = "GET";
1515 let issue_client_id = "client-issuer";
1516 let verify_client_id = "client-verifier";
1517
1518 let jkt = thumbprint_ec_p256(&x, &y).unwrap();
1519 let secret = SecretBox::from(b"secret-client-mismatch".to_vec());
1520 let ctx = crate::nonce::NonceCtx {
1521 htu: Some(expected_htu),
1522 htm: Some(expected_htm),
1523 jkt: Some(&jkt),
1524 client: Some(issue_client_id),
1525 };
1526 let nonce = issue_nonce(&secret, now, &ctx).expect("issue_nonce");
1527
1528 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1529 let p = serde_json::json!({
1530 "jti":"n-hmac-client-mismatch",
1531 "iat":now,
1532 "htm":expected_htm,
1533 "htu":expected_htu,
1534 "nonce": nonce
1535 });
1536 let jws = make_jws(&sk, h, p);
1537
1538 let mut store = MemoryStore::default();
1539 let opts = VerifyOptions {
1540 max_age_seconds: 300,
1541 future_skew_seconds: 5,
1542 nonce_mode: NonceMode::Hmac {
1543 secret: secret.clone(),
1544 max_age_seconds: 300,
1545 bind_htu_htm: true,
1546 bind_jkt: true,
1547 bind_client: true,
1548 },
1549 client_binding: Some(ClientBinding::new(verify_client_id)),
1550 };
1551 let err = verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1552 .await
1553 .unwrap_err();
1554 if let DpopError::UseDpopNonce { nonce: new_nonce } = err {
1555 let retry_ctx = crate::nonce::NonceCtx {
1557 htu: Some(expected_htu),
1558 htm: Some(expected_htm),
1559 jkt: Some(&jkt),
1560 client: Some(verify_client_id),
1561 };
1562 assert!(
1563 crate::nonce::verify_nonce(&secret, &new_nonce, now, 300, &retry_ctx).is_ok(),
1564 "returned nonce should bind to verifier client id"
1565 );
1566 } else {
1567 panic!("expected UseDpopNonce, got {err:?}");
1568 }
1569 }
1570
1571 #[cfg(feature = "eddsa")]
1572 mod eddsa_tests {
1573 use super::*;
1574 use ed25519_dalek::Signer;
1575 use ed25519_dalek::{Signature as EdSig, SigningKey as EdSk, VerifyingKey as EdVk};
1576 use rand_core::OsRng;
1577
1578 fn gen_ed25519() -> (EdSk, String) {
1579 let sk = EdSk::generate(&mut OsRng);
1580 let vk = EdVk::from(&sk);
1581 let x_b64 = B64.encode(vk.as_bytes()); (sk, x_b64)
1583 }
1584
1585 fn make_jws_ed(sk: &EdSk, header: serde_json::Value, claims: serde_json::Value) -> String {
1586 let h = serde_json::to_vec(&header).unwrap();
1587 let p = serde_json::to_vec(&claims).unwrap();
1588 let h_b64 = B64.encode(h);
1589 let p_b64 = B64.encode(p);
1590 let signing_input = format!("{h_b64}.{p_b64}");
1591 let sig: EdSig = sk.sign(signing_input.as_bytes());
1592 let s_b64 = B64.encode(sig.to_bytes());
1593 format!("{h_b64}.{p_b64}.{s_b64}")
1594 }
1595
1596 #[tokio::test]
1597 async fn verify_valid_eddsa_proof() {
1598 let (sk, x) = gen_ed25519();
1599 let now = OffsetDateTime::now_utc().unix_timestamp();
1600 let h = serde_json::json!({"typ":"dpop+jwt","alg":"EdDSA","jwk":{"kty":"OKP","crv":"Ed25519","x":x}});
1601 let p =
1602 serde_json::json!({"jti":"ed-ok","iat":now,"htm":"GET","htu":"https://ex.com/a"});
1603 let jws = make_jws_ed(&sk, h, p);
1604
1605 let mut store = super::MemoryStore::default();
1606 assert!(verify_proof(
1607 &mut store,
1608 &jws,
1609 "https://ex.com/a",
1610 "GET",
1611 None,
1612 VerifyOptions::default(),
1613 )
1614 .await
1615 .is_ok());
1616 }
1617
1618 #[tokio::test]
1619 async fn eddsa_wrong_jwk_type_rejected() {
1620 let (sk, x) = gen_ed25519();
1621 let now = OffsetDateTime::now_utc().unix_timestamp();
1622 let h = serde_json::json!({"typ":"dpop+jwt","alg":"EdDSA","jwk":{"kty":"EC","crv":"P-256","x":x,"y":x}});
1624 let p = serde_json::json!({"jti":"ed-badjwk","iat":now,"htm":"GET","htu":"https://ex.com/a"});
1625 let jws = make_jws_ed(&sk, h, p);
1626
1627 let mut store = super::MemoryStore::default();
1628 let err = verify_proof(
1629 &mut store,
1630 &jws,
1631 "https://ex.com/a",
1632 "GET",
1633 None,
1634 VerifyOptions::default(),
1635 )
1636 .await
1637 .unwrap_err();
1638 matches!(err, DpopError::BadJwk(_));
1639 }
1640
1641 #[tokio::test]
1642 async fn eddsa_signature_tamper_detected() {
1643 let (sk, x) = gen_ed25519();
1644 let now = OffsetDateTime::now_utc().unix_timestamp();
1645 let h = serde_json::json!({"typ":"dpop+jwt","alg":"EdDSA","jwk":{"kty":"OKP","crv":"Ed25519","x":x}});
1646 let p = serde_json::json!({"jti":"ed-tamper","iat":now,"htm":"GET","htu":"https://ex.com/a"});
1647 let mut jws = make_jws_ed(&sk, h, p);
1648 unsafe {
1650 let bytes = jws.as_bytes_mut();
1651 for i in 10..(bytes.len().min(40)) {
1652 bytes[i] ^= 1;
1653 break;
1654 }
1655 }
1656 let mut store = super::MemoryStore::default();
1657 let err = verify_proof(
1658 &mut store,
1659 &jws,
1660 "https://ex.com/a",
1661 "GET",
1662 None,
1663 VerifyOptions::default(),
1664 )
1665 .await
1666 .unwrap_err();
1667 matches!(err, DpopError::InvalidSignature);
1668 }
1669 }
1670}