1use std::fmt::Write as _;
5
6use base64::{engine::general_purpose::STANDARD, Engine};
7use wimsey_jose::{SigningKey, VerifyingKey, SIGNATURE_LEN};
8
9use crate::error::HttpSigError;
10use crate::message::{Component, ComponentSource};
11
12pub const ALG: &str = "ed25519";
19
20pub const WIMSE_TAG: &str = "wimse-workload-to-workload";
22
23pub const WIMSE_LABEL: &str = "wimse";
26
27#[derive(Debug, Clone, Default, PartialEq, Eq)]
33pub struct SignatureParams {
34 pub created: Option<u64>,
36 pub expires: Option<u64>,
38 pub keyid: Option<String>,
40 pub alg: Option<String>,
42 pub nonce: Option<String>,
44 pub tag: Option<String>,
46 pub wimse_aud: Option<String>,
49 pub wimse_sign_response: Option<bool>,
53 pub wimse_req_nonce: Option<String>,
56}
57
58#[derive(Debug, Clone)]
60pub struct SignedSignature {
61 pub signature_input: String,
63 pub signature: String,
65}
66
67#[derive(Debug, Clone)]
69pub struct VerifiedSignature {
70 pub label: String,
72 pub components: Vec<Component>,
74 pub params: SignatureParams,
76}
77
78#[derive(Debug, Clone, Default)]
87pub struct VerifyConfig {
88 pub now: Option<u64>,
91 pub leeway: u64,
93 pub label: Option<String>,
95 pub required_components: Vec<Component>,
98 pub max_age: Option<u64>,
101 pub wimse_profile: bool,
108 pub expected_audience: Option<String>,
111 pub wimse_response_profile: bool,
120 pub expected_req_nonce: Option<String>,
124}
125
126pub fn check_request_profile(params: &SignatureParams) -> Result<(), HttpSigError> {
139 if params.keyid.is_some() {
140 return Err(HttpSigError::ForbiddenParameter("keyid"));
141 }
142 if params.alg.is_some() {
143 return Err(HttpSigError::ForbiddenParameter("alg"));
144 }
145 if params.created.is_none() {
146 return Err(HttpSigError::MissingParameter("created"));
147 }
148 if params.expires.is_none() {
149 return Err(HttpSigError::MissingParameter("expires"));
150 }
151 if params.nonce.is_none() {
152 return Err(HttpSigError::MissingParameter("nonce"));
153 }
154 match params.tag.as_deref() {
155 None => return Err(HttpSigError::MissingParameter("tag")),
156 Some(tag) if tag != WIMSE_TAG => {
157 return Err(HttpSigError::WrongTag {
158 found: tag.to_owned(),
159 })
160 }
161 Some(_) => {}
162 }
163 if params.wimse_aud.is_none() {
164 return Err(HttpSigError::MissingParameter("wimse-aud"));
165 }
166 Ok(())
167}
168
169pub fn check_response_profile(
184 params: &SignatureParams,
185 response_signing_required: bool,
186) -> Result<(), HttpSigError> {
187 if params.keyid.is_some() {
188 return Err(HttpSigError::ForbiddenParameter("keyid"));
189 }
190 if params.alg.is_some() {
191 return Err(HttpSigError::ForbiddenParameter("alg"));
192 }
193 if params.created.is_none() {
194 return Err(HttpSigError::MissingParameter("created"));
195 }
196 if params.expires.is_none() {
197 return Err(HttpSigError::MissingParameter("expires"));
198 }
199 if params.nonce.is_none() {
200 return Err(HttpSigError::MissingParameter("nonce"));
201 }
202 match params.tag.as_deref() {
203 None => return Err(HttpSigError::MissingParameter("tag")),
204 Some(tag) if tag != WIMSE_TAG => {
205 return Err(HttpSigError::WrongTag {
206 found: tag.to_owned(),
207 })
208 }
209 Some(_) => {}
210 }
211 if params.wimse_aud.is_some() {
212 return Err(HttpSigError::ForbiddenParameter("wimse-aud"));
213 }
214 if response_signing_required && params.wimse_req_nonce.is_none() {
215 return Err(HttpSigError::MissingParameter("wimse-req-nonce"));
216 }
217 Ok(())
218}
219
220#[must_use]
228pub fn response_components(headers: &[(String, String)]) -> Vec<Component> {
229 let mut components = vec![
230 Component::Status,
231 Component::Req(Box::new(Component::Method)),
232 Component::Req(Box::new(Component::RequestTarget)),
233 ];
234 for name in ["content-type", "content-digest", "workload-identity-token"] {
235 if headers.iter().any(|(n, _)| n.eq_ignore_ascii_case(name)) {
236 components.push(Component::header(name));
237 }
238 }
239 components
240}
241
242fn sf_string(value: &str) -> String {
243 let mut out = String::with_capacity(value.len() + 2);
244 out.push('"');
245 for c in value.chars() {
246 if c == '\\' || c == '"' {
247 out.push('\\');
248 }
249 out.push(c);
250 }
251 out.push('"');
252 out
253}
254
255fn serialize_params_value(components: &[Component], params: &SignatureParams) -> String {
256 let inner = components
257 .iter()
258 .map(Component::quoted_id)
259 .collect::<Vec<_>>()
260 .join(" ");
261 let mut s = format!("({inner})");
262 if let Some(created) = params.created {
263 let _ = write!(s, ";created={created}");
264 }
265 if let Some(expires) = params.expires {
266 let _ = write!(s, ";expires={expires}");
267 }
268 if let Some(keyid) = ¶ms.keyid {
269 let _ = write!(s, ";keyid={}", sf_string(keyid));
270 }
271 if let Some(alg) = ¶ms.alg {
272 let _ = write!(s, ";alg={}", sf_string(alg));
273 }
274 if let Some(nonce) = ¶ms.nonce {
275 let _ = write!(s, ";nonce={}", sf_string(nonce));
276 }
277 if let Some(tag) = ¶ms.tag {
278 let _ = write!(s, ";tag={}", sf_string(tag));
279 }
280 if let Some(aud) = ¶ms.wimse_aud {
281 let _ = write!(s, ";wimse-aud={}", sf_string(aud));
282 }
283 if let Some(sign_response) = params.wimse_sign_response {
284 if sign_response {
287 s.push_str(";wimse-sign-response");
288 } else {
289 s.push_str(";wimse-sign-response=?0");
290 }
291 }
292 if let Some(req_nonce) = ¶ms.wimse_req_nonce {
293 let _ = write!(s, ";wimse-req-nonce={}", sf_string(req_nonce));
294 }
295 s
296}
297
298fn signature_base_from_params_str(
299 message: &impl ComponentSource,
300 components: &[Component],
301 params_value: &str,
302) -> Result<String, HttpSigError> {
303 if params_value.contains(['\r', '\n']) {
306 return Err(HttpSigError::Parse(
307 "signature parameters contain CR or LF".to_owned(),
308 ));
309 }
310 let mut base = String::new();
311 for component in components {
312 let value = message.component_value(component)?;
313 if value.contains(['\r', '\n']) {
315 return Err(HttpSigError::InvalidComponentValue(component.quoted_id()));
316 }
317 base.push_str(&component.quoted_id());
318 base.push_str(": ");
319 base.push_str(&value);
320 base.push('\n');
321 }
322 base.push_str("\"@signature-params\": ");
323 base.push_str(params_value);
324 Ok(base)
325}
326
327pub fn signature_base(
334 message: &impl ComponentSource,
335 components: &[Component],
336 params: &SignatureParams,
337) -> Result<String, HttpSigError> {
338 let params_value = serialize_params_value(components, params);
339 signature_base_from_params_str(message, components, ¶ms_value)
340}
341
342pub fn sign(
349 message: &impl ComponentSource,
350 components: &[Component],
351 params: &SignatureParams,
352 label: &str,
353 signing_key: &SigningKey,
354) -> Result<SignedSignature, HttpSigError> {
355 let params_value = serialize_params_value(components, params);
356 let base = signature_base_from_params_str(message, components, ¶ms_value)?;
357 Ok(SignedSignature {
358 signature_input: format!("{label}={params_value}"),
359 signature: format!(
360 "{label}=:{}:",
361 STANDARD.encode(signing_key.sign(base.as_bytes()))
362 ),
363 })
364}
365
366fn split_member(value: &str) -> Result<(&str, &str), HttpSigError> {
368 let value = value.trim();
369 let eq = value
370 .find('=')
371 .ok_or_else(|| HttpSigError::Parse("missing `=` in dictionary member".to_owned()))?;
372 let label = value[..eq].trim();
373 if label.is_empty() {
374 return Err(HttpSigError::Parse("empty signature label".to_owned()));
375 }
376 Ok((label, &value[eq + 1..]))
377}
378
379fn parse_sf_string(token: &str) -> Result<String, HttpSigError> {
380 let inner = token
381 .strip_prefix('"')
382 .and_then(|t| t.strip_suffix('"'))
383 .ok_or_else(|| HttpSigError::Parse(format!("not a string: {token}")))?;
384 let mut out = String::with_capacity(inner.len());
385 let mut chars = inner.chars();
386 while let Some(c) = chars.next() {
387 if c == '\\' {
388 match chars.next() {
389 Some(next @ ('\\' | '"')) => out.push(next),
390 _ => return Err(HttpSigError::Parse("bad string escape".to_owned())),
391 }
392 } else {
393 out.push(c);
394 }
395 }
396 Ok(out)
397}
398
399fn find_unquoted(s: &str, target: char) -> Option<usize> {
402 let mut in_quotes = false;
403 let mut escaped = false;
404 for (idx, c) in s.char_indices() {
405 if escaped {
406 escaped = false;
407 } else if in_quotes && c == '\\' {
408 escaped = true;
409 } else if c == '"' {
410 in_quotes = !in_quotes;
411 } else if c == target && !in_quotes {
412 return Some(idx);
413 }
414 }
415 None
416}
417
418fn split_unquoted_semicolons(s: &str) -> Vec<&str> {
420 let mut parts = Vec::new();
421 let mut start = 0;
422 let mut in_quotes = false;
423 let mut escaped = false;
424 for (idx, c) in s.char_indices() {
425 if escaped {
426 escaped = false;
427 } else if in_quotes && c == '\\' {
428 escaped = true;
429 } else if c == '"' {
430 in_quotes = !in_quotes;
431 } else if c == ';' && !in_quotes {
432 parts.push(&s[start..idx]);
433 start = idx + 1;
434 }
435 }
436 parts.push(&s[start..]);
437 parts
438}
439
440fn parse_params(rest: &str, params: &mut SignatureParams) -> Result<(), HttpSigError> {
441 for part in split_unquoted_semicolons(rest) {
442 let part = part.trim();
443 if part.is_empty() {
444 continue;
445 }
446 let Some((name, raw)) = part.split_once('=') else {
447 if part == "wimse-sign-response" {
450 params.wimse_sign_response = Some(true);
451 }
452 continue;
453 };
454 let name = name.trim();
455 let raw = raw.trim();
456 match name {
457 "created" => {
458 params.created = Some(parse_int(raw)?);
459 }
460 "expires" => {
461 params.expires = Some(parse_int(raw)?);
462 }
463 "keyid" => params.keyid = Some(parse_sf_string(raw)?),
464 "alg" => params.alg = Some(parse_sf_string(raw)?),
465 "nonce" => params.nonce = Some(parse_sf_string(raw)?),
466 "tag" => params.tag = Some(parse_sf_string(raw)?),
467 "wimse-aud" => params.wimse_aud = Some(parse_sf_string(raw)?),
468 "wimse-sign-response" => params.wimse_sign_response = Some(parse_sf_boolean(raw)?),
469 "wimse-req-nonce" => params.wimse_req_nonce = Some(parse_sf_string(raw)?),
470 _ => {}
472 }
473 }
474 Ok(())
475}
476
477fn parse_sf_boolean(raw: &str) -> Result<bool, HttpSigError> {
479 match raw {
480 "?1" => Ok(true),
481 "?0" => Ok(false),
482 other => Err(HttpSigError::Parse(format!("not a boolean: {other}"))),
483 }
484}
485
486fn parse_int(raw: &str) -> Result<u64, HttpSigError> {
487 raw.trim()
488 .parse()
489 .map_err(|_| HttpSigError::Parse(format!("not an integer: {raw}")))
490}
491
492fn parse_signature_input(
495 value: &str,
496) -> Result<(String, Vec<Component>, SignatureParams, String), HttpSigError> {
497 let (label, rest) = split_member(value)?;
498 let rest = rest.trim();
499 if !rest.starts_with('(') {
500 return Err(HttpSigError::Parse(
501 "inner list must start with `(`".to_owned(),
502 ));
503 }
504 let close = find_unquoted(rest, ')')
506 .ok_or_else(|| HttpSigError::Parse("missing `)` in inner list".to_owned()))?;
507 let inner = &rest[1..close];
508
509 let mut components = Vec::new();
510 for token in inner.split_whitespace() {
511 components.push(Component::from_quoted_id(token)?);
512 }
513
514 let mut params = SignatureParams::default();
515 parse_params(&rest[close + 1..], &mut params)?;
516
517 Ok((label.to_owned(), components, params, rest.to_owned()))
518}
519
520fn parse_signature(value: &str) -> Result<(String, [u8; SIGNATURE_LEN]), HttpSigError> {
522 let (label, rest) = split_member(value)?;
523 let b64 = rest
524 .trim()
525 .strip_prefix(':')
526 .and_then(|t| t.strip_suffix(':'))
527 .ok_or_else(|| HttpSigError::Parse("byte sequence must be wrapped in `:`".to_owned()))?;
528 let bytes = STANDARD
529 .decode(b64)
530 .map_err(|_| HttpSigError::MalformedSignature)?;
531 let array: [u8; SIGNATURE_LEN] = bytes
532 .try_into()
533 .map_err(|_| HttpSigError::MalformedSignature)?;
534 Ok((label.to_owned(), array))
535}
536
537pub fn verify(
560 message: &impl ComponentSource,
561 signature_input: &str,
562 signature: &str,
563 verifying_key: &VerifyingKey,
564 config: &VerifyConfig,
565) -> Result<VerifiedSignature, HttpSigError> {
566 let (input_label, components, params, params_value) = parse_signature_input(signature_input)?;
567 let (sig_label, sig_bytes) = parse_signature(signature)?;
568
569 if input_label != sig_label {
570 return Err(HttpSigError::LabelMismatch);
571 }
572 if let Some(expected) = &config.label {
573 if expected != &input_label {
574 return Err(HttpSigError::LabelMismatch);
575 }
576 }
577 if config.wimse_profile {
578 check_request_profile(¶ms)?;
579 }
580 if config.wimse_response_profile {
581 check_response_profile(¶ms, config.expected_req_nonce.is_some())?;
582 }
583 if let Some(alg) = ¶ms.alg {
584 if alg != ALG {
585 return Err(HttpSigError::UnsupportedAlg { found: alg.clone() });
586 }
587 }
588
589 let base = signature_base_from_params_str(message, &components, ¶ms_value)?;
590 verifying_key
591 .verify(base.as_bytes(), &sig_bytes)
592 .map_err(|_| HttpSigError::InvalidSignature)?;
593
594 for required in &config.required_components {
595 if !components.contains(required) {
596 return Err(HttpSigError::MissingRequiredComponent(required.quoted_id()));
597 }
598 }
599 if let Some(expected) = &config.expected_audience {
604 if params.wimse_aud.as_ref() != Some(expected) {
605 return Err(HttpSigError::AudienceMismatch);
606 }
607 }
608 if let Some(expected) = &config.expected_req_nonce {
612 if params.wimse_req_nonce.as_ref() != Some(expected) {
613 return Err(HttpSigError::RequestNonceMismatch);
614 }
615 }
616
617 if let (Some(created), Some(expires)) = (params.created, params.expires) {
618 if expires < created {
619 return Err(HttpSigError::InvalidTimeWindow);
620 }
621 }
622 if config.max_age.is_some() && config.now.is_none() {
625 return Err(HttpSigError::TooOld);
626 }
627 if let Some(now) = config.now {
628 if let Some(expires) = params.expires {
629 if now > expires.saturating_add(config.leeway) {
630 return Err(HttpSigError::Expired);
631 }
632 }
633 if let Some(created) = params.created {
634 if created > now.saturating_add(config.leeway) {
635 return Err(HttpSigError::CreatedInFuture);
636 }
637 }
638 if let Some(max_age) = config.max_age {
639 let created = params.created.ok_or(HttpSigError::TooOld)?;
640 if now.saturating_sub(created) > max_age {
641 return Err(HttpSigError::TooOld);
642 }
643 }
644 }
645
646 Ok(VerifiedSignature {
647 label: input_label,
648 components,
649 params,
650 })
651}
652
653#[cfg(test)]
654mod tests {
655 use wimsey_jose::SigningKey;
656
657 use super::{sign, signature_base, verify, SignatureParams, VerifyConfig, ALG};
658 use crate::error::HttpSigError;
659 use crate::message::{Component, HttpExchange, HttpRequest};
660
661 fn rfc_request() -> HttpRequest {
663 HttpRequest {
664 method: "POST".to_owned(),
665 authority: "example.com".to_owned(),
666 path: "/foo".to_owned(),
667 query: Some("param=Value&Pet=dog".to_owned()),
668 headers: vec![
669 ("Host".to_owned(), "example.com".to_owned()),
670 ("Date".to_owned(), "Tue, 20 Apr 2021 02:07:55 GMT".to_owned()),
671 ("Content-Type".to_owned(), "application/json".to_owned()),
672 (
673 "Content-Digest".to_owned(),
674 "sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:".to_owned(),
675 ),
676 ("Content-Length".to_owned(), "18".to_owned()),
677 ],
678 }
679 }
680
681 fn rfc_components() -> Vec<Component> {
682 vec![
683 Component::Method,
684 Component::Authority,
685 Component::Path,
686 Component::header("content-digest"),
687 Component::header("content-length"),
688 Component::header("content-type"),
689 ]
690 }
691
692 #[test]
695 fn signature_base_matches_rfc_9421() {
696 let params = SignatureParams {
697 created: Some(1_618_884_473),
698 keyid: Some("test-key-rsa-pss".to_owned()),
699 ..SignatureParams::default()
700 };
701 let base = signature_base(&rfc_request(), &rfc_components(), ¶ms).unwrap();
702
703 let expected = concat!(
704 "\"@method\": POST\n",
705 "\"@authority\": example.com\n",
706 "\"@path\": /foo\n",
707 "\"content-digest\": sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:\n",
708 "\"content-length\": 18\n",
709 "\"content-type\": application/json\n",
710 "\"@signature-params\": (\"@method\" \"@authority\" \"@path\" \"content-digest\" \"content-length\" \"content-type\");created=1618884473;keyid=\"test-key-rsa-pss\""
711 );
712 assert_eq!(base, expected);
713 }
714
715 fn ed25519_params() -> SignatureParams {
716 SignatureParams {
717 created: Some(1_700_000_000),
718 keyid: Some("issuer-key-1".to_owned()),
719 alg: Some(ALG.to_owned()),
720 ..SignatureParams::default()
721 }
722 }
723
724 #[test]
725 fn round_trips() {
726 let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
727 let request = rfc_request();
728 let components = rfc_components();
729 let signed = sign(&request, &components, &ed25519_params(), "sig1", &key).unwrap();
730
731 let verified = verify(
732 &request,
733 &signed.signature_input,
734 &signed.signature,
735 &key.verifying_key(),
736 &VerifyConfig::default(),
737 )
738 .unwrap();
739 assert_eq!(verified.label, "sig1");
740 assert_eq!(verified.components, components);
741 assert_eq!(verified.params.keyid.as_deref(), Some("issuer-key-1"));
742 }
743
744 #[test]
745 fn rejects_a_tampered_request() {
746 let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
747 let mut request = rfc_request();
748 let components = rfc_components();
749 let signed = sign(&request, &components, &ed25519_params(), "sig1", &key).unwrap();
750
751 request
753 .headers
754 .push(("Content-Length".to_owned(), "19".to_owned()));
755 request
756 .headers
757 .retain(|(n, v)| !(n == "Content-Length" && v == "18"));
758
759 let err = verify(
760 &request,
761 &signed.signature_input,
762 &signed.signature,
763 &key.verifying_key(),
764 &VerifyConfig::default(),
765 );
766 assert!(matches!(err, Err(HttpSigError::InvalidSignature)));
767 }
768
769 #[test]
770 fn rejects_the_wrong_key() {
771 let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
772 let other = SigningKey::from_ed25519_seed(&[6u8; 32]);
773 let request = rfc_request();
774 let signed = sign(&request, &rfc_components(), &ed25519_params(), "sig1", &key).unwrap();
775
776 let err = verify(
777 &request,
778 &signed.signature_input,
779 &signed.signature,
780 &other.verifying_key(),
781 &VerifyConfig::default(),
782 );
783 assert!(matches!(err, Err(HttpSigError::InvalidSignature)));
784 }
785
786 #[test]
787 fn rejects_a_missing_covered_header() {
788 let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
789 let request = HttpRequest {
790 headers: vec![],
791 ..rfc_request()
792 };
793 let err = sign(&request, &rfc_components(), &ed25519_params(), "sig1", &key);
794 assert!(matches!(err, Err(HttpSigError::MissingComponent(_))));
795 }
796
797 #[test]
798 fn enforces_expiry() {
799 let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
800 let request = rfc_request();
801 let params = SignatureParams {
802 created: Some(1_700_000_000),
803 expires: Some(1_700_000_300),
804 keyid: Some("k".to_owned()),
805 alg: Some(ALG.to_owned()),
806 ..SignatureParams::default()
807 };
808 let signed = sign(&request, &rfc_components(), ¶ms, "sig1", &key).unwrap();
809
810 let config = VerifyConfig {
811 now: Some(1_700_000_301),
812 ..VerifyConfig::default()
813 };
814 let err = verify(
815 &request,
816 &signed.signature_input,
817 &signed.signature,
818 &key.verifying_key(),
819 &config,
820 );
821 assert!(matches!(err, Err(HttpSigError::Expired)));
822 }
823
824 #[test]
825 fn rejects_a_label_mismatch() {
826 let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
827 let request = rfc_request();
828 let signed = sign(&request, &rfc_components(), &ed25519_params(), "sig1", &key).unwrap();
829
830 let config = VerifyConfig {
831 label: Some("other".to_owned()),
832 ..VerifyConfig::default()
833 };
834 let err = verify(
835 &request,
836 &signed.signature_input,
837 &signed.signature,
838 &key.verifying_key(),
839 &config,
840 );
841 assert!(matches!(err, Err(HttpSigError::LabelMismatch)));
842 }
843
844 #[test]
845 fn is_deterministic() {
846 let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
847 let request = rfc_request();
848 let components = rfc_components();
849 let a = sign(&request, &components, &ed25519_params(), "sig1", &key).unwrap();
850 let b = sign(&request, &components, &ed25519_params(), "sig1", &key).unwrap();
851 assert_eq!(a.signature_input, b.signature_input);
852 assert_eq!(a.signature, b.signature);
853 }
854
855 #[test]
856 fn rejects_a_missing_required_component() {
857 let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
858 let request = rfc_request();
859 let signed = sign(
861 &request,
862 &[Component::Method, Component::Path],
863 &ed25519_params(),
864 "sig1",
865 &key,
866 )
867 .unwrap();
868
869 let config = VerifyConfig {
870 required_components: vec![Component::header("content-digest")],
871 ..VerifyConfig::default()
872 };
873 let err = verify(
874 &request,
875 &signed.signature_input,
876 &signed.signature,
877 &key.verifying_key(),
878 &config,
879 );
880 assert!(matches!(
881 err,
882 Err(HttpSigError::MissingRequiredComponent(_))
883 ));
884 }
885
886 #[test]
887 fn rejects_a_non_ed25519_alg() {
888 let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
889 let request = rfc_request();
890 let params = SignatureParams {
891 created: Some(1_700_000_000),
892 keyid: Some("k".to_owned()),
893 alg: Some("rsa-pss".to_owned()),
894 ..SignatureParams::default()
895 };
896 let signed = sign(&request, &rfc_components(), ¶ms, "sig1", &key).unwrap();
897
898 let err = verify(
899 &request,
900 &signed.signature_input,
901 &signed.signature,
902 &key.verifying_key(),
903 &VerifyConfig::default(),
904 );
905 assert!(matches!(err, Err(HttpSigError::UnsupportedAlg { .. })));
906 }
907
908 #[test]
909 fn rejects_crlf_in_a_covered_header() {
910 let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
911 let mut request = rfc_request();
912 request
913 .headers
914 .push(("X-Evil".to_owned(), "ok\n\"@path\": /evil".to_owned()));
915
916 let err = sign(
917 &request,
918 &[Component::Method, Component::header("x-evil")],
919 &ed25519_params(),
920 "sig1",
921 &key,
922 );
923 assert!(matches!(err, Err(HttpSigError::InvalidComponentValue(_))));
924 }
925
926 #[test]
927 fn rejects_an_inverted_time_window() {
928 let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
929 let request = rfc_request();
930 let params = SignatureParams {
931 created: Some(1_700_000_300),
932 expires: Some(1_700_000_000),
933 keyid: Some("k".to_owned()),
934 alg: Some(ALG.to_owned()),
935 ..SignatureParams::default()
936 };
937 let signed = sign(&request, &rfc_components(), ¶ms, "sig1", &key).unwrap();
938
939 let err = verify(
940 &request,
941 &signed.signature_input,
942 &signed.signature,
943 &key.verifying_key(),
944 &VerifyConfig::default(),
945 );
946 assert!(matches!(err, Err(HttpSigError::InvalidTimeWindow)));
947 }
948
949 #[test]
950 fn enforces_max_age() {
951 let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
952 let request = rfc_request();
953 let signed = sign(&request, &rfc_components(), &ed25519_params(), "sig1", &key).unwrap();
955
956 let config = VerifyConfig {
957 now: Some(1_700_000_400),
958 max_age: Some(60),
959 ..VerifyConfig::default()
960 };
961 let err = verify(
962 &request,
963 &signed.signature_input,
964 &signed.signature,
965 &key.verifying_key(),
966 &config,
967 );
968 assert!(matches!(err, Err(HttpSigError::TooOld)));
969 }
970
971 #[test]
972 fn tolerates_unknown_boolean_parameters() {
973 use base64::{engine::general_purpose::STANDARD, Engine};
974
975 let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
976 let request = rfc_request();
977 let params_value = "(\"@method\" \"@path\");created=1700000000;ext";
979 let base = format!(
980 "\"@method\": {}\n\"@path\": {}\n\"@signature-params\": {params_value}",
981 request.method, request.path,
982 );
983 let signature = STANDARD.encode(key.sign(base.as_bytes()));
984 let signature_input = format!("sig1={params_value}");
985 let signature = format!("sig1=:{signature}:");
986
987 let verified = verify(
988 &request,
989 &signature_input,
990 &signature,
991 &key.verifying_key(),
992 &VerifyConfig::default(),
993 )
994 .unwrap();
995 assert_eq!(verified.params.created, Some(1_700_000_000));
996 }
997
998 #[test]
999 fn content_digest_helper_binds_the_body() {
1000 use crate::message::{content_digest_sha256, verify_content_digest};
1001
1002 let body = br#"{"amount":100}"#;
1003 let header = content_digest_sha256(body);
1004 assert!(verify_content_digest(&header, body));
1005 assert!(!verify_content_digest(&header, b"tampered"));
1006 }
1007
1008 #[test]
1009 fn round_trips_params_with_quoted_delimiters() {
1010 let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
1013 let request = rfc_request();
1014 let params = SignatureParams {
1015 created: Some(1_700_000_000),
1016 keyid: Some("weird;key)with\"quote".to_owned()),
1017 alg: Some(ALG.to_owned()),
1018 ..SignatureParams::default()
1019 };
1020 let signed = sign(&request, &rfc_components(), ¶ms, "sig1", &key).unwrap();
1021
1022 let verified = verify(
1023 &request,
1024 &signed.signature_input,
1025 &signed.signature,
1026 &key.verifying_key(),
1027 &VerifyConfig::default(),
1028 )
1029 .unwrap();
1030 assert_eq!(
1031 verified.params.keyid.as_deref(),
1032 Some("weird;key)with\"quote")
1033 );
1034 }
1035
1036 #[test]
1037 fn rejects_crlf_in_signature_params() {
1038 use base64::{engine::general_purpose::STANDARD, Engine};
1039
1040 let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
1041 let request = rfc_request();
1042 let signature_input = "sig1=(\"@method\")\n;created=1700000000";
1044 let signature = format!("sig1=:{}:", STANDARD.encode([0u8; 64]));
1045
1046 let err = verify(
1047 &request,
1048 signature_input,
1049 &signature,
1050 &key.verifying_key(),
1051 &VerifyConfig::default(),
1052 );
1053 assert!(matches!(err, Err(HttpSigError::Parse(_))));
1054 }
1055
1056 use base64::{engine::general_purpose::STANDARD, Engine};
1057
1058 use super::{check_request_profile, WIMSE_TAG};
1059
1060 fn wimse_params() -> SignatureParams {
1062 SignatureParams {
1063 created: Some(1_700_000_000),
1064 expires: Some(1_700_000_300),
1065 nonce: Some("abcd1111".to_owned()),
1066 tag: Some(WIMSE_TAG.to_owned()),
1067 wimse_aud: Some("https://svcb.example.com/gimme-ice-cream".to_owned()),
1068 ..SignatureParams::default()
1069 }
1070 }
1071
1072 fn wimse_config() -> VerifyConfig {
1073 VerifyConfig {
1074 wimse_profile: true,
1075 ..VerifyConfig::default()
1076 }
1077 }
1078
1079 fn sign_with(params: &SignatureParams) -> (SigningKey, HttpRequest, super::SignedSignature) {
1080 let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
1081 let request = rfc_request();
1082 let signed = sign(&request, &rfc_components(), params, "wimse", &key).unwrap();
1083 (key, request, signed)
1084 }
1085
1086 #[test]
1087 fn accepts_a_profile_conforming_signature() {
1088 let (key, request, signed) = sign_with(&wimse_params());
1089 let verified = verify(
1090 &request,
1091 &signed.signature_input,
1092 &signed.signature,
1093 &key.verifying_key(),
1094 &wimse_config(),
1095 )
1096 .unwrap();
1097 assert_eq!(verified.params.tag.as_deref(), Some(WIMSE_TAG));
1098 }
1099
1100 #[test]
1103 fn profile_rejects_keyid_and_alg() {
1104 for (label, params) in [
1105 (
1106 "keyid",
1107 SignatureParams {
1108 keyid: Some("k".to_owned()),
1109 ..wimse_params()
1110 },
1111 ),
1112 (
1113 "alg",
1114 SignatureParams {
1115 alg: Some(ALG.to_owned()),
1116 ..wimse_params()
1117 },
1118 ),
1119 ] {
1120 let (key, request, signed) = sign_with(¶ms);
1121 let err = verify(
1122 &request,
1123 &signed.signature_input,
1124 &signed.signature,
1125 &key.verifying_key(),
1126 &wimse_config(),
1127 );
1128 assert!(
1129 matches!(err, Err(HttpSigError::ForbiddenParameter(p)) if p == label),
1130 "expected `{label}` to be rejected, got {err:?}"
1131 );
1132 }
1133 }
1134
1135 #[test]
1136 fn profile_requires_the_mandatory_parameters() {
1137 let cases: [(&str, SignatureParams); 5] = [
1138 (
1139 "created",
1140 SignatureParams {
1141 created: None,
1142 ..wimse_params()
1143 },
1144 ),
1145 (
1146 "expires",
1147 SignatureParams {
1148 expires: None,
1149 ..wimse_params()
1150 },
1151 ),
1152 (
1153 "nonce",
1154 SignatureParams {
1155 nonce: None,
1156 ..wimse_params()
1157 },
1158 ),
1159 (
1160 "tag",
1161 SignatureParams {
1162 tag: None,
1163 ..wimse_params()
1164 },
1165 ),
1166 (
1167 "wimse-aud",
1168 SignatureParams {
1169 wimse_aud: None,
1170 ..wimse_params()
1171 },
1172 ),
1173 ];
1174 for (name, params) in cases {
1175 let err = check_request_profile(¶ms);
1176 assert!(
1177 matches!(err, Err(HttpSigError::MissingParameter(p)) if p == name),
1178 "expected `{name}` to be required, got {err:?}"
1179 );
1180 }
1181 }
1182
1183 #[test]
1184 fn profile_rejects_a_foreign_tag() {
1185 let params = SignatureParams {
1186 tag: Some("something-else".to_owned()),
1187 ..wimse_params()
1188 };
1189 let (key, request, signed) = sign_with(¶ms);
1190 let err = verify(
1191 &request,
1192 &signed.signature_input,
1193 &signed.signature,
1194 &key.verifying_key(),
1195 &wimse_config(),
1196 );
1197 assert!(matches!(err, Err(HttpSigError::WrongTag { .. })));
1198 }
1199
1200 #[test]
1203 fn rejects_a_signature_minted_for_another_audience() {
1204 let (key, request, signed) = sign_with(&wimse_params());
1205 let config = VerifyConfig {
1206 expected_audience: Some("https://svcc.example.com/other".to_owned()),
1207 ..wimse_config()
1208 };
1209 let err = verify(
1210 &request,
1211 &signed.signature_input,
1212 &signed.signature,
1213 &key.verifying_key(),
1214 &config,
1215 );
1216 assert!(matches!(err, Err(HttpSigError::AudienceMismatch)));
1217 }
1218
1219 #[test]
1224 fn reports_a_forgery_as_invalid_regardless_of_audience() {
1225 let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
1226 let request = rfc_request();
1227 let (_, _, signed) = sign_with(&wimse_params());
1228 let forged = format!("wimse=:{}:", STANDARD.encode([0u8; 64]));
1229
1230 for audience in [
1231 "https://svcb.example.com/gimme-ice-cream",
1232 "https://wrong.example/inbox",
1233 ] {
1234 let config = VerifyConfig {
1235 expected_audience: Some(audience.to_owned()),
1236 ..wimse_config()
1237 };
1238 let err = verify(
1239 &request,
1240 &signed.signature_input,
1241 &forged,
1242 &key.verifying_key(),
1243 &config,
1244 );
1245 assert!(
1246 matches!(err, Err(HttpSigError::InvalidSignature)),
1247 "expected InvalidSignature for audience {audience}, got {err:?}"
1248 );
1249 }
1250 }
1251
1252 #[test]
1253 fn accepts_the_matching_audience() {
1254 let (key, request, signed) = sign_with(&wimse_params());
1255 let config = VerifyConfig {
1256 expected_audience: Some("https://svcb.example.com/gimme-ice-cream".to_owned()),
1257 ..wimse_config()
1258 };
1259 assert!(verify(
1260 &request,
1261 &signed.signature_input,
1262 &signed.signature,
1263 &key.verifying_key(),
1264 &config,
1265 )
1266 .is_ok());
1267 }
1268
1269 #[test]
1270 fn serializes_sign_response_as_a_bare_boolean() {
1271 let params = SignatureParams {
1272 wimse_sign_response: Some(true),
1273 ..wimse_params()
1274 };
1275 let (key, request, signed) = sign_with(¶ms);
1276 assert!(signed.signature_input.ends_with(";wimse-sign-response"));
1277
1278 let verified = verify(
1279 &request,
1280 &signed.signature_input,
1281 &signed.signature,
1282 &key.verifying_key(),
1283 &wimse_config(),
1284 )
1285 .unwrap();
1286 assert_eq!(verified.params.wimse_sign_response, Some(true));
1287 }
1288
1289 #[test]
1290 fn round_trips_an_explicit_false_sign_response() {
1291 let params = SignatureParams {
1292 wimse_sign_response: Some(false),
1293 ..wimse_params()
1294 };
1295 let (key, request, signed) = sign_with(¶ms);
1296 assert!(signed.signature_input.ends_with(";wimse-sign-response=?0"));
1297
1298 let verified = verify(
1299 &request,
1300 &signed.signature_input,
1301 &signed.signature,
1302 &key.verifying_key(),
1303 &wimse_config(),
1304 )
1305 .unwrap();
1306 assert_eq!(verified.params.wimse_sign_response, Some(false));
1307 }
1308
1309 #[test]
1310 fn round_trips_the_response_nonce_binding() {
1311 let params = SignatureParams {
1312 wimse_req_nonce: Some("abcd1111".to_owned()),
1313 ..wimse_params()
1314 };
1315 let (key, request, signed) = sign_with(¶ms);
1316 let verified = verify(
1317 &request,
1318 &signed.signature_input,
1319 &signed.signature,
1320 &key.verifying_key(),
1321 &wimse_config(),
1322 )
1323 .unwrap();
1324 assert_eq!(verified.params.wimse_req_nonce.as_deref(), Some("abcd1111"));
1325 }
1326
1327 #[test]
1330 fn profile_is_off_by_default() {
1331 let (key, request, signed) = sign_with(&ed25519_params());
1332 assert!(verify(
1333 &request,
1334 &signed.signature_input,
1335 &signed.signature,
1336 &key.verifying_key(),
1337 &VerifyConfig::default(),
1338 )
1339 .is_ok());
1340 }
1341
1342 use crate::message::HttpResponse;
1343
1344 fn rfc_response() -> HttpResponse {
1345 HttpResponse {
1346 status: 200,
1347 headers: vec![
1348 ("Content-Type".to_owned(), "application/json".to_owned()),
1349 (
1350 "Workload-Identity-Token".to_owned(),
1351 "eyJ0eXAi.wit.value".to_owned(),
1352 ),
1353 ],
1354 }
1355 }
1356
1357 fn response_params() -> SignatureParams {
1358 SignatureParams {
1359 created: Some(1_700_000_000),
1360 expires: Some(1_700_000_300),
1361 nonce: Some("resp-2222".to_owned()),
1362 tag: Some(WIMSE_TAG.to_owned()),
1363 wimse_req_nonce: Some("abcd1111".to_owned()),
1364 ..SignatureParams::default()
1365 }
1366 }
1367
1368 fn response_config() -> VerifyConfig {
1369 VerifyConfig {
1370 wimse_response_profile: true,
1371 expected_req_nonce: Some("abcd1111".to_owned()),
1372 ..VerifyConfig::default()
1373 }
1374 }
1375
1376 #[test]
1377 fn signs_and_verifies_a_response() {
1378 let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
1379 let request = rfc_request();
1380 let response = rfc_response();
1381 let exchange = HttpExchange {
1382 response: &response,
1383 request: &request,
1384 };
1385 let components = super::response_components(&response.headers);
1386 let signed = sign(&exchange, &components, &response_params(), "wimse", &key).unwrap();
1387
1388 assert!(signed
1389 .signature_input
1390 .contains(r#""@status" "@method";req "@request-target";req"#));
1391
1392 let verified = verify(
1393 &exchange,
1394 &signed.signature_input,
1395 &signed.signature,
1396 &key.verifying_key(),
1397 &response_config(),
1398 )
1399 .unwrap();
1400 assert_eq!(verified.params.wimse_req_nonce.as_deref(), Some("abcd1111"));
1401 }
1402
1403 #[test]
1404 fn a_response_cannot_be_lifted_onto_another_request() {
1405 let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
1406 let request = rfc_request();
1407 let response = rfc_response();
1408 let signed = sign(
1409 &HttpExchange {
1410 response: &response,
1411 request: &request,
1412 },
1413 &super::response_components(&response.headers),
1414 &response_params(),
1415 "wimse",
1416 &key,
1417 )
1418 .unwrap();
1419
1420 let other_request = HttpRequest {
1421 path: "/admin".to_owned(),
1422 ..rfc_request()
1423 };
1424 let err = verify(
1425 &HttpExchange {
1426 response: &response,
1427 request: &other_request,
1428 },
1429 &signed.signature_input,
1430 &signed.signature,
1431 &key.verifying_key(),
1432 &response_config(),
1433 );
1434 assert!(matches!(err, Err(HttpSigError::InvalidSignature)));
1435 }
1436
1437 #[test]
1438 fn rejects_a_response_answering_a_different_request() {
1439 let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
1440 let request = rfc_request();
1441 let response = rfc_response();
1442 let exchange = HttpExchange {
1443 response: &response,
1444 request: &request,
1445 };
1446 let signed = sign(
1447 &exchange,
1448 &super::response_components(&response.headers),
1449 &response_params(),
1450 "wimse",
1451 &key,
1452 )
1453 .unwrap();
1454
1455 let config = VerifyConfig {
1456 expected_req_nonce: Some("some-other-nonce".to_owned()),
1457 ..response_config()
1458 };
1459 let err = verify(
1460 &exchange,
1461 &signed.signature_input,
1462 &signed.signature,
1463 &key.verifying_key(),
1464 &config,
1465 );
1466 assert!(matches!(err, Err(HttpSigError::RequestNonceMismatch)));
1467 }
1468
1469 #[test]
1470 fn response_profile_requires_the_returned_nonce() {
1471 let params = SignatureParams {
1472 wimse_req_nonce: None,
1473 ..response_params()
1474 };
1475 assert!(matches!(
1476 super::check_response_profile(¶ms, true),
1477 Err(HttpSigError::MissingParameter("wimse-req-nonce"))
1478 ));
1479 assert!(super::check_response_profile(¶ms, false).is_ok());
1481 }
1482
1483 #[test]
1486 fn response_profile_forbids_the_request_audience() {
1487 let params = SignatureParams {
1488 wimse_aud: Some("https://svcb.example.com/x".to_owned()),
1489 ..response_params()
1490 };
1491 assert!(matches!(
1492 super::check_response_profile(¶ms, true),
1493 Err(HttpSigError::ForbiddenParameter("wimse-aud"))
1494 ));
1495 }
1496
1497 #[test]
1498 fn a_request_has_no_status_component() {
1499 let err = rfc_request().component_value(&Component::Status);
1500 assert!(matches!(err, Err(HttpSigError::UnsupportedComponent(_))));
1501 }
1502
1503 #[test]
1504 fn rejects_max_age_without_now() {
1505 let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
1506 let request = rfc_request();
1507 let signed = sign(&request, &rfc_components(), &ed25519_params(), "sig1", &key).unwrap();
1508
1509 let config = VerifyConfig {
1511 max_age: Some(60),
1512 ..VerifyConfig::default()
1513 };
1514 let err = verify(
1515 &request,
1516 &signed.signature_input,
1517 &signed.signature,
1518 &key.verifying_key(),
1519 &config,
1520 );
1521 assert!(matches!(err, Err(HttpSigError::TooOld)));
1522 }
1523}