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