1use crate::Error;
19use crate::signature::PrivateKey;
20use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
21use sha2::{Digest as _, Sha256};
22
23#[derive(Debug, Clone)]
25pub struct SignedHeaders {
26 pub signature_input: String,
28 pub signature: String,
30 pub content_digest: Option<String>,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Algorithm {
40 RsaV1_5Sha256,
43 Ed25519,
45}
46
47impl Algorithm {
48 #[must_use]
50 pub fn as_str(self) -> &'static str {
51 match self {
52 Self::RsaV1_5Sha256 => "rsa-v1_5-sha256",
53 Self::Ed25519 => "ed25519",
54 }
55 }
56
57 fn from_str(value: &str) -> Option<Self> {
58 match value {
59 "rsa-v1_5-sha256" => Some(Self::RsaV1_5Sha256),
60 "ed25519" => Some(Self::Ed25519),
61 _ => None,
62 }
63 }
64}
65
66pub enum SigningKey<'a> {
68 RsaPem(&'a str),
70 Rsa(&'a PrivateKey),
72 Ed25519(&'a [u8; 32]),
74}
75
76impl SigningKey<'_> {
77 fn algorithm(&self) -> Algorithm {
78 match self {
79 Self::RsaPem(_) | Self::Rsa(_) => Algorithm::RsaV1_5Sha256,
80 Self::Ed25519(_) => Algorithm::Ed25519,
81 }
82 }
83
84 fn sign(&self, message: &[u8]) -> Result<String, Error> {
85 match self {
86 Self::RsaPem(pem) => crate::signature::rsa_sign_pkcs1v15(pem, message),
87 Self::Rsa(key) => Ok(key.sign(message)),
88 Self::Ed25519(seed) => {
89 use ed25519_dalek::Signer as _;
90
91 let key = ed25519_dalek::SigningKey::from_bytes(seed);
92 Ok(BASE64.encode(key.sign(message).to_bytes()))
93 }
94 }
95 }
96}
97
98pub enum VerifyingKey<'a> {
100 RsaPem(&'a str),
102 Ed25519(&'a [u8; 32]),
104}
105
106impl VerifyingKey<'_> {
107 fn algorithm(&self) -> Algorithm {
108 match self {
109 Self::RsaPem(_) => Algorithm::RsaV1_5Sha256,
110 Self::Ed25519(_) => Algorithm::Ed25519,
111 }
112 }
113
114 fn verify(&self, message: &[u8], signature_b64: &str) -> Result<(), Error> {
115 match self {
116 Self::RsaPem(pem) => crate::signature::rsa_verify_pkcs1v15(pem, message, signature_b64),
117 Self::Ed25519(key) => {
118 use ed25519_dalek::Verifier as _;
119
120 let bytes = BASE64
121 .decode(signature_b64)
122 .map_err(|e| Error::Malformed(format!("signature is not base64: {e}")))?;
123 let signature = ed25519_dalek::Signature::from_slice(&bytes)
124 .map_err(|e| Error::Malformed(format!("Ed25519 signature: {e}")))?;
125 ed25519_dalek::VerifyingKey::from_bytes(key)
126 .map_err(|e| Error::Key(format!("Ed25519 public key: {e}")))?
127 .verify(message, &signature)
128 .map_err(|_| Error::Invalid)
129 }
130 }
131 }
132}
133
134const LABEL: &str = "sig1";
137
138pub fn sign_request(
156 method: &str,
157 url: &str,
158 body: Option<&[u8]>,
159 key_id: &str,
160 key: &SigningKey<'_>,
161 now: i64,
162) -> Result<SignedHeaders, Error> {
163 let content_digest = body.map(content_digest);
164
165 let mut components = vec![
166 ("@method".to_string(), method.to_uppercase()),
167 ("@target-uri".to_string(), url.to_string()),
168 ];
169 if let Some(digest) = &content_digest {
170 components.push(("content-digest".to_string(), digest.clone()));
171 }
172
173 let params = signature_params(
174 &components
175 .iter()
176 .map(|(name, _)| name.as_str())
177 .collect::<Vec<_>>(),
178 now,
179 key_id,
180 key.algorithm(),
181 );
182 let base = signature_base(&components, ¶ms);
183 let signature = key.sign(base.as_bytes())?;
184
185 Ok(SignedHeaders {
186 signature_input: format!("{LABEL}={params}"),
187 signature: format!("{LABEL}=:{signature}:"),
188 content_digest,
189 })
190}
191
192pub fn content_digest(body: &[u8]) -> String {
195 format!("sha-256=:{}:", BASE64.encode(Sha256::digest(body)))
196}
197
198fn signature_params(
202 components: &[&str],
203 created: i64,
204 key_id: &str,
205 algorithm: Algorithm,
206) -> String {
207 let covered = components
208 .iter()
209 .map(|name| format!("\"{name}\""))
210 .collect::<Vec<_>>()
211 .join(" ");
212 let _ = algorithm;
217 format!("({covered});created={created};keyid=\"{key_id}\"")
218}
219
220fn signature_base(components: &[(String, String)], params: &str) -> String {
226 let mut base = String::new();
227 for (name, value) in components {
228 base.push_str(&format!("\"{name}\": {value}\n"));
229 }
230 base.push_str(&format!("\"@signature-params\": {params}"));
231 base
232}
233
234#[allow(clippy::too_many_arguments)]
259pub fn verify_request(
260 method: &str,
261 target_uri: &str,
262 signature_input: &str,
263 signature: &str,
264 content_digest: Option<&str>,
265 body: &[u8],
266 headers: &[(&str, &str)],
267 key: &VerifyingKey<'_>,
268) -> Result<(), Error> {
269 let input = Input::parse(signature_input)?;
270 let sig_b64 = BASE64.encode(signature_bytes(signature, &input.label)?);
271 let Input {
272 covered, params, ..
273 } = &input;
274
275 if let Some(alg) = input.string("alg") {
279 let stated = Algorithm::from_str(&alg)
280 .ok_or_else(|| Error::Unsupported(format!("algorithm {alg:?}")))?;
281 if stated != key.algorithm() {
282 return Err(Error::WrongKey(format!(
283 "signature claims {} but the key is {}",
284 stated.as_str(),
285 key.algorithm().as_str()
286 )));
287 }
288 }
289
290 let url = url::Url::parse(target_uri)
291 .map_err(|error| Error::Malformed(format!("target URI {target_uri:?}: {error}")))?;
292 let mut components = Vec::with_capacity(covered.len());
293 for name in covered {
294 let value = match name.as_str() {
295 "@method" => method.to_uppercase(),
296 "@target-uri" => target_uri.to_string(),
297 "@authority" => {
299 let host = url
300 .host_str()
301 .ok_or_else(|| Error::Malformed("target URI has no authority".into()))?;
302 match url.port() {
303 Some(port) => format!("{host}:{port}"),
304 None => host.to_string(),
305 }
306 }
307 "@path" => url.path().to_string(),
308 "@scheme" => url.scheme().to_string(),
309 "@query" => format!("?{}", url.query().unwrap_or("")),
310 "@request-target" => match url.query() {
311 Some(query) => format!("{}?{query}", url.path()),
312 None => url.path().to_string(),
313 },
314 "content-digest" => {
315 let sent = content_digest.ok_or_else(|| Error::Missing("content-digest".into()))?;
316 if !crate::digest::content_digest_matches(sent, body) {
317 return Err(Error::DigestMismatch);
318 }
319 sent.to_string()
320 }
321 derived if derived.starts_with('@') => {
322 return Err(Error::Unsupported(format!("component {derived:?}")));
323 }
324 field => {
325 let values: Vec<&str> = headers
326 .iter()
327 .filter(|(name, _)| name.eq_ignore_ascii_case(field))
328 .map(|(_, value)| value.trim())
329 .collect();
330 if values.is_empty() {
331 return Err(Error::Missing(field.to_owned()));
332 }
333 values.join(", ")
334 }
335 };
336 components.push((name.clone(), value));
337 }
338
339 let base = signature_base(&components, params);
340 key.verify(base.as_bytes(), &sig_b64)
341}
342
343pub fn key_id(signature_input: &str) -> Option<String> {
346 Input::parse(signature_input).ok()?.string("keyid")
347}
348
349pub fn covered_components(signature_input: &str) -> Option<Vec<String>> {
354 Some(Input::parse(signature_input).ok()?.covered)
355}
356
357pub fn created_at(signature_input: &str) -> Option<i64> {
362 Input::parse(signature_input).ok()?.integer("created")
363}
364
365pub fn expires_at(signature_input: &str) -> Option<i64> {
367 Input::parse(signature_input).ok()?.integer("expires")
368}
369
370pub fn sign_response(
383 status: u16,
384 content_digest: &str,
385 key_id: Option<&str>,
386 key: &SigningKey<'_>,
387 now: i64,
388) -> Result<SignedHeaders, Error> {
389 let components = vec![
390 ("@status".to_string(), status.to_string()),
391 ("content-digest".to_string(), content_digest.to_string()),
392 ];
393 let mut params = format!("(\"@status\" \"content-digest\");created={now}");
394 if let Some(key_id) = key_id {
395 params.push_str(&format!(";keyid=\"{key_id}\""));
396 }
397 let base = signature_base(&components, ¶ms);
398 let signature = key.sign(base.as_bytes())?;
399 Ok(SignedHeaders {
400 signature_input: format!("{LABEL}={params}"),
401 signature: format!("{LABEL}=:{signature}:"),
402 content_digest: Some(content_digest.to_string()),
403 })
404}
405
406pub fn verify_response(
422 status: u16,
423 signature_input: &str,
424 signature: &str,
425 headers: &[(&str, &str)],
426 key: &VerifyingKey<'_>,
427) -> Result<(), Error> {
428 let input = Input::parse(signature_input)?;
429 let sig_b64 = BASE64.encode(signature_bytes(signature, &input.label)?);
430 if let Some(alg) = input.string("alg") {
431 let stated = Algorithm::from_str(&alg)
432 .ok_or_else(|| Error::Unsupported(format!("algorithm {alg:?}")))?;
433 if stated != key.algorithm() {
434 return Err(Error::WrongKey(format!(
435 "signature claims {} but the key is {}",
436 stated.as_str(),
437 key.algorithm().as_str()
438 )));
439 }
440 }
441 let mut components = Vec::with_capacity(input.covered.len());
442 for name in &input.covered {
443 let value = match name.as_str() {
444 "@status" => status.to_string(),
445 derived if derived.starts_with('@') => {
446 return Err(Error::Unsupported(format!("component {derived:?}")));
447 }
448 field => {
449 let values: Vec<&str> = headers
450 .iter()
451 .filter(|(name, _)| name.eq_ignore_ascii_case(field))
452 .map(|(_, value)| value.trim())
453 .collect();
454 if values.is_empty() {
455 return Err(Error::Missing(field.to_owned()));
456 }
457 values.join(", ")
458 }
459 };
460 components.push((name.clone(), value));
461 }
462 let base = signature_base(&components, &input.params);
463 key.verify(base.as_bytes(), &sig_b64)
464}
465
466struct Input {
471 label: String,
472 covered: Vec<String>,
474 params: String,
478 parameters: sfv::Parameters,
479}
480
481impl Input {
482 fn parse(header: &str) -> Result<Self, Error> {
483 let malformed = |why: String| Error::Malformed(format!("Signature-Input: {why}"));
484 let dictionary = sfv::Parser::new(header)
485 .parse::<sfv::Dictionary>()
486 .map_err(|error| malformed(error.to_string()))?;
487 let (label, entry) = dictionary
488 .first()
489 .ok_or_else(|| malformed("no signature".into()))?;
490 let sfv::ListEntry::InnerList(list) = entry else {
491 return Err(malformed("not a component list".into()));
492 };
493 let mut covered = Vec::with_capacity(list.items.len());
494 for item in &list.items {
495 let name = item
496 .bare_item
497 .as_string()
498 .ok_or_else(|| malformed("a covered component is not a string".into()))?;
499 if !item.params.is_empty() {
503 return Err(Error::Unsupported(format!(
504 "component {:?} with parameters",
505 name.as_str()
506 )));
507 }
508 let name = name.as_str().to_owned();
509 if covered.contains(&name) {
510 return Err(malformed(format!("{name:?} is covered twice")));
511 }
512 covered.push(name);
513 }
514 let mut serializer = sfv::ListSerializer::new();
515 serializer.members([entry]);
516 let params = serializer
517 .finish()
518 .ok_or_else(|| malformed("no signature".into()))?;
519 Ok(Self {
520 label: label.as_str().to_owned(),
521 covered,
522 params,
523 parameters: list.params.clone(),
524 })
525 }
526
527 fn string(&self, name: &str) -> Option<String> {
528 let key = sfv::KeyRef::from_str(name).ok()?;
529 Some(self.parameters.get(key)?.as_string()?.as_str().to_owned())
530 }
531
532 fn integer(&self, name: &str) -> Option<i64> {
533 let key = sfv::KeyRef::from_str(name).ok()?;
534 Some(self.parameters.get(key)?.as_integer()?.into())
535 }
536}
537
538fn signature_bytes(header: &str, label: &str) -> Result<Vec<u8>, Error> {
540 let malformed = |why: String| Error::Malformed(format!("Signature: {why}"));
541 let dictionary = sfv::Parser::new(header)
542 .parse::<sfv::Dictionary>()
543 .map_err(|error| malformed(error.to_string()))?;
544 let key = sfv::KeyRef::from_str(label).map_err(|error| malformed(error.to_string()))?;
545 let not_bytes = || malformed("not a byte sequence".into());
546 match dictionary.get(key) {
547 Some(sfv::ListEntry::Item(item)) => Ok(item
548 .bare_item
549 .as_byte_sequence()
550 .ok_or_else(not_bytes)?
551 .to_vec()),
552 Some(sfv::ListEntry::InnerList(_)) => Err(not_bytes()),
553 None => Err(malformed(format!("no signature labelled {label:?}"))),
554 }
555}
556
557#[cfg(test)]
558mod tests {
559 use super::*;
560
561 const TEST_KEY_RSA: &str = include_str!("../tests/fixtures/rfc9421_test_key_rsa.pem");
563
564 const NOW: i64 = 1_759_000_000;
566
567 #[test]
571 fn builds_the_signature_base_from_the_rfc() {
572 let components = vec![
573 ("@method".to_string(), "POST".to_string()),
574 (
575 "@authority".to_string(),
576 "origin.host.internal.example".to_string(),
577 ),
578 ("@path".to_string(), "/foo".to_string()),
579 (
580 "content-digest".to_string(),
581 "sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:".to_string(),
582 ),
583 ("content-type".to_string(), "application/json".to_string()),
584 ("content-length".to_string(), "18".to_string()),
585 (
586 "forwarded".to_string(),
587 "for=192.0.2.123;host=example.com;proto=https".to_string(),
588 ),
589 ];
590 let params = "(\"@method\" \"@authority\" \"@path\" \"content-digest\" \
591 \"content-type\" \"content-length\" \"forwarded\")\
592 ;created=1618884480;keyid=\"test-key-rsa\";alg=\"rsa-v1_5-sha256\"\
593 ;expires=1618884540";
594
595 let base = signature_base(&components, params);
596
597 let expected = concat!(
598 "\"@method\": POST\n",
599 "\"@authority\": origin.host.internal.example\n",
600 "\"@path\": /foo\n",
601 "\"content-digest\": sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:\n",
602 "\"content-type\": application/json\n",
603 "\"content-length\": 18\n",
604 "\"forwarded\": for=192.0.2.123;host=example.com;proto=https\n",
605 "\"@signature-params\": (\"@method\" \"@authority\" \"@path\" \"content-digest\" \"content-type\" \"content-length\" \"forwarded\");created=1618884480;keyid=\"test-key-rsa\";alg=\"rsa-v1_5-sha256\";expires=1618884540",
606 );
607 assert_eq!(base, expected);
608 }
609
610 #[test]
613 fn reproduces_the_rfc_signature() {
614 let base = concat!(
615 "\"@method\": POST\n",
616 "\"@authority\": origin.host.internal.example\n",
617 "\"@path\": /foo\n",
618 "\"content-digest\": sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:\n",
619 "\"content-type\": application/json\n",
620 "\"content-length\": 18\n",
621 "\"forwarded\": for=192.0.2.123;host=example.com;proto=https\n",
622 "\"@signature-params\": (\"@method\" \"@authority\" \"@path\" \"content-digest\" \"content-type\" \"content-length\" \"forwarded\");created=1618884480;keyid=\"test-key-rsa\";alg=\"rsa-v1_5-sha256\";expires=1618884540",
623 );
624
625 let signature = crate::signature::rsa_sign_pkcs1v15(TEST_KEY_RSA, base.as_bytes()).unwrap();
626
627 let expected = concat!(
628 "S6ZzPXSdAMOPjN/6KXfXWNO/f7V6cHm7BXYUh3YD/fRad4BCaRZxP+JH+8XY1I6+8Cy",
629 "+CM5g92iHgxtRPz+MjniOaYmdkDcnL9cCpXJleXsOckpURl49GwiyUpZ10KHgOEe11s",
630 "x3G2gxI8S0jnxQB+Pu68U9vVcasqOWAEObtNKKZd8tSFu7LB5YAv0RAGhB8tmpv7sFn",
631 "Im9y+7X5kXQfi8NMaZaA8i2ZHwpBdg7a6CMfwnnrtflzvZdXAsD3LH2TwevU+/PBPv0",
632 "B6NMNk93wUs/vfJvye+YuI87HU38lZHowtznbLVdp770I6VHR6WfgS9ddzirrswsE1w",
633 "5o0LV/g==",
634 );
635 assert_eq!(signature, expected);
636 }
637
638 #[test]
639 fn signs_a_post_the_way_mastodon_expects() {
640 let body = br#"{"type":"Create"}"#;
641 let signed = sign_request(
642 "post",
643 "https://remote.example/inbox",
644 Some(body),
645 "https://local.example/users/alice#main-key",
646 &SigningKey::RsaPem(TEST_KEY_RSA),
647 NOW,
648 )
649 .unwrap();
650
651 assert!(
652 signed
653 .signature_input
654 .starts_with("sig1=(\"@method\" \"@target-uri\" \"content-digest\");created=")
655 );
656 assert!(
657 signed
658 .signature_input
659 .contains("keyid=\"https://local.example/users/alice#main-key\"")
660 );
661 assert!(
662 signed
663 .signature_input
664 .ends_with("keyid=\"https://local.example/users/alice#main-key\"")
665 );
666 assert!(signed.signature.starts_with("sig1=:"));
667 assert!(signed.signature.ends_with(':'));
668 assert_eq!(signed.content_digest, Some(content_digest(body)));
669 }
670
671 #[test]
672 fn a_request_without_a_body_covers_no_digest() {
673 let signed = sign_request(
674 "get",
675 "https://remote.example/users/bob",
676 None,
677 "https://local.example/actor#main-key",
678 &SigningKey::RsaPem(TEST_KEY_RSA),
679 NOW,
680 )
681 .unwrap();
682
683 assert!(
684 signed
685 .signature_input
686 .starts_with("sig1=(\"@method\" \"@target-uri\");created=")
687 );
688 assert_eq!(signed.content_digest, None);
689 }
690
691 const TEST_KEY_RSA_PUBLIC: &str =
693 include_str!("../tests/fixtures/rfc9421_test_key_rsa_public.pem");
694
695 #[test]
696 fn verifies_what_it_signs() {
697 let body = br#"{"type":"Create"}"#;
698 let signed = sign_request(
699 "post",
700 "https://remote.example/inbox",
701 Some(body),
702 "https://local.example/users/alice#main-key",
703 &SigningKey::RsaPem(TEST_KEY_RSA),
704 NOW,
705 )
706 .unwrap();
707
708 verify_request(
709 "POST",
710 "https://remote.example/inbox",
711 &signed.signature_input,
712 &signed.signature,
713 signed.content_digest.as_deref(),
714 body,
715 &[],
716 &VerifyingKey::RsaPem(TEST_KEY_RSA_PUBLIC),
717 )
718 .expect("a signature this module produced should verify");
719 }
720
721 #[test]
722 fn rejects_a_body_the_digest_does_not_cover() {
723 let signed = sign_request(
724 "post",
725 "https://remote.example/inbox",
726 Some(b"original"),
727 "https://local.example/users/alice#main-key",
728 &SigningKey::RsaPem(TEST_KEY_RSA),
729 NOW,
730 )
731 .unwrap();
732
733 let err = verify_request(
734 "POST",
735 "https://remote.example/inbox",
736 &signed.signature_input,
737 &signed.signature,
738 signed.content_digest.as_deref(),
739 b"swapped",
740 &[],
741 &VerifyingKey::RsaPem(TEST_KEY_RSA_PUBLIC),
742 )
743 .unwrap_err();
744 assert_eq!(err, Error::DigestMismatch);
745 }
746
747 #[test]
748 fn rejects_a_request_sent_somewhere_else() {
749 let body = b"body";
750 let signed = sign_request(
751 "post",
752 "https://remote.example/inbox",
753 Some(body),
754 "https://local.example/users/alice#main-key",
755 &SigningKey::RsaPem(TEST_KEY_RSA),
756 NOW,
757 )
758 .unwrap();
759
760 assert!(
761 verify_request(
762 "POST",
763 "https://elsewhere.example/inbox",
764 &signed.signature_input,
765 &signed.signature,
766 signed.content_digest.as_deref(),
767 body,
768 &[],
769 &VerifyingKey::RsaPem(TEST_KEY_RSA_PUBLIC),
770 )
771 .is_err()
772 );
773 }
774
775 #[test]
776 fn reads_the_first_signature_when_a_proxy_has_added_its_own() {
777 let body = br#"{"type":"Create"}"#;
778 let signed = sign_request(
779 "post",
780 "https://remote.example/inbox",
781 Some(body),
782 "https://local.example/users/alice#main-key",
783 &SigningKey::RsaPem(TEST_KEY_RSA),
784 NOW,
785 )
786 .unwrap();
787 let input = signed
789 .signature_input
790 .replacen("sig1=(", "sig1=( ", 1)
791 .replacen(");", " );", 1);
792 let input = format!("{input}, proxy=(\"@method\");created=1;keyid=\"p;)=\"");
793 let signature = format!("{}, proxy=:AAAA:", signed.signature);
794
795 verify_request(
796 "POST",
797 "https://remote.example/inbox",
798 &input,
799 &signature,
800 signed.content_digest.as_deref(),
801 body,
802 &[],
803 &VerifyingKey::RsaPem(TEST_KEY_RSA_PUBLIC),
804 )
805 .expect("the sender's signature verifies beside the proxy's");
806 assert_eq!(
807 key_id(&input).as_deref(),
808 Some("https://local.example/users/alice#main-key")
809 );
810 }
811
812 #[test]
813 fn the_authority_has_the_port_when_it_is_not_the_default() {
814 let key = SigningKey::RsaPem(TEST_KEY_RSA);
815 let params = "(\"@method\" \"@authority\");created=1618884480;keyid=\"k\"";
816 let base = signature_base(
817 &[
818 ("@method".into(), "GET".into()),
819 ("@authority".into(), "remote.example:8443".into()),
820 ],
821 params,
822 );
823 let signature = key.sign(base.as_bytes()).unwrap();
824
825 verify_request(
826 "GET",
827 "https://remote.example:8443/users/bob",
828 &format!("sig1={params}"),
829 &format!("sig1=:{signature}:"),
830 None,
831 b"",
832 &[],
833 &VerifyingKey::RsaPem(TEST_KEY_RSA_PUBLIC),
834 )
835 .expect("the authority is the host and its port");
836 }
837
838 #[test]
839 fn rejects_a_component_covered_twice_or_in_part() {
840 for input in [
841 "sig1=(\"@method\" \"@method\");created=1;keyid=\"k\"",
842 "sig1=(\"@method\" \"x-dict\";key=\"a\");created=1;keyid=\"k\"",
843 ] {
844 assert!(
845 verify_request(
846 "GET",
847 "https://remote.example/",
848 input,
849 "sig1=:AAAA:",
850 None,
851 b"",
852 &[("x-dict", "a=1")],
853 &VerifyingKey::RsaPem(TEST_KEY_RSA_PUBLIC),
854 )
855 .is_err(),
856 "{input}"
857 );
858 assert_eq!(covered_components(input), None, "{input}");
859 }
860 }
861
862 #[test]
863 fn rejects_components_it_cannot_rebuild() {
864 let err = verify_request(
867 "POST",
868 "https://remote.example/inbox",
869 "sig1=(\"@method\" \"x-custom\");created=1618884480;keyid=\"k\"",
870 "sig1=:AAAA:",
871 None,
872 b"",
873 &[],
874 &VerifyingKey::RsaPem(TEST_KEY_RSA_PUBLIC),
875 )
876 .unwrap_err();
877 assert_eq!(err, Error::Missing("x-custom".into()));
878 }
879
880 #[test]
881 fn reads_the_created_parameter() {
882 assert_eq!(
883 created_at("sig1=(\"@method\");created=1618884480;keyid=\"k\""),
884 Some(1618884480)
885 );
886 assert_eq!(created_at("sig1=(\"@method\");keyid=\"k\""), None);
887 }
888
889 const TEST_KEY_ED25519_SEED: [u8; 32] = [
891 0x9f, 0x83, 0x62, 0xf8, 0x7a, 0x48, 0x4a, 0x95, 0x4e, 0x6e, 0x74, 0x0c, 0x5b, 0x4c, 0x0e,
892 0x84, 0x22, 0x91, 0x39, 0xa2, 0x0a, 0xa8, 0xab, 0x56, 0xff, 0x66, 0x58, 0x6f, 0x6a, 0x7d,
893 0x29, 0xc5,
894 ];
895 const TEST_KEY_ED25519_PUBLIC: [u8; 32] = [
896 0x26, 0xb4, 0x0b, 0x8f, 0x93, 0xff, 0xf3, 0xd8, 0x97, 0x11, 0x2f, 0x7e, 0xbc, 0x58, 0x2b,
897 0x23, 0x2d, 0xbd, 0x72, 0x51, 0x7d, 0x08, 0x2f, 0xe8, 0x3c, 0xfb, 0x30, 0xdd, 0xce, 0x43,
898 0xd1, 0xbb,
899 ];
900
901 #[test]
905 fn reproduces_the_rfc_ed25519_signature() {
906 let base = concat!(
907 "\"date\": Tue, 20 Apr 2021 02:07:55 GMT\n",
908 "\"@method\": POST\n",
909 "\"@path\": /foo\n",
910 "\"@authority\": example.com\n",
911 "\"content-type\": application/json\n",
912 "\"content-length\": 18\n",
913 "\"@signature-params\": (\"date\" \"@method\" \"@path\" \"@authority\" \"content-type\" \"content-length\");created=1618884473;keyid=\"test-key-ed25519\"",
914 );
915
916 let signature = SigningKey::Ed25519(&TEST_KEY_ED25519_SEED)
917 .sign(base.as_bytes())
918 .unwrap();
919
920 assert_eq!(
921 signature,
922 concat!(
923 "wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1",
924 "u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw==",
925 )
926 );
927
928 VerifyingKey::Ed25519(&TEST_KEY_ED25519_PUBLIC)
930 .verify(base.as_bytes(), &signature)
931 .expect("the RFC's own signature must verify");
932 }
933
934 #[test]
938 fn verifies_the_rfc_signature_over_header_fields() {
939 verify_request(
940 "POST",
941 "https://example.com/foo?param=Value&Pet=dog",
942 "sig-b26=(\"date\" \"@method\" \"@path\" \"@authority\" \"content-type\" \"content-length\");created=1618884473;keyid=\"test-key-ed25519\"",
943 "sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw==:",
944 None,
945 br#"{"hello": "world"}"#,
946 &[
947 ("Host", "example.com"),
948 ("Date", "Tue, 20 Apr 2021 02:07:55 GMT"),
949 ("Content-Type", "application/json"),
950 ("Content-Length", "18"),
951 ],
952 &VerifyingKey::Ed25519(&TEST_KEY_ED25519_PUBLIC),
953 )
954 .expect("the RFC's signature over header fields verifies");
955 }
956
957 #[test]
961 fn verifies_a_signature_covering_host_and_date_as_fedify_makes_them() {
962 let body = br#"{"type":"Create"}"#;
963 let digest = content_digest(body);
964 let target = "https://seoul.earth/users/alice/inbox";
965 let params = "(\"@method\" \"@target-uri\" \"@authority\" \"host\" \"date\" \"content-digest\");created=1759000000;keyid=\"https://hollo.example/@bob#main-key\";alg=\"rsa-v1_5-sha256\"";
966 let date = "Sun, 28 Sep 2025 00:00:00 GMT";
967 let base = signature_base(
968 &[
969 ("@method".into(), "POST".into()),
970 ("@target-uri".into(), target.into()),
971 ("@authority".into(), "seoul.earth".into()),
972 ("host".into(), "seoul.earth".into()),
973 ("date".into(), date.into()),
974 ("content-digest".into(), digest.clone()),
975 ],
976 params,
977 );
978 let signature = SigningKey::RsaPem(TEST_KEY_RSA)
979 .sign(base.as_bytes())
980 .unwrap();
981 let input = format!("sig1={params}");
982 let signed = format!("sig1=:{signature}:");
983 let headers = [("host", "seoul.earth"), ("date", date)];
984
985 verify_request(
986 "POST",
987 target,
988 &input,
989 &signed,
990 Some(&digest),
991 body,
992 &headers,
993 &VerifyingKey::RsaPem(TEST_KEY_RSA_PUBLIC),
994 )
995 .expect("a signature over host and date verifies");
996
997 let changed = [
998 ("host", "seoul.earth"),
999 ("date", "Mon, 29 Sep 2025 00:00:00 GMT"),
1000 ];
1001 assert!(
1002 verify_request(
1003 "POST",
1004 target,
1005 &input,
1006 &signed,
1007 Some(&digest),
1008 body,
1009 &changed,
1010 &VerifyingKey::RsaPem(TEST_KEY_RSA_PUBLIC),
1011 )
1012 .is_err(),
1013 "a covered header that changed must not verify"
1014 );
1015 }
1016
1017 #[test]
1018 fn signs_and_verifies_a_request_with_ed25519() {
1019 let body = br#"{"type":"Create"}"#;
1020 let signed = sign_request(
1021 "post",
1022 "https://remote.example/inbox",
1023 Some(body),
1024 "https://local.example/users/alice#ed25519-key",
1025 &SigningKey::Ed25519(&TEST_KEY_ED25519_SEED),
1026 NOW,
1027 )
1028 .unwrap();
1029
1030 assert!(
1031 !signed.signature_input.contains("alg="),
1032 "Mastodon's signer emits no alg parameter"
1033 );
1034
1035 verify_request(
1036 "POST",
1037 "https://remote.example/inbox",
1038 &signed.signature_input,
1039 &signed.signature,
1040 signed.content_digest.as_deref(),
1041 body,
1042 &[],
1043 &VerifyingKey::Ed25519(&TEST_KEY_ED25519_PUBLIC),
1044 )
1045 .expect("an Ed25519 signature this module made should verify");
1046 }
1047
1048 #[test]
1052 fn refuses_a_key_that_does_not_match_a_stated_algorithm() {
1053 let body = b"body";
1054 let signed = sign_request(
1055 "post",
1056 "https://remote.example/inbox",
1057 Some(body),
1058 "https://local.example/users/alice#ed25519-key",
1059 &SigningKey::Ed25519(&TEST_KEY_ED25519_SEED),
1060 NOW,
1061 )
1062 .unwrap();
1063
1064 let with_alg = format!("{};alg=\"ed25519\"", signed.signature_input);
1066
1067 let err = verify_request(
1068 "POST",
1069 "https://remote.example/inbox",
1070 &with_alg,
1071 &signed.signature,
1072 signed.content_digest.as_deref(),
1073 body,
1074 &[],
1075 &VerifyingKey::RsaPem(TEST_KEY_RSA_PUBLIC),
1076 )
1077 .unwrap_err();
1078 assert!(
1079 matches!(&err, Error::WrongKey(why) if why.contains("claims ed25519")),
1080 "{err}"
1081 );
1082 }
1083
1084 #[test]
1087 fn refuses_an_unknown_algorithm() {
1088 let err = verify_request(
1089 "POST",
1090 "https://remote.example/inbox",
1091 "sig1=(\"@method\" \"@target-uri\");created=1;keyid=\"k\";alg=\"rsa-pss-sha512\"",
1092 "sig1=:AAAA:",
1093 None,
1094 b"",
1095 &[],
1096 &VerifyingKey::RsaPem(TEST_KEY_RSA_PUBLIC),
1097 )
1098 .unwrap_err();
1099 assert!(matches!(err, Error::Unsupported(_)), "{err}");
1100 }
1101
1102 #[test]
1103 fn content_digest_is_an_rfc9530_byte_sequence() {
1104 assert_eq!(
1106 content_digest(b""),
1107 "sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:"
1108 );
1109 }
1110
1111 #[test]
1112 fn verifies_the_responses_it_signs() {
1113 let digest = content_digest(b"{}");
1114 let signed = sign_response(
1115 201,
1116 &digest,
1117 None,
1118 &SigningKey::Ed25519(&TEST_KEY_ED25519_SEED),
1119 1_700_000_000,
1120 )
1121 .unwrap();
1122 assert_eq!(
1123 signed.signature_input,
1124 "sig1=(\"@status\" \"content-digest\");created=1700000000"
1125 );
1126 let key = VerifyingKey::Ed25519(&TEST_KEY_ED25519_PUBLIC);
1127 let headers = [("Content-Digest", digest.as_str())];
1128 verify_response(
1129 201,
1130 &signed.signature_input,
1131 &signed.signature,
1132 &headers,
1133 &key,
1134 )
1135 .unwrap();
1136 assert!(
1137 verify_response(
1138 200,
1139 &signed.signature_input,
1140 &signed.signature,
1141 &headers,
1142 &key
1143 )
1144 .is_err()
1145 );
1146 let other = content_digest(b"[]");
1147 assert!(
1148 verify_response(
1149 201,
1150 &signed.signature_input,
1151 &signed.signature,
1152 &[("content-digest", other.as_str())],
1153 &key,
1154 )
1155 .is_err()
1156 );
1157 assert!(matches!(
1158 verify_response(201, &signed.signature_input, &signed.signature, &[], &key),
1159 Err(Error::Missing(_))
1160 ));
1161
1162 let named = sign_response(
1163 200,
1164 &digest,
1165 Some("https://a.example/key"),
1166 &SigningKey::Ed25519(&TEST_KEY_ED25519_SEED),
1167 1_700_000_000,
1168 )
1169 .unwrap();
1170 assert_eq!(
1171 key_id(&named.signature_input).as_deref(),
1172 Some("https://a.example/key")
1173 );
1174 verify_response(
1175 200,
1176 &named.signature_input,
1177 &named.signature,
1178 &headers,
1179 &key,
1180 )
1181 .unwrap();
1182 }
1183
1184 #[test]
1185 fn a_response_signature_covers_only_what_a_response_has() {
1186 let input = "sig1=(\"@method\" \"@status\");created=1";
1187 let signature = "sig1=:AAAA:";
1188 assert!(matches!(
1189 verify_response(
1190 200,
1191 input,
1192 signature,
1193 &[],
1194 &VerifyingKey::Ed25519(&TEST_KEY_ED25519_PUBLIC)
1195 ),
1196 Err(Error::Unsupported(_))
1197 ));
1198 }
1199}