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