1use base64::engine::general_purpose::STANDARD;
12use base64::Engine;
13use http::HeaderMap;
14use sha2::{Digest, Sha256};
15use url::Url;
16
17use crate::discovery::KeyResolver;
18use crate::error::{Error, SignatureError};
19use crate::identifier::authority;
20use crate::jwk::{Algorithm, Jwk, KeyProblem};
21use crate::jwt::{self, Decoded};
22use crate::keys::SigningKey;
23use crate::sf::{self, Bare, Item, Member};
24
25pub const LABEL: &str = "sig";
27
28pub const SIGNATURE_HEADERS: &[&str] = &[
31 "signature",
32 "signature-input",
33 "signature-key",
34 "content-digest",
35];
36
37#[derive(Clone, Debug, PartialEq, Eq)]
39#[non_exhaustive]
40pub enum SignatureKey {
41 Jwt(String),
43 JwksUri {
45 id: String,
47 dwk: String,
49 kid: String,
51 },
52 Hwk,
54}
55
56impl SignatureKey {
57 fn header_value(&self, key: &SigningKey) -> Result<String, Error> {
58 let quoted = |v: &str| {
59 if v.contains(['"', '\\']) || !v.is_ascii() {
60 Err(Error::Invalid(
61 "value cannot travel in Signature-Key".into(),
62 ))
63 } else {
64 Ok(sf::quote(v))
65 }
66 };
67 Ok(match self {
68 SignatureKey::Jwt(token) => format!("{LABEL}=jwt;jwt={}", quoted(token)?),
69 SignatureKey::JwksUri { id, dwk, kid } => format!(
70 "{LABEL}=jwks_uri;id={};dwk={};kid={}",
71 quoted(id)?,
72 quoted(dwk)?,
73 quoted(kid)?
74 ),
75 SignatureKey::Hwk => {
76 let jwk = key.public_jwk();
77 format!(
78 "{LABEL}=hwk;kty={};crv={};x={};alg={}",
79 quoted(&jwk.kty)?,
80 quoted(jwk.crv.as_deref().unwrap_or_default())?,
81 quoted(jwk.x.as_deref().unwrap_or_default())?,
82 quoted(jwk.alg.as_deref().unwrap_or_default())?
83 )
84 }
85 })
86 }
87}
88
89#[derive(Clone, Copy, Debug)]
91pub struct Outgoing<'a> {
92 pub method: &'a str,
94 pub url: &'a str,
96 pub content_type: Option<&'a str>,
98 pub body: Option<&'a [u8]>,
100 pub created: i64,
102}
103
104pub fn content_digest(body: &[u8]) -> String {
106 format!("sha-256=:{}:", STANDARD.encode(Sha256::digest(body)))
107}
108
109pub fn sign(
111 key: &SigningKey,
112 signature_key: &SignatureKey,
113 request: &Outgoing<'_>,
114) -> Result<Vec<(String, String)>, Error> {
115 let url = Url::parse(request.url).map_err(|e| Error::Url(format!("{}: {e}", request.url)))?;
116 let authority = authority(&url).ok_or_else(|| Error::Url("url has no host".into()))?;
117 let signature_key = signature_key.header_value(key)?;
118
119 let mut covered: Vec<(&str, String)> = vec![
120 ("@method", request.method.to_ascii_uppercase()),
121 ("@authority", authority),
122 ("@path", url.path().to_string()),
123 ];
124 if let Some(query) = url.query() {
125 covered.push(("@query", format!("?{query}")));
126 }
127 covered.push(("signature-key", signature_key.clone()));
128 let mut headers = Vec::new();
129 if let Some(body) = request.body.filter(|b| !b.is_empty()) {
130 let digest = content_digest(body);
131 let content_type = request
132 .content_type
133 .unwrap_or("application/json")
134 .to_string();
135 covered.push(("content-digest", digest.clone()));
136 covered.push(("content-type", content_type.clone()));
137 headers.push(("content-digest".to_string(), digest));
138 headers.push(("content-type".to_string(), content_type));
139 }
140
141 let components: Vec<String> = covered
142 .iter()
143 .map(|(name, _)| format!("\"{name}\""))
144 .collect();
145 let params = format!("({});created={}", components.join(" "), request.created);
146 let mut base: Vec<String> = covered
147 .iter()
148 .map(|(name, value)| format!("\"{name}\": {value}"))
149 .collect();
150 base.push(format!("\"@signature-params\": {params}"));
151 let signature = key.sign(base.join("\n").as_bytes());
152
153 headers.push(("signature-key".to_string(), signature_key));
154 headers.push(("signature-input".to_string(), format!("{LABEL}={params}")));
155 headers.push((
156 "signature".to_string(),
157 format!("{LABEL}=:{}:", STANDARD.encode(signature)),
158 ));
159 Ok(headers)
160}
161
162pub fn header_map(headers: &[(String, String)]) -> Result<HeaderMap, Error> {
164 let mut map = HeaderMap::new();
165 for (name, value) in headers {
166 map.insert(
167 http::HeaderName::from_bytes(name.as_bytes())
168 .map_err(|e| Error::Invalid(e.to_string()))?,
169 http::HeaderValue::from_str(value).map_err(|e| Error::Invalid(e.to_string()))?,
170 );
171 }
172 Ok(map)
173}
174
175pub fn sign_jwt(
177 key: &SigningKey,
178 token: &str,
179 request: &Outgoing<'_>,
180) -> Result<Vec<(String, String)>, Error> {
181 sign(key, &SignatureKey::Jwt(token.to_string()), request)
182}
183
184#[derive(Clone, Copy, Debug)]
186pub struct Incoming<'a> {
187 pub method: &'a str,
189 pub authority: &'a str,
192 pub path: &'a str,
194 pub query: Option<&'a str>,
196 pub headers: &'a HeaderMap,
198 pub body: Option<&'a [u8]>,
200}
201
202#[derive(Clone, Debug)]
205pub struct VerifyOptions {
206 pub now: i64,
208 pub max_clock_skew: i64,
210 pub require_content_digest: bool,
213 pub algorithms: Vec<Algorithm>,
215}
216
217impl Default for VerifyOptions {
218 fn default() -> Self {
219 Self {
220 now: crate::now_unix(),
221 max_clock_skew: 60,
222 require_content_digest: false,
223 algorithms: vec![Algorithm::Ed25519, Algorithm::Es256],
224 }
225 }
226}
227
228#[derive(Clone, Debug)]
230#[non_exhaustive]
231pub enum Signer {
232 Jwt {
234 raw: String,
236 decoded: Box<Decoded>,
238 },
239 JwksUri {
241 id: String,
243 dwk: String,
245 kid: String,
247 },
248 Hwk,
250}
251
252#[derive(Clone, Debug)]
254#[non_exhaustive]
255pub struct Verified {
256 pub label: String,
258 pub signer: Signer,
260 pub public_key: Jwk,
262 pub thumbprint: String,
264 pub created: i64,
266}
267
268impl Verified {
269 pub fn jwt(&self) -> Option<&str> {
271 match &self.signer {
272 Signer::Jwt { raw, .. } => Some(raw),
273 _ => None,
274 }
275 }
276}
277
278const REQUIRED_COMPONENTS: &[&str] = &["@method", "@authority", "@path", "signature-key"];
279
280fn invalid_input(detail: impl Into<String>) -> SignatureError {
281 SignatureError::new("invalid_input", detail)
282}
283
284fn header(headers: &HeaderMap, name: &str) -> Option<String> {
288 let values: Option<Vec<&str>> = headers
289 .get_all(name)
290 .iter()
291 .map(|v| v.to_str().ok())
292 .collect();
293 values.filter(|v| !v.is_empty()).map(|v| v.join(", "))
294}
295
296fn text_params(item: &Item) -> Result<Vec<(String, String)>, SignatureError> {
297 item.params
298 .iter()
299 .map(|(k, v)| {
300 v.as_text()
301 .map(|t| (k.clone(), t.to_string()))
302 .ok_or_else(|| {
303 SignatureError::new(
304 "invalid_key",
305 format!("Signature-Key {k} must be a String or Token"),
306 )
307 })
308 })
309 .collect()
310}
311
312pub async fn verify(
315 request: &Incoming<'_>,
316 options: &VerifyOptions,
317 resolver: &dyn KeyResolver,
318) -> Result<Verified, SignatureError> {
319 let invalid = |detail: &str| SignatureError::new("invalid_signature", detail);
320 let raw_key =
321 header(request.headers, "signature-key").ok_or_else(|| invalid("missing Signature-Key"))?;
322 let key_dict = sf::parse_dictionary(&raw_key)
323 .map_err(|e| SignatureError::new("invalid_key", format!("Signature-Key: {e}")))?;
324 let [(label, Member::Item(key_item))] = key_dict.as_slice() else {
325 return Err(SignatureError::new(
326 "invalid_key",
327 "Signature-Key must have exactly one member, an item",
328 ));
329 };
330 let Bare::Token(scheme) = &key_item.bare else {
331 return Err(SignatureError::new(
332 "invalid_key",
333 "Signature-Key scheme must be a token",
334 ));
335 };
336 let params = text_params(key_item)?;
337 let param = |name: &str| {
338 params
339 .iter()
340 .find(|(k, _)| k == name)
341 .map(|(_, v)| v.clone())
342 };
343
344 let raw_input = header(request.headers, "signature-input")
345 .ok_or_else(|| invalid("missing Signature-Input"))?;
346 let inputs =
347 sf::parse_dictionary(&raw_input).map_err(|e| invalid(&format!("Signature-Input: {e}")))?;
348 let Some((_, Member::InnerList(component_items, sig_params))) =
349 inputs.iter().find(|(l, _)| l == label)
350 else {
351 return Err(invalid("no Signature-Input for the Signature-Key label"));
352 };
353 let mut components = Vec::new();
354 for item in component_items {
355 match (&item.bare, item.params.is_empty()) {
356 (Bare::String(name), true) if components.contains(name) => {
357 return Err(invalid_input(format!("{name} is covered twice")))
358 }
359 (Bare::String(name), true) => components.push(name.clone()),
360 _ => return Err(invalid_input("only plain covered components are supported")),
361 }
362 }
363 if let Some(missing) = REQUIRED_COMPONENTS
364 .iter()
365 .find(|c| !components.iter().any(|n| n == *c))
366 {
367 return Err(invalid_input(format!("{missing} must be covered")));
368 }
369 let created = sig_params
370 .iter()
371 .find_map(|(k, v)| match (k.as_str(), v) {
372 ("created", Bare::Integer(i)) => Some(*i),
373 _ => None,
374 })
375 .ok_or_else(|| invalid("Signature-Input has no created"))?;
376 if created > options.now + options.max_clock_skew {
377 return Err(SignatureError::new(
378 "clock_skew",
379 format!(
380 "created is {}s ahead of this verifier's clock",
381 created - options.now
382 ),
383 ));
384 }
385 if options.now - created > options.max_clock_skew {
386 return Err(invalid("signature is stale"));
387 }
388 if let Some(expires) = sig_params.iter().find_map(|(k, v)| match (k.as_str(), v) {
389 ("expires", Bare::Integer(i)) => Some(*i),
390 _ => None,
391 }) {
392 if expires < options.now {
393 return Err(invalid("signature has expired"));
394 }
395 }
396
397 let (signer, public_key) = match scheme.as_str() {
398 "jwt" => {
399 let raw = param("jwt")
400 .ok_or_else(|| SignatureError::new("invalid_key", "jwt scheme missing jwt"))?;
401 let decoded =
402 jwt::decode(&raw).map_err(|e| SignatureError::new("invalid_jwt", e.to_string()))?;
403 let cnf = decoded
404 .claims
405 .get("cnf")
406 .and_then(|c| c.get("jwk"))
407 .cloned()
408 .ok_or_else(|| SignatureError::new("invalid_jwt", "JWT missing cnf.jwk"))?;
409 if !decoded.claims.get("exp").is_some_and(|e| e.is_number()) {
410 return Err(SignatureError::new("invalid_jwt", "JWT missing exp"));
411 }
412 let jwk: Jwk = serde_json::from_value(cnf)
413 .map_err(|e| SignatureError::new("invalid_key", format!("cnf.jwk: {e}")))?;
414 (
415 Signer::Jwt {
416 raw,
417 decoded: Box::new(decoded),
418 },
419 jwk,
420 )
421 }
422 "jwks_uri" => {
423 let (Some(id), Some(dwk), Some(kid)) = (param("id"), param("dwk"), param("kid")) else {
424 return Err(SignatureError::new(
425 "invalid_key",
426 "jwks_uri scheme needs id, dwk and kid",
427 ));
428 };
429 if !crate::identifier::is_server_identifier(&id)
432 || !crate::metadata::dwk::is_known(&dwk)
433 {
434 return Err(SignatureError::new(
435 "invalid_key",
436 "jwks_uri needs a server identifier and a known dwk",
437 ));
438 }
439 let jwks = resolver
440 .jwks(&id, &dwk)
441 .await
442 .map_err(|e| SignatureError::new("unknown_key", e.to_string()))?;
443 let jwk = jwks
444 .keys
445 .into_iter()
446 .find(|k| k.kid.as_deref() == Some(kid.as_str()))
447 .ok_or_else(|| {
448 SignatureError::new("unknown_key", format!("no key {kid:?} for {id}"))
449 })?;
450 (Signer::JwksUri { id, dwk, kid }, jwk)
451 }
452 "hwk" => {
453 if param("kid").is_some() {
454 return Err(SignatureError::new(
455 "invalid_key",
456 "hwk must not carry a kid",
457 ));
458 }
459 let jwk = Jwk {
460 kty: param("kty").unwrap_or_default(),
461 crv: param("crv"),
462 x: param("x"),
463 y: param("y"),
464 n: param("n"),
465 e: param("e"),
466 alg: param("alg"),
467 ..Default::default()
468 };
469 (Signer::Hwk, jwk)
470 }
471 other => {
472 return Err(SignatureError::new(
473 "unsupported_scheme",
474 format!("unsupported Signature-Key scheme {other}"),
475 ))
476 }
477 };
478 let algorithm = public_key.algorithm().map_err(|problem| match problem {
479 KeyProblem::Invalid(d) => SignatureError::new("invalid_key", d),
480 KeyProblem::Unsupported(d) => SignatureError::new("unsupported_algorithm", d),
481 })?;
482 if !options.algorithms.contains(&algorithm) {
483 return Err(SignatureError::new(
484 "unsupported_algorithm",
485 format!("{} is not accepted here", algorithm.as_str()),
486 ));
487 }
488 if let Some(declared) = sig_params
491 .iter()
492 .find_map(|(k, v)| (k == "alg").then_some(v))
493 {
494 let expected = match algorithm {
495 Algorithm::Ed25519 => "ed25519",
496 Algorithm::Es256 => "ecdsa-p256-sha256",
497 };
498 if declared.as_text() != Some(expected) {
499 return Err(invalid("the alg parameter disagrees with the key"));
500 }
501 }
502
503 let body = request.body.filter(|b| !b.is_empty());
504 let covers_digest = components.iter().any(|c| c == "content-digest");
505 if body.is_some() && options.require_content_digest && !covers_digest {
506 return Err(invalid_input(
507 "content-digest must be covered on a request with a body",
508 ));
509 }
510 if covers_digest {
511 let sent = header(request.headers, "content-digest")
512 .ok_or_else(|| invalid_input("content-digest is covered but absent"))?;
513 if sent != content_digest(body.unwrap_or_default()) {
514 return Err(invalid_input("content-digest does not match the body"));
515 }
516 }
517
518 let mut base = Vec::new();
519 for component in &components {
520 let value = match component.as_str() {
521 "@method" => request.method.to_ascii_uppercase(),
522 "@authority" => request.authority.to_ascii_lowercase(),
523 "@path" => request.path.to_string(),
524 "@query" => format!("?{}", request.query.unwrap_or_default()),
525 "@scheme" => "https".to_string(),
526 "@target-uri" => match request.query {
527 Some(q) => format!("https://{}{}?{q}", request.authority, request.path),
528 None => format!("https://{}{}", request.authority, request.path),
529 },
530 "@request-target" => match request.query {
531 Some(q) => format!("{}?{q}", request.path),
532 None => request.path.to_string(),
533 },
534 name if name.starts_with('@') => {
535 return Err(invalid_input(format!(
536 "unsupported derived component {name}"
537 )))
538 }
539 name => header(request.headers, name)
540 .map(|v| v.trim().to_string())
541 .ok_or_else(|| invalid_input(format!("covered header {name} is absent")))?,
542 };
543 base.push(format!("\"{component}\": {value}"));
544 }
545 base.push(format!(
546 "\"@signature-params\": {}",
547 sf::serialize_inner_list(component_items, sig_params)
548 ));
549
550 let raw_signature =
551 header(request.headers, "signature").ok_or_else(|| invalid("missing Signature"))?;
552 let signatures =
553 sf::parse_dictionary(&raw_signature).map_err(|e| invalid(&format!("Signature: {e}")))?;
554 let Some((
555 _,
556 Member::Item(Item {
557 bare: Bare::Bytes(signature),
558 ..
559 }),
560 )) = signatures.iter().find(|(l, _)| l == label)
561 else {
562 return Err(invalid("no Signature for the Signature-Key label"));
563 };
564 if !public_key.verify(algorithm, base.join("\n").as_bytes(), signature) {
565 return Err(invalid("signature does not verify"));
566 }
567 let thumbprint = public_key
568 .thumbprint()
569 .map_err(|e| SignatureError::new("invalid_key", e.to_string()))?;
570 Ok(Verified {
571 label: label.clone(),
572 signer,
573 public_key,
574 thumbprint,
575 created,
576 })
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582 use crate::discovery::NoKeys;
583 use http::{HeaderName, HeaderValue};
584 use serde_json::json;
585
586 fn agent_token(session: &SigningKey) -> String {
587 let header = json!({ "typ": "aa-agent+jwt", "kid": "ap-1" });
588 jwt::encode(
589 &SigningKey::generate(),
590 header.as_object().unwrap().clone(),
591 &json!({
592 "iss": "https://agent.example",
593 "dwk": "aauth-agent.json",
594 "sub": "aauth:travel@agent.example",
595 "iat": 1_791_000_000,
596 "exp": 1_791_003_600,
597 "cnf": { "jwk": session.public_jwk() },
598 }),
599 )
600 }
601
602 fn header_map(set: &[(String, String)]) -> HeaderMap {
603 let mut headers = HeaderMap::new();
604 for (k, v) in set {
605 headers.insert(
606 HeaderName::from_bytes(k.as_bytes()).unwrap(),
607 HeaderValue::from_str(v).unwrap(),
608 );
609 }
610 headers
611 }
612
613 fn get<'a>(set: &'a [(String, String)], name: &str) -> &'a str {
614 &set.iter().find(|(k, _)| k == name).unwrap().1
615 }
616
617 #[test]
618 fn a_signed_post_covers_its_body_in_the_signature_base() {
619 let key = SigningKey::generate();
620 let token = agent_token(&key);
621 let body = br#"{"offer_id":"ht-syd-qt"}"#;
622 let set = sign_jwt(
623 &key,
624 &token,
625 &Outgoing {
626 method: "post",
627 url: "https://API.booking.example/v1/reserve?x=1",
628 content_type: None,
629 body: Some(body),
630 created: 1_791_000_100,
631 },
632 )
633 .unwrap();
634 let params = r#"("@method" "@authority" "@path" "@query" "signature-key" "content-digest" "content-type");created=1791000100"#;
635 assert_eq!(get(&set, "signature-input"), format!("sig={params}"));
636 let digest = content_digest(body);
637 assert_eq!(get(&set, "content-digest"), digest);
638 let base = format!(
639 "\"@method\": POST\n\"@authority\": api.booking.example\n\"@path\": /v1/reserve\n\"@query\": ?x=1\n\"signature-key\": sig=jwt;jwt=\"{token}\"\n\"content-digest\": {digest}\n\"content-type\": application/json\n\"@signature-params\": {params}"
640 );
641 let raw = get(&set, "signature")
642 .strip_prefix("sig=:")
643 .unwrap()
644 .strip_suffix(':')
645 .unwrap();
646 let signature = STANDARD.decode(raw).unwrap();
647 assert!(key
648 .public_jwk()
649 .verify(Algorithm::Ed25519, base.as_bytes(), &signature));
650 }
651
652 async fn round_trip(
653 set: &[(String, String)],
654 method: &str,
655 path: &str,
656 query: Option<&str>,
657 body: Option<&[u8]>,
658 now: i64,
659 ) -> Result<Verified, SignatureError> {
660 let headers = header_map(set);
661 verify(
662 &Incoming {
663 method,
664 authority: "api.booking.example",
665 path,
666 query,
667 headers: &headers,
668 body,
669 },
670 &VerifyOptions {
671 now,
672 require_content_digest: true,
673 ..Default::default()
674 },
675 &NoKeys,
676 )
677 .await
678 }
679
680 #[tokio::test]
681 async fn what_is_signed_here_verifies_here() {
682 let key = SigningKey::generate();
683 let token = agent_token(&key);
684 let body = br#"{"a":1}"#;
685 let set = sign_jwt(
686 &key,
687 &token,
688 &Outgoing {
689 method: "POST",
690 url: "https://api.booking.example/v1/reserve?x=1",
691 content_type: None,
692 body: Some(body),
693 created: 1_000,
694 },
695 )
696 .unwrap();
697 let verified = round_trip(&set, "POST", "/v1/reserve", Some("x=1"), Some(body), 1_010)
698 .await
699 .unwrap();
700 assert_eq!(verified.thumbprint, key.thumbprint());
701 assert_eq!(verified.jwt(), Some(token.as_str()));
702
703 let tampered = round_trip(
704 &set,
705 "POST",
706 "/v1/reserve",
707 Some("x=1"),
708 Some(br#"{"a":2}"#),
709 1_010,
710 )
711 .await;
712 assert_eq!(tampered.unwrap_err().code, "invalid_input");
713 let moved = round_trip(&set, "POST", "/v1/cancel", Some("x=1"), Some(body), 1_010).await;
714 assert_eq!(moved.unwrap_err().code, "invalid_signature");
715 let stale = round_trip(&set, "POST", "/v1/reserve", Some("x=1"), Some(body), 2_000).await;
716 assert_eq!(stale.unwrap_err().code, "invalid_signature");
717 let early = round_trip(&set, "POST", "/v1/reserve", Some("x=1"), Some(body), 0).await;
718 assert_eq!(early.unwrap_err().code, "clock_skew");
719 }
720
721 #[tokio::test]
722 async fn an_unsigned_body_is_refused_where_the_profile_requires_it() {
723 let key = SigningKey::generate();
724 let set = sign_jwt(
725 &key,
726 &agent_token(&key),
727 &Outgoing {
728 method: "POST",
729 url: "https://api.booking.example/token",
730 content_type: None,
731 body: None,
732 created: 1_000,
733 },
734 )
735 .unwrap();
736 let refused = round_trip(&set, "POST", "/token", None, Some(b"{}"), 1_000).await;
737 assert_eq!(refused.unwrap_err().code, "invalid_input");
738 }
739
740 #[tokio::test]
741 async fn an_hwk_signature_carries_its_own_key() {
742 let key = SigningKey::generate();
743 let set = sign(
744 &key,
745 &SignatureKey::Hwk,
746 &Outgoing {
747 method: "GET",
748 url: "https://api.booking.example/v1/search",
749 content_type: None,
750 body: None,
751 created: 1_000,
752 },
753 )
754 .unwrap();
755 let verified = round_trip(&set, "GET", "/v1/search", None, None, 1_000)
756 .await
757 .unwrap();
758 assert!(matches!(verified.signer, Signer::Hwk));
759 assert_eq!(verified.thumbprint, key.thumbprint());
760 }
761
762 #[test]
763 fn a_token_that_would_break_the_header_is_refused() {
764 let key = SigningKey::generate();
765 let out = Outgoing {
766 method: "GET",
767 url: "https://a.example/",
768 content_type: None,
769 body: None,
770 created: 0,
771 };
772 assert!(sign_jwt(&key, "a\"b", &out).is_err());
773 }
774
775 #[derive(Default)]
777 struct Recording(std::sync::Mutex<Vec<String>>);
778
779 #[async_trait::async_trait]
780 impl KeyResolver for Recording {
781 async fn jwks(&self, issuer: &str, dwk: &str) -> crate::Result<crate::jwk::Jwks> {
782 self.0.lock().unwrap().push(format!("{issuer} {dwk}"));
783 Err(crate::Error::Discovery("none".into()))
784 }
785 }
786
787 #[tokio::test]
788 async fn a_jwks_uri_naming_an_internal_address_or_path_is_never_fetched() {
789 let key = SigningKey::generate();
790 for (id, dwk) in [
791 ("http://127.0.0.1:8080", "aauth-agent.json"),
792 ("https://10.0.0.1", "aauth-agent.json"),
793 ("https://ps.example", "../internal/admin?x=1"),
794 ] {
795 let set = sign(
796 &key,
797 &SignatureKey::JwksUri {
798 id: id.into(),
799 dwk: dwk.into(),
800 kid: "k".into(),
801 },
802 &Outgoing {
803 method: "GET",
804 url: "https://api.booking.example/v1/search",
805 content_type: None,
806 body: None,
807 created: 1_000,
808 },
809 )
810 .unwrap();
811 let headers = super::header_map(&set).unwrap();
812 let resolver = Recording::default();
813 let refused = verify(
814 &Incoming {
815 method: "GET",
816 authority: "api.booking.example",
817 path: "/v1/search",
818 query: None,
819 headers: &headers,
820 body: None,
821 },
822 &VerifyOptions {
823 now: 1_000,
824 ..Default::default()
825 },
826 &resolver,
827 )
828 .await
829 .unwrap_err();
830 assert_eq!(refused.code, "invalid_key", "{id} {dwk}");
831 assert!(
832 resolver.0.lock().unwrap().is_empty(),
833 "{id} {dwk} was fetched"
834 );
835 }
836 }
837
838 #[tokio::test]
839 async fn a_component_covered_twice_is_refused() {
840 let key = SigningKey::generate();
841 let mut set = sign_jwt(
842 &key,
843 &agent_token(&key),
844 &Outgoing {
845 method: "GET",
846 url: "https://api.booking.example/v1/search",
847 content_type: None,
848 body: None,
849 created: 1_000,
850 },
851 )
852 .unwrap();
853 for (name, value) in set.iter_mut() {
854 if name == "signature-input" {
855 *value = value.replace("\"@path\"", "\"@path\" \"@path\"");
856 }
857 }
858 let refused = round_trip(&set, "GET", "/v1/search", None, None, 1_000)
859 .await
860 .unwrap_err();
861 assert_eq!(refused.code, "invalid_input");
862 }
863}