1use std::fmt::Write as _;
5
6use base64::{engine::general_purpose::STANDARD, Engine};
7use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
8
9use crate::error::HttpSigError;
10use crate::message::{Component, HttpRequest};
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}
112
113pub fn check_request_profile(params: &SignatureParams) -> Result<(), HttpSigError> {
126 if params.keyid.is_some() {
127 return Err(HttpSigError::ForbiddenParameter("keyid"));
128 }
129 if params.alg.is_some() {
130 return Err(HttpSigError::ForbiddenParameter("alg"));
131 }
132 if params.created.is_none() {
133 return Err(HttpSigError::MissingParameter("created"));
134 }
135 if params.expires.is_none() {
136 return Err(HttpSigError::MissingParameter("expires"));
137 }
138 if params.nonce.is_none() {
139 return Err(HttpSigError::MissingParameter("nonce"));
140 }
141 match params.tag.as_deref() {
142 None => return Err(HttpSigError::MissingParameter("tag")),
143 Some(tag) if tag != WIMSE_TAG => {
144 return Err(HttpSigError::WrongTag {
145 found: tag.to_owned(),
146 })
147 }
148 Some(_) => {}
149 }
150 if params.wimse_aud.is_none() {
151 return Err(HttpSigError::MissingParameter("wimse-aud"));
152 }
153 Ok(())
154}
155
156fn sf_string(value: &str) -> String {
157 let mut out = String::with_capacity(value.len() + 2);
158 out.push('"');
159 for c in value.chars() {
160 if c == '\\' || c == '"' {
161 out.push('\\');
162 }
163 out.push(c);
164 }
165 out.push('"');
166 out
167}
168
169fn serialize_params_value(components: &[Component], params: &SignatureParams) -> String {
170 let inner = components
171 .iter()
172 .map(Component::quoted_id)
173 .collect::<Vec<_>>()
174 .join(" ");
175 let mut s = format!("({inner})");
176 if let Some(created) = params.created {
177 let _ = write!(s, ";created={created}");
178 }
179 if let Some(expires) = params.expires {
180 let _ = write!(s, ";expires={expires}");
181 }
182 if let Some(keyid) = ¶ms.keyid {
183 let _ = write!(s, ";keyid={}", sf_string(keyid));
184 }
185 if let Some(alg) = ¶ms.alg {
186 let _ = write!(s, ";alg={}", sf_string(alg));
187 }
188 if let Some(nonce) = ¶ms.nonce {
189 let _ = write!(s, ";nonce={}", sf_string(nonce));
190 }
191 if let Some(tag) = ¶ms.tag {
192 let _ = write!(s, ";tag={}", sf_string(tag));
193 }
194 if let Some(aud) = ¶ms.wimse_aud {
195 let _ = write!(s, ";wimse-aud={}", sf_string(aud));
196 }
197 if let Some(sign_response) = params.wimse_sign_response {
198 if sign_response {
201 s.push_str(";wimse-sign-response");
202 } else {
203 s.push_str(";wimse-sign-response=?0");
204 }
205 }
206 if let Some(req_nonce) = ¶ms.wimse_req_nonce {
207 let _ = write!(s, ";wimse-req-nonce={}", sf_string(req_nonce));
208 }
209 s
210}
211
212fn signature_base_from_params_str(
213 request: &HttpRequest,
214 components: &[Component],
215 params_value: &str,
216) -> Result<String, HttpSigError> {
217 if params_value.contains(['\r', '\n']) {
220 return Err(HttpSigError::Parse(
221 "signature parameters contain CR or LF".to_owned(),
222 ));
223 }
224 let mut base = String::new();
225 for component in components {
226 let value = request.component_value(component)?;
227 if value.contains(['\r', '\n']) {
229 return Err(HttpSigError::InvalidComponentValue(component.quoted_id()));
230 }
231 base.push_str(&component.quoted_id());
232 base.push_str(": ");
233 base.push_str(&value);
234 base.push('\n');
235 }
236 base.push_str("\"@signature-params\": ");
237 base.push_str(params_value);
238 Ok(base)
239}
240
241pub fn signature_base(
248 request: &HttpRequest,
249 components: &[Component],
250 params: &SignatureParams,
251) -> Result<String, HttpSigError> {
252 let params_value = serialize_params_value(components, params);
253 signature_base_from_params_str(request, components, ¶ms_value)
254}
255
256pub fn sign(
263 request: &HttpRequest,
264 components: &[Component],
265 params: &SignatureParams,
266 label: &str,
267 signing_key: &SigningKey,
268) -> Result<SignedSignature, HttpSigError> {
269 let params_value = serialize_params_value(components, params);
270 let base = signature_base_from_params_str(request, components, ¶ms_value)?;
271 let signature: Signature = signing_key.sign(base.as_bytes());
272 Ok(SignedSignature {
273 signature_input: format!("{label}={params_value}"),
274 signature: format!("{label}=:{}:", STANDARD.encode(signature.to_bytes())),
275 })
276}
277
278fn split_member(value: &str) -> Result<(&str, &str), HttpSigError> {
280 let value = value.trim();
281 let eq = value
282 .find('=')
283 .ok_or_else(|| HttpSigError::Parse("missing `=` in dictionary member".to_owned()))?;
284 let label = value[..eq].trim();
285 if label.is_empty() {
286 return Err(HttpSigError::Parse("empty signature label".to_owned()));
287 }
288 Ok((label, &value[eq + 1..]))
289}
290
291fn parse_sf_string(token: &str) -> Result<String, HttpSigError> {
292 let inner = token
293 .strip_prefix('"')
294 .and_then(|t| t.strip_suffix('"'))
295 .ok_or_else(|| HttpSigError::Parse(format!("not a string: {token}")))?;
296 let mut out = String::with_capacity(inner.len());
297 let mut chars = inner.chars();
298 while let Some(c) = chars.next() {
299 if c == '\\' {
300 match chars.next() {
301 Some(next @ ('\\' | '"')) => out.push(next),
302 _ => return Err(HttpSigError::Parse("bad string escape".to_owned())),
303 }
304 } else {
305 out.push(c);
306 }
307 }
308 Ok(out)
309}
310
311fn find_unquoted(s: &str, target: char) -> Option<usize> {
314 let mut in_quotes = false;
315 let mut escaped = false;
316 for (idx, c) in s.char_indices() {
317 if escaped {
318 escaped = false;
319 } else if in_quotes && c == '\\' {
320 escaped = true;
321 } else if c == '"' {
322 in_quotes = !in_quotes;
323 } else if c == target && !in_quotes {
324 return Some(idx);
325 }
326 }
327 None
328}
329
330fn split_unquoted_semicolons(s: &str) -> Vec<&str> {
332 let mut parts = Vec::new();
333 let mut start = 0;
334 let mut in_quotes = false;
335 let mut escaped = false;
336 for (idx, c) in s.char_indices() {
337 if escaped {
338 escaped = false;
339 } else if in_quotes && c == '\\' {
340 escaped = true;
341 } else if c == '"' {
342 in_quotes = !in_quotes;
343 } else if c == ';' && !in_quotes {
344 parts.push(&s[start..idx]);
345 start = idx + 1;
346 }
347 }
348 parts.push(&s[start..]);
349 parts
350}
351
352fn parse_params(rest: &str, params: &mut SignatureParams) -> Result<(), HttpSigError> {
353 for part in split_unquoted_semicolons(rest) {
354 let part = part.trim();
355 if part.is_empty() {
356 continue;
357 }
358 let Some((name, raw)) = part.split_once('=') else {
359 if part == "wimse-sign-response" {
362 params.wimse_sign_response = Some(true);
363 }
364 continue;
365 };
366 let name = name.trim();
367 let raw = raw.trim();
368 match name {
369 "created" => {
370 params.created = Some(parse_int(raw)?);
371 }
372 "expires" => {
373 params.expires = Some(parse_int(raw)?);
374 }
375 "keyid" => params.keyid = Some(parse_sf_string(raw)?),
376 "alg" => params.alg = Some(parse_sf_string(raw)?),
377 "nonce" => params.nonce = Some(parse_sf_string(raw)?),
378 "tag" => params.tag = Some(parse_sf_string(raw)?),
379 "wimse-aud" => params.wimse_aud = Some(parse_sf_string(raw)?),
380 "wimse-sign-response" => params.wimse_sign_response = Some(parse_sf_boolean(raw)?),
381 "wimse-req-nonce" => params.wimse_req_nonce = Some(parse_sf_string(raw)?),
382 _ => {}
384 }
385 }
386 Ok(())
387}
388
389fn parse_sf_boolean(raw: &str) -> Result<bool, HttpSigError> {
391 match raw {
392 "?1" => Ok(true),
393 "?0" => Ok(false),
394 other => Err(HttpSigError::Parse(format!("not a boolean: {other}"))),
395 }
396}
397
398fn parse_int(raw: &str) -> Result<u64, HttpSigError> {
399 raw.trim()
400 .parse()
401 .map_err(|_| HttpSigError::Parse(format!("not an integer: {raw}")))
402}
403
404fn parse_signature_input(
407 value: &str,
408) -> Result<(String, Vec<Component>, SignatureParams, String), HttpSigError> {
409 let (label, rest) = split_member(value)?;
410 let rest = rest.trim();
411 if !rest.starts_with('(') {
412 return Err(HttpSigError::Parse(
413 "inner list must start with `(`".to_owned(),
414 ));
415 }
416 let close = find_unquoted(rest, ')')
418 .ok_or_else(|| HttpSigError::Parse("missing `)` in inner list".to_owned()))?;
419 let inner = &rest[1..close];
420
421 let mut components = Vec::new();
422 for token in inner.split_whitespace() {
423 components.push(Component::from_quoted_id(token)?);
424 }
425
426 let mut params = SignatureParams::default();
427 parse_params(&rest[close + 1..], &mut params)?;
428
429 Ok((label.to_owned(), components, params, rest.to_owned()))
430}
431
432fn parse_signature(value: &str) -> Result<(String, [u8; 64]), HttpSigError> {
434 let (label, rest) = split_member(value)?;
435 let b64 = rest
436 .trim()
437 .strip_prefix(':')
438 .and_then(|t| t.strip_suffix(':'))
439 .ok_or_else(|| HttpSigError::Parse("byte sequence must be wrapped in `:`".to_owned()))?;
440 let bytes = STANDARD
441 .decode(b64)
442 .map_err(|_| HttpSigError::MalformedSignature)?;
443 let array: [u8; 64] = bytes
444 .try_into()
445 .map_err(|_| HttpSigError::MalformedSignature)?;
446 Ok((label.to_owned(), array))
447}
448
449pub fn verify(
472 request: &HttpRequest,
473 signature_input: &str,
474 signature: &str,
475 verifying_key: &VerifyingKey,
476 config: &VerifyConfig,
477) -> Result<VerifiedSignature, HttpSigError> {
478 let (input_label, components, params, params_value) = parse_signature_input(signature_input)?;
479 let (sig_label, sig_bytes) = parse_signature(signature)?;
480
481 if input_label != sig_label {
482 return Err(HttpSigError::LabelMismatch);
483 }
484 if let Some(expected) = &config.label {
485 if expected != &input_label {
486 return Err(HttpSigError::LabelMismatch);
487 }
488 }
489 if config.wimse_profile {
490 check_request_profile(¶ms)?;
491 }
492 if let Some(alg) = ¶ms.alg {
493 if alg != ALG {
494 return Err(HttpSigError::UnsupportedAlg { found: alg.clone() });
495 }
496 }
497
498 let base = signature_base_from_params_str(request, &components, ¶ms_value)?;
499 let signature = Signature::from_bytes(&sig_bytes);
500 verifying_key
501 .verify_strict(base.as_bytes(), &signature)
502 .map_err(|_| HttpSigError::InvalidSignature)?;
503
504 for required in &config.required_components {
505 if !components.contains(required) {
506 return Err(HttpSigError::MissingRequiredComponent(required.quoted_id()));
507 }
508 }
509 if let Some(expected) = &config.expected_audience {
514 if params.wimse_aud.as_ref() != Some(expected) {
515 return Err(HttpSigError::AudienceMismatch);
516 }
517 }
518
519 if let (Some(created), Some(expires)) = (params.created, params.expires) {
520 if expires < created {
521 return Err(HttpSigError::InvalidTimeWindow);
522 }
523 }
524 if config.max_age.is_some() && config.now.is_none() {
527 return Err(HttpSigError::TooOld);
528 }
529 if let Some(now) = config.now {
530 if let Some(expires) = params.expires {
531 if now > expires.saturating_add(config.leeway) {
532 return Err(HttpSigError::Expired);
533 }
534 }
535 if let Some(created) = params.created {
536 if created > now.saturating_add(config.leeway) {
537 return Err(HttpSigError::CreatedInFuture);
538 }
539 }
540 if let Some(max_age) = config.max_age {
541 let created = params.created.ok_or(HttpSigError::TooOld)?;
542 if now.saturating_sub(created) > max_age {
543 return Err(HttpSigError::TooOld);
544 }
545 }
546 }
547
548 Ok(VerifiedSignature {
549 label: input_label,
550 components,
551 params,
552 })
553}
554
555#[cfg(test)]
556mod tests {
557 use ed25519_dalek::SigningKey;
558
559 use super::{sign, signature_base, verify, SignatureParams, VerifyConfig, ALG};
560 use crate::error::HttpSigError;
561 use crate::message::{Component, HttpRequest};
562
563 fn rfc_request() -> HttpRequest {
565 HttpRequest {
566 method: "POST".to_owned(),
567 authority: "example.com".to_owned(),
568 path: "/foo".to_owned(),
569 query: Some("param=Value&Pet=dog".to_owned()),
570 headers: vec![
571 ("Host".to_owned(), "example.com".to_owned()),
572 ("Date".to_owned(), "Tue, 20 Apr 2021 02:07:55 GMT".to_owned()),
573 ("Content-Type".to_owned(), "application/json".to_owned()),
574 (
575 "Content-Digest".to_owned(),
576 "sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:".to_owned(),
577 ),
578 ("Content-Length".to_owned(), "18".to_owned()),
579 ],
580 }
581 }
582
583 fn rfc_components() -> Vec<Component> {
584 vec![
585 Component::Method,
586 Component::Authority,
587 Component::Path,
588 Component::header("content-digest"),
589 Component::header("content-length"),
590 Component::header("content-type"),
591 ]
592 }
593
594 #[test]
597 fn signature_base_matches_rfc_9421() {
598 let params = SignatureParams {
599 created: Some(1_618_884_473),
600 keyid: Some("test-key-rsa-pss".to_owned()),
601 ..SignatureParams::default()
602 };
603 let base = signature_base(&rfc_request(), &rfc_components(), ¶ms).unwrap();
604
605 let expected = concat!(
606 "\"@method\": POST\n",
607 "\"@authority\": example.com\n",
608 "\"@path\": /foo\n",
609 "\"content-digest\": sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:\n",
610 "\"content-length\": 18\n",
611 "\"content-type\": application/json\n",
612 "\"@signature-params\": (\"@method\" \"@authority\" \"@path\" \"content-digest\" \"content-length\" \"content-type\");created=1618884473;keyid=\"test-key-rsa-pss\""
613 );
614 assert_eq!(base, expected);
615 }
616
617 fn ed25519_params() -> SignatureParams {
618 SignatureParams {
619 created: Some(1_700_000_000),
620 keyid: Some("issuer-key-1".to_owned()),
621 alg: Some(ALG.to_owned()),
622 ..SignatureParams::default()
623 }
624 }
625
626 #[test]
627 fn round_trips() {
628 let key = SigningKey::from_bytes(&[5u8; 32]);
629 let request = rfc_request();
630 let components = rfc_components();
631 let signed = sign(&request, &components, &ed25519_params(), "sig1", &key).unwrap();
632
633 let verified = verify(
634 &request,
635 &signed.signature_input,
636 &signed.signature,
637 &key.verifying_key(),
638 &VerifyConfig::default(),
639 )
640 .unwrap();
641 assert_eq!(verified.label, "sig1");
642 assert_eq!(verified.components, components);
643 assert_eq!(verified.params.keyid.as_deref(), Some("issuer-key-1"));
644 }
645
646 #[test]
647 fn rejects_a_tampered_request() {
648 let key = SigningKey::from_bytes(&[5u8; 32]);
649 let mut request = rfc_request();
650 let components = rfc_components();
651 let signed = sign(&request, &components, &ed25519_params(), "sig1", &key).unwrap();
652
653 request
655 .headers
656 .push(("Content-Length".to_owned(), "19".to_owned()));
657 request
658 .headers
659 .retain(|(n, v)| !(n == "Content-Length" && v == "18"));
660
661 let err = verify(
662 &request,
663 &signed.signature_input,
664 &signed.signature,
665 &key.verifying_key(),
666 &VerifyConfig::default(),
667 );
668 assert!(matches!(err, Err(HttpSigError::InvalidSignature)));
669 }
670
671 #[test]
672 fn rejects_the_wrong_key() {
673 let key = SigningKey::from_bytes(&[5u8; 32]);
674 let other = SigningKey::from_bytes(&[6u8; 32]);
675 let request = rfc_request();
676 let signed = sign(&request, &rfc_components(), &ed25519_params(), "sig1", &key).unwrap();
677
678 let err = verify(
679 &request,
680 &signed.signature_input,
681 &signed.signature,
682 &other.verifying_key(),
683 &VerifyConfig::default(),
684 );
685 assert!(matches!(err, Err(HttpSigError::InvalidSignature)));
686 }
687
688 #[test]
689 fn rejects_a_missing_covered_header() {
690 let key = SigningKey::from_bytes(&[5u8; 32]);
691 let request = HttpRequest {
692 headers: vec![],
693 ..rfc_request()
694 };
695 let err = sign(&request, &rfc_components(), &ed25519_params(), "sig1", &key);
696 assert!(matches!(err, Err(HttpSigError::MissingComponent(_))));
697 }
698
699 #[test]
700 fn enforces_expiry() {
701 let key = SigningKey::from_bytes(&[5u8; 32]);
702 let request = rfc_request();
703 let params = SignatureParams {
704 created: Some(1_700_000_000),
705 expires: Some(1_700_000_300),
706 keyid: Some("k".to_owned()),
707 alg: Some(ALG.to_owned()),
708 ..SignatureParams::default()
709 };
710 let signed = sign(&request, &rfc_components(), ¶ms, "sig1", &key).unwrap();
711
712 let config = VerifyConfig {
713 now: Some(1_700_000_301),
714 ..VerifyConfig::default()
715 };
716 let err = verify(
717 &request,
718 &signed.signature_input,
719 &signed.signature,
720 &key.verifying_key(),
721 &config,
722 );
723 assert!(matches!(err, Err(HttpSigError::Expired)));
724 }
725
726 #[test]
727 fn rejects_a_label_mismatch() {
728 let key = SigningKey::from_bytes(&[5u8; 32]);
729 let request = rfc_request();
730 let signed = sign(&request, &rfc_components(), &ed25519_params(), "sig1", &key).unwrap();
731
732 let config = VerifyConfig {
733 label: Some("other".to_owned()),
734 ..VerifyConfig::default()
735 };
736 let err = verify(
737 &request,
738 &signed.signature_input,
739 &signed.signature,
740 &key.verifying_key(),
741 &config,
742 );
743 assert!(matches!(err, Err(HttpSigError::LabelMismatch)));
744 }
745
746 #[test]
747 fn is_deterministic() {
748 let key = SigningKey::from_bytes(&[5u8; 32]);
749 let request = rfc_request();
750 let components = rfc_components();
751 let a = sign(&request, &components, &ed25519_params(), "sig1", &key).unwrap();
752 let b = sign(&request, &components, &ed25519_params(), "sig1", &key).unwrap();
753 assert_eq!(a.signature_input, b.signature_input);
754 assert_eq!(a.signature, b.signature);
755 }
756
757 #[test]
758 fn rejects_a_missing_required_component() {
759 let key = SigningKey::from_bytes(&[5u8; 32]);
760 let request = rfc_request();
761 let signed = sign(
763 &request,
764 &[Component::Method, Component::Path],
765 &ed25519_params(),
766 "sig1",
767 &key,
768 )
769 .unwrap();
770
771 let config = VerifyConfig {
772 required_components: vec![Component::header("content-digest")],
773 ..VerifyConfig::default()
774 };
775 let err = verify(
776 &request,
777 &signed.signature_input,
778 &signed.signature,
779 &key.verifying_key(),
780 &config,
781 );
782 assert!(matches!(
783 err,
784 Err(HttpSigError::MissingRequiredComponent(_))
785 ));
786 }
787
788 #[test]
789 fn rejects_a_non_ed25519_alg() {
790 let key = SigningKey::from_bytes(&[5u8; 32]);
791 let request = rfc_request();
792 let params = SignatureParams {
793 created: Some(1_700_000_000),
794 keyid: Some("k".to_owned()),
795 alg: Some("rsa-pss".to_owned()),
796 ..SignatureParams::default()
797 };
798 let signed = sign(&request, &rfc_components(), ¶ms, "sig1", &key).unwrap();
799
800 let err = verify(
801 &request,
802 &signed.signature_input,
803 &signed.signature,
804 &key.verifying_key(),
805 &VerifyConfig::default(),
806 );
807 assert!(matches!(err, Err(HttpSigError::UnsupportedAlg { .. })));
808 }
809
810 #[test]
811 fn rejects_crlf_in_a_covered_header() {
812 let key = SigningKey::from_bytes(&[5u8; 32]);
813 let mut request = rfc_request();
814 request
815 .headers
816 .push(("X-Evil".to_owned(), "ok\n\"@path\": /evil".to_owned()));
817
818 let err = sign(
819 &request,
820 &[Component::Method, Component::header("x-evil")],
821 &ed25519_params(),
822 "sig1",
823 &key,
824 );
825 assert!(matches!(err, Err(HttpSigError::InvalidComponentValue(_))));
826 }
827
828 #[test]
829 fn rejects_an_inverted_time_window() {
830 let key = SigningKey::from_bytes(&[5u8; 32]);
831 let request = rfc_request();
832 let params = SignatureParams {
833 created: Some(1_700_000_300),
834 expires: Some(1_700_000_000),
835 keyid: Some("k".to_owned()),
836 alg: Some(ALG.to_owned()),
837 ..SignatureParams::default()
838 };
839 let signed = sign(&request, &rfc_components(), ¶ms, "sig1", &key).unwrap();
840
841 let err = verify(
842 &request,
843 &signed.signature_input,
844 &signed.signature,
845 &key.verifying_key(),
846 &VerifyConfig::default(),
847 );
848 assert!(matches!(err, Err(HttpSigError::InvalidTimeWindow)));
849 }
850
851 #[test]
852 fn enforces_max_age() {
853 let key = SigningKey::from_bytes(&[5u8; 32]);
854 let request = rfc_request();
855 let signed = sign(&request, &rfc_components(), &ed25519_params(), "sig1", &key).unwrap();
857
858 let config = VerifyConfig {
859 now: Some(1_700_000_400),
860 max_age: Some(60),
861 ..VerifyConfig::default()
862 };
863 let err = verify(
864 &request,
865 &signed.signature_input,
866 &signed.signature,
867 &key.verifying_key(),
868 &config,
869 );
870 assert!(matches!(err, Err(HttpSigError::TooOld)));
871 }
872
873 #[test]
874 fn tolerates_unknown_boolean_parameters() {
875 use base64::{engine::general_purpose::STANDARD, Engine};
876 use ed25519_dalek::Signer;
877
878 let key = SigningKey::from_bytes(&[5u8; 32]);
879 let request = rfc_request();
880 let params_value = "(\"@method\" \"@path\");created=1700000000;ext";
882 let base = format!(
883 "\"@method\": {}\n\"@path\": {}\n\"@signature-params\": {params_value}",
884 request.method, request.path,
885 );
886 let signature = STANDARD.encode(key.sign(base.as_bytes()).to_bytes());
887 let signature_input = format!("sig1={params_value}");
888 let signature = format!("sig1=:{signature}:");
889
890 let verified = verify(
891 &request,
892 &signature_input,
893 &signature,
894 &key.verifying_key(),
895 &VerifyConfig::default(),
896 )
897 .unwrap();
898 assert_eq!(verified.params.created, Some(1_700_000_000));
899 }
900
901 #[test]
902 fn content_digest_helper_binds_the_body() {
903 use crate::message::{content_digest_sha256, verify_content_digest};
904
905 let body = br#"{"amount":100}"#;
906 let header = content_digest_sha256(body);
907 assert!(verify_content_digest(&header, body));
908 assert!(!verify_content_digest(&header, b"tampered"));
909 }
910
911 #[test]
912 fn round_trips_params_with_quoted_delimiters() {
913 let key = SigningKey::from_bytes(&[5u8; 32]);
916 let request = rfc_request();
917 let params = SignatureParams {
918 created: Some(1_700_000_000),
919 keyid: Some("weird;key)with\"quote".to_owned()),
920 alg: Some(ALG.to_owned()),
921 ..SignatureParams::default()
922 };
923 let signed = sign(&request, &rfc_components(), ¶ms, "sig1", &key).unwrap();
924
925 let verified = verify(
926 &request,
927 &signed.signature_input,
928 &signed.signature,
929 &key.verifying_key(),
930 &VerifyConfig::default(),
931 )
932 .unwrap();
933 assert_eq!(
934 verified.params.keyid.as_deref(),
935 Some("weird;key)with\"quote")
936 );
937 }
938
939 #[test]
940 fn rejects_crlf_in_signature_params() {
941 use base64::{engine::general_purpose::STANDARD, Engine};
942
943 let key = SigningKey::from_bytes(&[5u8; 32]);
944 let request = rfc_request();
945 let signature_input = "sig1=(\"@method\")\n;created=1700000000";
947 let signature = format!("sig1=:{}:", STANDARD.encode([0u8; 64]));
948
949 let err = verify(
950 &request,
951 signature_input,
952 &signature,
953 &key.verifying_key(),
954 &VerifyConfig::default(),
955 );
956 assert!(matches!(err, Err(HttpSigError::Parse(_))));
957 }
958
959 use base64::{engine::general_purpose::STANDARD, Engine};
960
961 use super::{check_request_profile, WIMSE_TAG};
962
963 fn wimse_params() -> SignatureParams {
965 SignatureParams {
966 created: Some(1_700_000_000),
967 expires: Some(1_700_000_300),
968 nonce: Some("abcd1111".to_owned()),
969 tag: Some(WIMSE_TAG.to_owned()),
970 wimse_aud: Some("https://svcb.example.com/gimme-ice-cream".to_owned()),
971 ..SignatureParams::default()
972 }
973 }
974
975 fn wimse_config() -> VerifyConfig {
976 VerifyConfig {
977 wimse_profile: true,
978 ..VerifyConfig::default()
979 }
980 }
981
982 fn sign_with(params: &SignatureParams) -> (SigningKey, HttpRequest, super::SignedSignature) {
983 let key = SigningKey::from_bytes(&[5u8; 32]);
984 let request = rfc_request();
985 let signed = sign(&request, &rfc_components(), params, "wimse", &key).unwrap();
986 (key, request, signed)
987 }
988
989 #[test]
990 fn accepts_a_profile_conforming_signature() {
991 let (key, request, signed) = sign_with(&wimse_params());
992 let verified = verify(
993 &request,
994 &signed.signature_input,
995 &signed.signature,
996 &key.verifying_key(),
997 &wimse_config(),
998 )
999 .unwrap();
1000 assert_eq!(verified.params.tag.as_deref(), Some(WIMSE_TAG));
1001 }
1002
1003 #[test]
1006 fn profile_rejects_keyid_and_alg() {
1007 for (label, params) in [
1008 (
1009 "keyid",
1010 SignatureParams {
1011 keyid: Some("k".to_owned()),
1012 ..wimse_params()
1013 },
1014 ),
1015 (
1016 "alg",
1017 SignatureParams {
1018 alg: Some(ALG.to_owned()),
1019 ..wimse_params()
1020 },
1021 ),
1022 ] {
1023 let (key, request, signed) = sign_with(¶ms);
1024 let err = verify(
1025 &request,
1026 &signed.signature_input,
1027 &signed.signature,
1028 &key.verifying_key(),
1029 &wimse_config(),
1030 );
1031 assert!(
1032 matches!(err, Err(HttpSigError::ForbiddenParameter(p)) if p == label),
1033 "expected `{label}` to be rejected, got {err:?}"
1034 );
1035 }
1036 }
1037
1038 #[test]
1039 fn profile_requires_the_mandatory_parameters() {
1040 let cases: [(&str, SignatureParams); 5] = [
1041 (
1042 "created",
1043 SignatureParams {
1044 created: None,
1045 ..wimse_params()
1046 },
1047 ),
1048 (
1049 "expires",
1050 SignatureParams {
1051 expires: None,
1052 ..wimse_params()
1053 },
1054 ),
1055 (
1056 "nonce",
1057 SignatureParams {
1058 nonce: None,
1059 ..wimse_params()
1060 },
1061 ),
1062 (
1063 "tag",
1064 SignatureParams {
1065 tag: None,
1066 ..wimse_params()
1067 },
1068 ),
1069 (
1070 "wimse-aud",
1071 SignatureParams {
1072 wimse_aud: None,
1073 ..wimse_params()
1074 },
1075 ),
1076 ];
1077 for (name, params) in cases {
1078 let err = check_request_profile(¶ms);
1079 assert!(
1080 matches!(err, Err(HttpSigError::MissingParameter(p)) if p == name),
1081 "expected `{name}` to be required, got {err:?}"
1082 );
1083 }
1084 }
1085
1086 #[test]
1087 fn profile_rejects_a_foreign_tag() {
1088 let params = SignatureParams {
1089 tag: Some("something-else".to_owned()),
1090 ..wimse_params()
1091 };
1092 let (key, request, signed) = sign_with(¶ms);
1093 let err = verify(
1094 &request,
1095 &signed.signature_input,
1096 &signed.signature,
1097 &key.verifying_key(),
1098 &wimse_config(),
1099 );
1100 assert!(matches!(err, Err(HttpSigError::WrongTag { .. })));
1101 }
1102
1103 #[test]
1106 fn rejects_a_signature_minted_for_another_audience() {
1107 let (key, request, signed) = sign_with(&wimse_params());
1108 let config = VerifyConfig {
1109 expected_audience: Some("https://svcc.example.com/other".to_owned()),
1110 ..wimse_config()
1111 };
1112 let err = verify(
1113 &request,
1114 &signed.signature_input,
1115 &signed.signature,
1116 &key.verifying_key(),
1117 &config,
1118 );
1119 assert!(matches!(err, Err(HttpSigError::AudienceMismatch)));
1120 }
1121
1122 #[test]
1127 fn reports_a_forgery_as_invalid_regardless_of_audience() {
1128 let key = SigningKey::from_bytes(&[5u8; 32]);
1129 let request = rfc_request();
1130 let (_, _, signed) = sign_with(&wimse_params());
1131 let forged = format!("wimse=:{}:", STANDARD.encode([0u8; 64]));
1132
1133 for audience in [
1134 "https://svcb.example.com/gimme-ice-cream",
1135 "https://wrong.example/inbox",
1136 ] {
1137 let config = VerifyConfig {
1138 expected_audience: Some(audience.to_owned()),
1139 ..wimse_config()
1140 };
1141 let err = verify(
1142 &request,
1143 &signed.signature_input,
1144 &forged,
1145 &key.verifying_key(),
1146 &config,
1147 );
1148 assert!(
1149 matches!(err, Err(HttpSigError::InvalidSignature)),
1150 "expected InvalidSignature for audience {audience}, got {err:?}"
1151 );
1152 }
1153 }
1154
1155 #[test]
1156 fn accepts_the_matching_audience() {
1157 let (key, request, signed) = sign_with(&wimse_params());
1158 let config = VerifyConfig {
1159 expected_audience: Some("https://svcb.example.com/gimme-ice-cream".to_owned()),
1160 ..wimse_config()
1161 };
1162 assert!(verify(
1163 &request,
1164 &signed.signature_input,
1165 &signed.signature,
1166 &key.verifying_key(),
1167 &config,
1168 )
1169 .is_ok());
1170 }
1171
1172 #[test]
1173 fn serializes_sign_response_as_a_bare_boolean() {
1174 let params = SignatureParams {
1175 wimse_sign_response: Some(true),
1176 ..wimse_params()
1177 };
1178 let (key, request, signed) = sign_with(¶ms);
1179 assert!(signed.signature_input.ends_with(";wimse-sign-response"));
1180
1181 let verified = verify(
1182 &request,
1183 &signed.signature_input,
1184 &signed.signature,
1185 &key.verifying_key(),
1186 &wimse_config(),
1187 )
1188 .unwrap();
1189 assert_eq!(verified.params.wimse_sign_response, Some(true));
1190 }
1191
1192 #[test]
1193 fn round_trips_an_explicit_false_sign_response() {
1194 let params = SignatureParams {
1195 wimse_sign_response: Some(false),
1196 ..wimse_params()
1197 };
1198 let (key, request, signed) = sign_with(¶ms);
1199 assert!(signed.signature_input.ends_with(";wimse-sign-response=?0"));
1200
1201 let verified = verify(
1202 &request,
1203 &signed.signature_input,
1204 &signed.signature,
1205 &key.verifying_key(),
1206 &wimse_config(),
1207 )
1208 .unwrap();
1209 assert_eq!(verified.params.wimse_sign_response, Some(false));
1210 }
1211
1212 #[test]
1213 fn round_trips_the_response_nonce_binding() {
1214 let params = SignatureParams {
1215 wimse_req_nonce: Some("abcd1111".to_owned()),
1216 ..wimse_params()
1217 };
1218 let (key, request, signed) = sign_with(¶ms);
1219 let verified = verify(
1220 &request,
1221 &signed.signature_input,
1222 &signed.signature,
1223 &key.verifying_key(),
1224 &wimse_config(),
1225 )
1226 .unwrap();
1227 assert_eq!(verified.params.wimse_req_nonce.as_deref(), Some("abcd1111"));
1228 }
1229
1230 #[test]
1233 fn profile_is_off_by_default() {
1234 let (key, request, signed) = sign_with(&ed25519_params());
1235 assert!(verify(
1236 &request,
1237 &signed.signature_input,
1238 &signed.signature,
1239 &key.verifying_key(),
1240 &VerifyConfig::default(),
1241 )
1242 .is_ok());
1243 }
1244
1245 #[test]
1246 fn rejects_max_age_without_now() {
1247 let key = SigningKey::from_bytes(&[5u8; 32]);
1248 let request = rfc_request();
1249 let signed = sign(&request, &rfc_components(), &ed25519_params(), "sig1", &key).unwrap();
1250
1251 let config = VerifyConfig {
1253 max_age: Some(60),
1254 ..VerifyConfig::default()
1255 };
1256 let err = verify(
1257 &request,
1258 &signed.signature_input,
1259 &signed.signature,
1260 &key.verifying_key(),
1261 &config,
1262 );
1263 assert!(matches!(err, Err(HttpSigError::TooOld)));
1264 }
1265}