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: std::sync::Arc<[u8]>, max_age_secs: i64, 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_secs: i64,
61 pub future_skew_secs: 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_secs: 300,
69 future_skew_secs: 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(mut self, max_age_secs: i64) -> Self {
152 self.options.max_age_secs = max_age_secs;
153 self
154 }
155
156 pub fn with_future_skew(mut self, future_skew_secs: i64) -> Self {
158 self.options.future_skew_secs = future_skew_secs;
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_secs {
394 return Err(DpopError::FutureSkew);
395 }
396 if current_time - iat > self.options.max_age_secs {
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_secs,
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_secs,
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 std::sync::Arc;
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 p_future =
894 serde_json::json!({"jti":"jf","iat":now + 6,"htm":"GET","htu":"https://ex.com/a"});
895 let jws_future = make_jws(&sk, h.clone(), p_future);
896 let mut store1 = MemoryStore::default();
897 let opts = VerifyOptions {
898 max_age_secs: 300,
899 future_skew_secs: 5,
900 nonce_mode: NonceMode::Disabled,
901 client_binding: None,
902 };
903 let err = verify_proof(
904 &mut store1,
905 &jws_future,
906 "https://ex.com/a",
907 "GET",
908 None,
909 opts,
910 )
911 .await
912 .unwrap_err();
913 matches!(err, DpopError::FutureSkew);
914
915 let p_stale =
917 serde_json::json!({"jti":"js","iat":now - 301,"htm":"GET","htu":"https://ex.com/a"});
918 let jws_stale = make_jws(&sk, h.clone(), p_stale);
919 let mut store2 = MemoryStore::default();
920 let opts = VerifyOptions {
921 max_age_secs: 300,
922 future_skew_secs: 5,
923 nonce_mode: NonceMode::Disabled,
924 client_binding: None,
925 };
926 let err = verify_proof(
927 &mut store2,
928 &jws_stale,
929 "https://ex.com/a",
930 "GET",
931 None,
932 opts,
933 )
934 .await
935 .unwrap_err();
936 matches!(err, DpopError::Stale);
937 }
938
939 #[tokio::test]
940 async fn replay_same_jti_is_rejected() {
941 let (sk, x, y) = gen_es256_key();
942 let now = OffsetDateTime::now_utc().unix_timestamp();
943 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
944 let p = serde_json::json!({"jti":"jr","iat":now,"htm":"GET","htu":"https://ex.com/a"});
945 let jws = make_jws(&sk, h, p);
946
947 let mut store = MemoryStore::default();
948 let ok1 = verify_proof(
949 &mut store,
950 &jws,
951 "https://ex.com/a",
952 "GET",
953 None,
954 VerifyOptions::default(),
955 )
956 .await;
957 assert!(ok1.is_ok());
958 let err = verify_proof(
959 &mut store,
960 &jws,
961 "https://ex.com/a",
962 "GET",
963 None,
964 VerifyOptions::default(),
965 )
966 .await
967 .unwrap_err();
968 matches!(err, DpopError::Replay);
969 }
970
971 #[tokio::test]
972 async fn signature_tamper_detected() {
973 let (sk, x, y) = gen_es256_key();
974 let now = OffsetDateTime::now_utc().unix_timestamp();
975 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
976 let p = serde_json::json!({"jti":"jt","iat":now,"htm":"GET","htu":"https://ex.com/a"});
977 let mut jws = make_jws(&sk, h, p);
978
979 let bytes = unsafe { jws.as_bytes_mut() }; let mut dot_count = 0usize;
983 for i in 0..bytes.len() {
984 if bytes[i] == b'.' {
985 dot_count += 1;
986 if dot_count == 2 && i > 10 {
987 bytes[i - 5] ^= 0x01; break;
989 }
990 }
991 }
992
993 let mut store = MemoryStore::default();
994 let err = verify_proof(
995 &mut store,
996 &jws,
997 "https://ex.com/a",
998 "GET",
999 None,
1000 VerifyOptions::default(),
1001 )
1002 .await
1003 .unwrap_err();
1004 matches!(err, DpopError::InvalidSignature);
1005 }
1006
1007 #[tokio::test]
1008 async fn method_mismatch_rejected() {
1009 let (sk, x, y) = gen_es256_key();
1010 let now = OffsetDateTime::now_utc().unix_timestamp();
1011 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1012 let p = serde_json::json!({"jti":"jm","iat":now,"htm":"POST","htu":"https://ex.com/a"});
1013 let jws = make_jws(&sk, h, p);
1014
1015 let mut store = MemoryStore::default();
1016 let err = verify_proof(
1017 &mut store,
1018 &jws,
1019 "https://ex.com/a",
1020 "GET",
1021 None,
1022 VerifyOptions::default(),
1023 )
1024 .await
1025 .unwrap_err();
1026 matches!(err, DpopError::HtmMismatch);
1027 }
1028
1029 #[test]
1030 fn normalize_helpers_examples() {
1031 assert_eq!(
1033 normalize_htu("https://EX.com:443/a/./b/../c?x=1#frag").unwrap(),
1034 "https://ex.com/a/c"
1035 );
1036 assert_eq!(normalize_method("get").unwrap(), "GET");
1037 assert!(normalize_method("CUSTOM").is_err());
1038 }
1039
1040 #[tokio::test]
1041 async fn jti_too_long_rejected() {
1042 let (sk, x, y) = gen_es256_key();
1043 let now = OffsetDateTime::now_utc().unix_timestamp();
1044 let too_long = "x".repeat(513);
1045 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1046 let p = serde_json::json!({"jti":too_long,"iat":now,"htm":"GET","htu":"https://ex.com/a"});
1047 let jws = make_jws(&sk, h, p);
1048
1049 let mut store = MemoryStore::default();
1050 let err = verify_proof(
1051 &mut store,
1052 &jws,
1053 "https://ex.com/a",
1054 "GET",
1055 None,
1056 VerifyOptions::default(),
1057 )
1058 .await
1059 .unwrap_err();
1060 matches!(err, DpopError::JtiTooLong);
1061 }
1062 #[tokio::test]
1065 async fn nonce_require_equal_ok() {
1066 let (sk, x, y) = gen_es256_key();
1067 let now = OffsetDateTime::now_utc().unix_timestamp();
1068 let expected_htu = "https://ex.com/a";
1069 let expected_htm = "GET";
1070
1071 let expected_nonce = "nonce-123";
1072 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1073 let p = serde_json::json!({
1074 "jti":"n-reqeq-ok",
1075 "iat":now,
1076 "htm":expected_htm,
1077 "htu":expected_htu,
1078 "nonce": expected_nonce
1079 });
1080 let jws = make_jws(&sk, h, p);
1081
1082 let mut store = MemoryStore::default();
1083 let opts = VerifyOptions {
1084 max_age_secs: 300,
1085 future_skew_secs: 5,
1086 nonce_mode: NonceMode::RequireEqual {
1087 expected_nonce: expected_nonce.to_string(),
1088 },
1089 client_binding: None,
1090 };
1091 assert!(
1092 verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1093 .await
1094 .is_ok()
1095 );
1096 }
1097
1098 #[tokio::test]
1099 async fn nonce_require_equal_missing_claim() {
1100 let (sk, x, y) = gen_es256_key();
1101 let now = OffsetDateTime::now_utc().unix_timestamp();
1102 let expected_htu = "https://ex.com/a";
1103 let expected_htm = "GET";
1104
1105 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1106 let p = serde_json::json!({
1107 "jti":"n-reqeq-miss",
1108 "iat":now,
1109 "htm":expected_htm,
1110 "htu":expected_htu
1111 });
1112 let jws = make_jws(&sk, h, p);
1113
1114 let mut store = MemoryStore::default();
1115 let opts = VerifyOptions {
1116 max_age_secs: 300,
1117 future_skew_secs: 5,
1118 nonce_mode: NonceMode::RequireEqual {
1119 expected_nonce: "x".into(),
1120 },
1121 client_binding: None,
1122 };
1123 let err = verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1124 .await
1125 .unwrap_err();
1126 matches!(err, DpopError::MissingNonce);
1127 }
1128
1129 #[tokio::test]
1130 async fn nonce_require_equal_mismatch_yields_usedpopnonce() {
1131 let (sk, x, y) = gen_es256_key();
1132 let now = OffsetDateTime::now_utc().unix_timestamp();
1133 let expected_htu = "https://ex.com/a";
1134 let expected_htm = "GET";
1135
1136 let claim_nonce = "client-value";
1137 let expected_nonce = "server-expected";
1138 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1139 let p = serde_json::json!({
1140 "jti":"n-reqeq-mis",
1141 "iat":now,
1142 "htm":expected_htm,
1143 "htu":expected_htu,
1144 "nonce": claim_nonce
1145 });
1146 let jws = make_jws(&sk, h, p);
1147
1148 let mut store = MemoryStore::default();
1149 let opts = VerifyOptions {
1150 max_age_secs: 300,
1151 future_skew_secs: 5,
1152 nonce_mode: NonceMode::RequireEqual {
1153 expected_nonce: expected_nonce.into(),
1154 },
1155 client_binding: None,
1156 };
1157 let err = verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1158 .await
1159 .unwrap_err();
1160 if let DpopError::UseDpopNonce { nonce } = err {
1162 assert_eq!(nonce, expected_nonce);
1163 } else {
1164 panic!("expected UseDpopNonce, got {err:?}");
1165 }
1166 }
1167
1168 #[tokio::test]
1171 async fn nonce_hmac_ok_bound_all() {
1172 let (sk, x, y) = gen_es256_key();
1173 let now = OffsetDateTime::now_utc().unix_timestamp();
1174 let expected_htu = "https://ex.com/a";
1175 let expected_htm = "GET";
1176
1177 let jkt = thumbprint_ec_p256(&x, &y).unwrap();
1179
1180 let secret: Arc<[u8]> = Arc::from(&b"supersecret"[..]);
1181 let ctx = crate::nonce::NonceCtx {
1182 htu: Some(expected_htu),
1183 htm: Some(expected_htm),
1184 jkt: Some(&jkt),
1185 client: None,
1186 };
1187 let nonce = issue_nonce(&secret, now, &ctx).expect("issue_nonce");
1188
1189 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1190 let p = serde_json::json!({
1191 "jti":"n-hmac-ok",
1192 "iat":now,
1193 "htm":expected_htm,
1194 "htu":expected_htu,
1195 "nonce": nonce
1196 });
1197 let jws = make_jws(&sk, h, p);
1198
1199 let mut store = MemoryStore::default();
1200 let opts = VerifyOptions {
1201 max_age_secs: 300,
1202 future_skew_secs: 5,
1203 nonce_mode: NonceMode::Hmac {
1204 secret: secret.clone(),
1205 max_age_secs: 300,
1206 bind_htu_htm: true,
1207 bind_jkt: true,
1208 bind_client: false,
1209 },
1210 client_binding: None,
1211 };
1212 assert!(
1213 verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1214 .await
1215 .is_ok()
1216 );
1217 }
1218
1219 #[tokio::test]
1220 async fn nonce_hmac_missing_claim_prompts_use_dpop_nonce() {
1221 let (sk, x, y) = gen_es256_key();
1222 let now = OffsetDateTime::now_utc().unix_timestamp();
1223 let expected_htu = "https://ex.com/a";
1224 let expected_htm = "GET";
1225
1226 let secret: Arc<[u8]> = Arc::from(&b"supersecret"[..]);
1227
1228 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1229 let p = serde_json::json!({
1230 "jti":"n-hmac-miss",
1231 "iat":now,
1232 "htm":expected_htm,
1233 "htu":expected_htu
1234 });
1235 let jws = make_jws(&sk, h, p);
1236
1237 let mut store = MemoryStore::default();
1238 let opts = VerifyOptions {
1239 max_age_secs: 300,
1240 future_skew_secs: 5,
1241 nonce_mode: NonceMode::Hmac {
1242 secret: secret.clone(),
1243 max_age_secs: 300,
1244 bind_htu_htm: true,
1245 bind_jkt: true,
1246 bind_client: false,
1247 },
1248 client_binding: None,
1249 };
1250 let err = verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1251 .await
1252 .unwrap_err();
1253 matches!(err, DpopError::UseDpopNonce { .. });
1254 }
1255
1256 #[tokio::test]
1257 async fn nonce_hmac_wrong_htu_prompts_use_dpop_nonce() {
1258 let (sk, x, y) = gen_es256_key();
1259 let now = OffsetDateTime::now_utc().unix_timestamp();
1260 let expected_htm = "GET";
1261 let expected_htu = "https://ex.com/correct";
1262
1263 let jkt = thumbprint_ec_p256(&x, &y).unwrap();
1265 let secret: Arc<[u8]> = Arc::from(&b"k"[..]);
1266 let ctx_wrong = crate::nonce::NonceCtx {
1267 htu: Some("https://ex.com/wrong"),
1268 htm: Some(expected_htm),
1269 jkt: Some(&jkt),
1270 client: None,
1271 };
1272 let nonce = issue_nonce(&secret, now, &ctx_wrong).expect("issue_nonce");
1273
1274 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1275 let p = serde_json::json!({
1276 "jti":"n-hmac-htu-mis",
1277 "iat":now,
1278 "htm":expected_htm,
1279 "htu":expected_htu,
1280 "nonce": nonce
1281 });
1282 let jws = make_jws(&sk, h, p);
1283
1284 let mut store = MemoryStore::default();
1285 let opts = VerifyOptions {
1286 max_age_secs: 300,
1287 future_skew_secs: 5,
1288 nonce_mode: NonceMode::Hmac {
1289 secret: secret.clone(),
1290 max_age_secs: 300,
1291 bind_htu_htm: true,
1292 bind_jkt: true,
1293 bind_client: false,
1294 },
1295 client_binding: None,
1296 };
1297 let err = verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1298 .await
1299 .unwrap_err();
1300 matches!(err, DpopError::UseDpopNonce { .. });
1301 }
1302
1303 #[tokio::test]
1304 async fn nonce_hmac_wrong_jkt_prompts_use_dpop_nonce() {
1305 let (_sk_a, x_a, y_a) = gen_es256_key();
1307 let (sk_b, x_b, y_b) = gen_es256_key();
1308 let now = OffsetDateTime::now_utc().unix_timestamp();
1309 let expected_htu = "https://ex.com/a";
1310 let expected_htm = "GET";
1311
1312 let jkt_a = thumbprint_ec_p256(&x_a, &y_a).unwrap();
1313 let secret: Arc<[u8]> = Arc::from(&b"secret-2"[..]);
1314 let ctx = crate::nonce::NonceCtx {
1315 htu: Some(expected_htu),
1316 htm: Some(expected_htm),
1317 jkt: Some(&jkt_a), client: None,
1319 };
1320 let nonce = issue_nonce(&secret, now, &ctx).expect("issue_nonce");
1321
1322 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x_b,"y":y_b}});
1324 let p = serde_json::json!({
1325 "jti":"n-hmac-jkt-mis",
1326 "iat":now,
1327 "htm":expected_htm,
1328 "htu":expected_htu,
1329 "nonce": nonce
1330 });
1331 let jws = make_jws(&sk_b, h, p);
1332
1333 let mut store = MemoryStore::default();
1334 let opts = VerifyOptions {
1335 max_age_secs: 300,
1336 future_skew_secs: 5,
1337 nonce_mode: NonceMode::Hmac {
1338 secret: secret.clone(),
1339 max_age_secs: 300,
1340 bind_htu_htm: true,
1341 bind_jkt: true,
1342 bind_client: false,
1343 },
1344 client_binding: None,
1345 };
1346 let err = verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1347 .await
1348 .unwrap_err();
1349 matches!(err, DpopError::UseDpopNonce { .. });
1350 }
1351
1352 #[tokio::test]
1353 async fn nonce_hmac_stale_prompts_use_dpop_nonce() {
1354 let (sk, x, y) = gen_es256_key();
1355 let now = OffsetDateTime::now_utc().unix_timestamp();
1356 let expected_htu = "https://ex.com/a";
1357 let expected_htm = "GET";
1358
1359 let jkt = thumbprint_ec_p256(&x, &y).unwrap();
1360 let secret: Arc<[u8]> = Arc::from(&b"secret-3"[..]);
1361 let issued_ts = now - 400;
1363 let nonce = issue_nonce(
1364 &secret,
1365 issued_ts,
1366 &crate::nonce::NonceCtx {
1367 htu: Some(expected_htu),
1368 htm: Some(expected_htm),
1369 jkt: Some(&jkt),
1370 client: None,
1371 },
1372 )
1373 .expect("issue_nonce");
1374
1375 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1376 let p = serde_json::json!({
1377 "jti":"n-hmac-stale",
1378 "iat":now,
1379 "htm":expected_htm,
1380 "htu":expected_htu,
1381 "nonce": nonce
1382 });
1383 let jws = make_jws(&sk, h, p);
1384
1385 let mut store = MemoryStore::default();
1386 let opts = VerifyOptions {
1387 max_age_secs: 300,
1388 future_skew_secs: 5,
1389 nonce_mode: NonceMode::Hmac {
1390 secret: secret.clone(),
1391 max_age_secs: 300,
1392 bind_htu_htm: true,
1393 bind_jkt: true,
1394 bind_client: false,
1395 },
1396 client_binding: None,
1397 };
1398 let err = verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1399 .await
1400 .unwrap_err();
1401 matches!(err, DpopError::UseDpopNonce { .. });
1402 }
1403
1404 #[tokio::test]
1405 async fn nonce_hmac_future_skew_prompts_use_dpop_nonce() {
1406 let (sk, x, y) = gen_es256_key();
1407 let now = OffsetDateTime::now_utc().unix_timestamp();
1408 let expected_htu = "https://ex.com/a";
1409 let expected_htm = "GET";
1410
1411 let jkt = thumbprint_ec_p256(&x, &y).unwrap();
1412 let secret: Arc<[u8]> = Arc::from(&b"secret-4"[..]);
1413 let issued_ts = now + 10;
1415 let nonce = issue_nonce(
1416 &secret,
1417 issued_ts,
1418 &crate::nonce::NonceCtx {
1419 htu: Some(expected_htu),
1420 htm: Some(expected_htm),
1421 jkt: Some(&jkt),
1422 client: None,
1423 },
1424 )
1425 .expect("issue_nonce");
1426
1427 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1428 let p = serde_json::json!({
1429 "jti":"n-hmac-future",
1430 "iat":now,
1431 "htm":expected_htm,
1432 "htu":expected_htu,
1433 "nonce": nonce
1434 });
1435 let jws = make_jws(&sk, h, p);
1436
1437 let mut store = MemoryStore::default();
1438 let opts = VerifyOptions {
1439 max_age_secs: 300,
1440 future_skew_secs: 5,
1441 nonce_mode: NonceMode::Hmac {
1442 secret: secret.clone(),
1443 max_age_secs: 300,
1444 bind_htu_htm: true,
1445 bind_jkt: true,
1446 bind_client: false,
1447 },
1448 client_binding: None,
1449 };
1450 let err = verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1451 .await
1452 .unwrap_err();
1453 matches!(err, DpopError::UseDpopNonce { .. });
1454 }
1455
1456 #[tokio::test]
1457 async fn nonce_hmac_client_binding_ok() {
1458 let (sk, x, y) = gen_es256_key();
1459 let now = OffsetDateTime::now_utc().unix_timestamp();
1460 let expected_htu = "https://ex.com/a";
1461 let expected_htm = "GET";
1462 let client_id = "client-123";
1463
1464 let jkt = thumbprint_ec_p256(&x, &y).unwrap();
1465 let secret: Arc<[u8]> = Arc::from(&b"secret-client"[..]);
1466 let ctx = crate::nonce::NonceCtx {
1467 htu: Some(expected_htu),
1468 htm: Some(expected_htm),
1469 jkt: Some(&jkt),
1470 client: Some(client_id),
1471 };
1472 let nonce = issue_nonce(&secret, now, &ctx).expect("issue_nonce");
1473
1474 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1475 let p = serde_json::json!({
1476 "jti":"n-hmac-client-ok",
1477 "iat":now,
1478 "htm":expected_htm,
1479 "htu":expected_htu,
1480 "nonce": nonce
1481 });
1482 let jws = make_jws(&sk, h, p);
1483
1484 let mut store = MemoryStore::default();
1485 let opts = VerifyOptions {
1486 max_age_secs: 300,
1487 future_skew_secs: 5,
1488 nonce_mode: NonceMode::Hmac {
1489 secret: secret.clone(),
1490 max_age_secs: 300,
1491 bind_htu_htm: true,
1492 bind_jkt: true,
1493 bind_client: true,
1494 },
1495 client_binding: Some(ClientBinding::new(client_id)),
1496 };
1497 assert!(
1498 verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1499 .await
1500 .is_ok()
1501 );
1502 }
1503
1504 #[tokio::test]
1505 async fn nonce_hmac_client_binding_mismatch_prompts_use_dpop_nonce() {
1506 let (sk, x, y) = gen_es256_key();
1507 let now = OffsetDateTime::now_utc().unix_timestamp();
1508 let expected_htu = "https://ex.com/a";
1509 let expected_htm = "GET";
1510 let issue_client_id = "client-issuer";
1511 let verify_client_id = "client-verifier";
1512
1513 let jkt = thumbprint_ec_p256(&x, &y).unwrap();
1514 let secret: Arc<[u8]> = Arc::from(&b"secret-client-mismatch"[..]);
1515 let ctx = crate::nonce::NonceCtx {
1516 htu: Some(expected_htu),
1517 htm: Some(expected_htm),
1518 jkt: Some(&jkt),
1519 client: Some(issue_client_id),
1520 };
1521 let nonce = issue_nonce(&secret, now, &ctx).expect("issue_nonce");
1522
1523 let h = serde_json::json!({"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":x,"y":y}});
1524 let p = serde_json::json!({
1525 "jti":"n-hmac-client-mismatch",
1526 "iat":now,
1527 "htm":expected_htm,
1528 "htu":expected_htu,
1529 "nonce": nonce
1530 });
1531 let jws = make_jws(&sk, h, p);
1532
1533 let mut store = MemoryStore::default();
1534 let opts = VerifyOptions {
1535 max_age_secs: 300,
1536 future_skew_secs: 5,
1537 nonce_mode: NonceMode::Hmac {
1538 secret: secret.clone(),
1539 max_age_secs: 300,
1540 bind_htu_htm: true,
1541 bind_jkt: true,
1542 bind_client: true,
1543 },
1544 client_binding: Some(ClientBinding::new(verify_client_id)),
1545 };
1546 let err = verify_proof(&mut store, &jws, expected_htu, expected_htm, None, opts)
1547 .await
1548 .unwrap_err();
1549 if let DpopError::UseDpopNonce { nonce: new_nonce } = err {
1550 let retry_ctx = crate::nonce::NonceCtx {
1552 htu: Some(expected_htu),
1553 htm: Some(expected_htm),
1554 jkt: Some(&jkt),
1555 client: Some(verify_client_id),
1556 };
1557 assert!(
1558 crate::nonce::verify_nonce(&secret, &new_nonce, now, 300, &retry_ctx).is_ok(),
1559 "returned nonce should bind to verifier client id"
1560 );
1561 } else {
1562 panic!("expected UseDpopNonce, got {err:?}");
1563 }
1564 }
1565
1566 #[cfg(feature = "eddsa")]
1567 mod eddsa_tests {
1568 use super::*;
1569 use ed25519_dalek::Signer;
1570 use ed25519_dalek::{Signature as EdSig, SigningKey as EdSk, VerifyingKey as EdVk};
1571 use rand_core::OsRng;
1572
1573 fn gen_ed25519() -> (EdSk, String) {
1574 let sk = EdSk::generate(&mut OsRng);
1575 let vk = EdVk::from(&sk);
1576 let x_b64 = B64.encode(vk.as_bytes()); (sk, x_b64)
1578 }
1579
1580 fn make_jws_ed(sk: &EdSk, header: serde_json::Value, claims: serde_json::Value) -> String {
1581 let h = serde_json::to_vec(&header).unwrap();
1582 let p = serde_json::to_vec(&claims).unwrap();
1583 let h_b64 = B64.encode(h);
1584 let p_b64 = B64.encode(p);
1585 let signing_input = format!("{h_b64}.{p_b64}");
1586 let sig: EdSig = sk.sign(signing_input.as_bytes());
1587 let s_b64 = B64.encode(sig.to_bytes());
1588 format!("{h_b64}.{p_b64}.{s_b64}")
1589 }
1590
1591 #[tokio::test]
1592 async fn verify_valid_eddsa_proof() {
1593 let (sk, x) = gen_ed25519();
1594 let now = OffsetDateTime::now_utc().unix_timestamp();
1595 let h = serde_json::json!({"typ":"dpop+jwt","alg":"EdDSA","jwk":{"kty":"OKP","crv":"Ed25519","x":x}});
1596 let p =
1597 serde_json::json!({"jti":"ed-ok","iat":now,"htm":"GET","htu":"https://ex.com/a"});
1598 let jws = make_jws_ed(&sk, h, p);
1599
1600 let mut store = super::MemoryStore::default();
1601 assert!(verify_proof(
1602 &mut store,
1603 &jws,
1604 "https://ex.com/a",
1605 "GET",
1606 None,
1607 VerifyOptions::default(),
1608 )
1609 .await
1610 .is_ok());
1611 }
1612
1613 #[tokio::test]
1614 async fn eddsa_wrong_jwk_type_rejected() {
1615 let (sk, x) = gen_ed25519();
1616 let now = OffsetDateTime::now_utc().unix_timestamp();
1617 let h = serde_json::json!({"typ":"dpop+jwt","alg":"EdDSA","jwk":{"kty":"EC","crv":"P-256","x":x,"y":x}});
1619 let p = serde_json::json!({"jti":"ed-badjwk","iat":now,"htm":"GET","htu":"https://ex.com/a"});
1620 let jws = make_jws_ed(&sk, h, p);
1621
1622 let mut store = super::MemoryStore::default();
1623 let err = verify_proof(
1624 &mut store,
1625 &jws,
1626 "https://ex.com/a",
1627 "GET",
1628 None,
1629 VerifyOptions::default(),
1630 )
1631 .await
1632 .unwrap_err();
1633 matches!(err, DpopError::BadJwk(_));
1634 }
1635
1636 #[tokio::test]
1637 async fn eddsa_signature_tamper_detected() {
1638 let (sk, x) = gen_ed25519();
1639 let now = OffsetDateTime::now_utc().unix_timestamp();
1640 let h = serde_json::json!({"typ":"dpop+jwt","alg":"EdDSA","jwk":{"kty":"OKP","crv":"Ed25519","x":x}});
1641 let p = serde_json::json!({"jti":"ed-tamper","iat":now,"htm":"GET","htu":"https://ex.com/a"});
1642 let mut jws = make_jws_ed(&sk, h, p);
1643 unsafe {
1645 let bytes = jws.as_bytes_mut();
1646 for i in 10..(bytes.len().min(40)) {
1647 bytes[i] ^= 1;
1648 break;
1649 }
1650 }
1651 let mut store = super::MemoryStore::default();
1652 let err = verify_proof(
1653 &mut store,
1654 &jws,
1655 "https://ex.com/a",
1656 "GET",
1657 None,
1658 VerifyOptions::default(),
1659 )
1660 .await
1661 .unwrap_err();
1662 matches!(err, DpopError::InvalidSignature);
1663 }
1664 }
1665}