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