1use crate::{CryptoRng, Error};
8use alloc::collections::BTreeMap;
9use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
10use core::fmt;
11use sha2::{Digest as _, Sha256};
12
13#[derive(Debug, Clone)]
15pub struct SignedHeaders {
16 pub signature: String,
18 pub date: String,
20 pub digest: String,
22}
23
24fn host_header(url: &url::Url) -> String {
35 let host = url.host_str().unwrap_or("");
36 match url.port() {
37 Some(port) => format!("{host}:{port}"),
38 None => host.to_owned(),
39 }
40}
41
42fn request_target(method: &str, url: &url::Url) -> String {
43 match url.query() {
44 Some(query) => format!("{} {}?{}", method.to_lowercase(), url.path(), query),
45 None => format!("{} {}", method.to_lowercase(), url.path()),
46 }
47}
48
49fn http_date(now: i64) -> Result<String, Error> {
51 let date = chrono::DateTime::from_timestamp(now, 0)
52 .ok_or_else(|| Error::Malformed(format!("{now} is not a time")))?;
53 Ok(date.format("%a, %d %b %Y %H:%M:%S GMT").to_string())
54}
55
56fn parse_url(url: &str) -> Result<url::Url, Error> {
57 url::Url::parse(url).map_err(|error| Error::Malformed(format!("URL {url:?}: {error}")))
58}
59
60fn signing_string(covered: &[(&str, String)]) -> (String, String) {
69 let string = covered
70 .iter()
71 .map(|(name, value)| format!("{}: {value}", name.to_lowercase()))
72 .collect::<Vec<_>>()
73 .join("\n");
74 let names = covered
75 .iter()
76 .map(|(name, _)| name.to_lowercase())
77 .collect::<Vec<_>>()
78 .join(" ");
79 (string, names)
80}
81
82pub fn sign_request(
105 method: &str,
106 url: &str,
107 body: &[u8],
108 key_id: &str,
109 private_key_pem: &str,
110 extra_headers: &[(&str, &str)],
111 now: i64,
112) -> Result<SignedHeaders, Error> {
113 let key = PrivateKey::from_pem(private_key_pem)?;
114 sign_request_with_key(method, url, body, key_id, &key, extra_headers, now)
115}
116
117pub fn sign_request_with_key(
125 method: &str,
126 url: &str,
127 body: &[u8],
128 key_id: &str,
129 key: &PrivateKey,
130 extra_headers: &[(&str, &str)],
131 now: i64,
132) -> Result<SignedHeaders, Error> {
133 let digest = format!("SHA-256={}", BASE64.encode(Sha256::digest(body)));
134 let date = http_date(now)?;
135
136 let parsed = parse_url(url)?;
137 let host = host_header(&parsed);
138
139 let mut covered: Vec<(&str, String)> = vec![("host", host), ("date", date.clone())];
140 covered.extend(
141 extra_headers
142 .iter()
143 .map(|(name, value)| (*name, (*value).to_string())),
144 );
145 covered.push(("digest", digest.clone()));
146 covered.push(("(request-target)", request_target(method, &parsed)));
147
148 let (signing_string, headers_param) = signing_string(&covered);
149 let sig_b64 = key.sign(signing_string.as_bytes());
150 let signature = format!(
151 r#"keyId="{key_id}",algorithm="rsa-sha256",headers="{headers_param}",signature="{sig_b64}""#
152 );
153
154 Ok(SignedHeaders {
155 signature,
156 date,
157 digest,
158 })
159}
160
161#[derive(Debug, Clone)]
163pub struct SignedGet {
164 pub signature: String,
166 pub date: String,
168}
169
170pub fn sign_get(
185 url: &str,
186 key_id: &str,
187 private_key_pem: &str,
188 now: i64,
189) -> Result<SignedGet, Error> {
190 let key = PrivateKey::from_pem(private_key_pem)?;
191 sign_get_with_key(url, key_id, &key, now)
192}
193
194pub fn sign_get_with_key(
199 url: &str,
200 key_id: &str,
201 key: &PrivateKey,
202 now: i64,
203) -> Result<SignedGet, Error> {
204 let date = http_date(now)?;
205
206 let parsed = parse_url(url)?;
207 let host = host_header(&parsed);
208
209 let covered = [
211 ("host", host),
212 ("date", date.clone()),
213 ("(request-target)", request_target("get", &parsed)),
214 ];
215 let (signing_string, headers_param) = signing_string(&covered);
216
217 let sig_b64 = key.sign(signing_string.as_bytes());
218 let signature = format!(
219 r#"keyId="{key_id}",algorithm="rsa-sha256",headers="{headers_param}",signature="{sig_b64}""#
220 );
221
222 Ok(SignedGet { signature, date })
223}
224
225pub fn verify_request(
243 method: &str,
244 path_and_query: &str,
245 headers: &[(&str, &str)],
246 body: &[u8],
247 public_key_pem: &str,
248) -> Result<(), Error> {
249 let get = |name: &str| -> &str {
250 headers
251 .iter()
252 .find(|(k, _)| *k == name)
253 .map(|(_, v)| *v)
254 .unwrap_or("")
255 };
256
257 let sig_header_val = get("signature");
258 if sig_header_val.is_empty() {
259 return Err(Error::Missing("Signature".into()));
260 }
261
262 let params = parse_params(sig_header_val);
263 let headers_list = params.get("headers").map_or("date", String::as_str);
264 let sig_b64 = params
265 .get("signature")
266 .ok_or_else(|| Error::Malformed("no signature parameter".into()))?;
267
268 let digest_val = get("digest");
269 if !digest_val.is_empty() && !crate::digest::digest_matches(digest_val, body) {
270 return Err(Error::DigestMismatch);
271 }
272
273 let signing_string = headers_list
274 .split_whitespace()
275 .map(|h| {
276 let h = h.to_ascii_lowercase();
277 let value = match h.as_str() {
278 "(request-target)" => format!("{} {}", method.to_lowercase(), path_and_query),
282 "(created)" | "(expires)" => params
283 .get(&h[1..h.len() - 1])
284 .ok_or_else(|| Error::Missing(h.clone()))?
285 .clone(),
286 other => headers
287 .iter()
288 .find(|(name, _)| *name == other)
289 .ok_or_else(|| Error::Missing(other.to_owned()))?
290 .1
291 .to_owned(),
292 };
293 Ok(format!("{h}: {value}"))
294 })
295 .collect::<Result<Vec<_>, Error>>()?
296 .join("\n");
297
298 rsa_verify_pkcs1v15(public_key_pem, signing_string.as_bytes(), sig_b64)
299}
300
301#[must_use]
303pub fn key_id_from_header(sig_header: &str) -> Option<&str> {
304 for part in sig_header.split(',') {
305 let part = part.trim();
306 if let Some(rest) = part.strip_prefix("keyId=") {
307 return Some(rest.trim_matches('"'));
308 }
309 }
310 None
311}
312
313#[derive(Clone)]
321pub struct PrivateKey(rsa::pkcs1v15::SigningKey<Sha256>);
322
323impl PrivateKey {
324 pub fn from_pem(pem: &str) -> Result<Self, Error> {
330 let key = parse_private_key(pem)?;
331 Ok(Self(rsa::pkcs1v15::SigningKey::<Sha256>::new(key)))
332 }
333
334 pub(crate) fn sign(&self, message: &[u8]) -> String {
336 use rsa::signature::{SignatureEncoding as _, Signer as _};
337
338 let sig: rsa::pkcs1v15::Signature = self.0.sign(message);
339 BASE64.encode(sig.to_bytes())
340 }
341}
342
343impl fmt::Debug for PrivateKey {
344 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
345 f.write_str("PrivateKey(..)")
346 }
347}
348
349pub(crate) fn rsa_sign_pkcs1v15(private_key_pem: &str, message: &[u8]) -> Result<String, Error> {
354 Ok(PrivateKey::from_pem(private_key_pem)?.sign(message))
355}
356
357fn parse_private_key(pem: &str) -> Result<rsa::RsaPrivateKey, Error> {
360 use rsa::pkcs8::DecodePrivateKey as _;
361
362 if let Ok(key) = rsa::RsaPrivateKey::from_pkcs8_pem(pem) {
363 return Ok(key);
364 }
365
366 use rsa::pkcs1::DecodeRsaPrivateKey as _;
367 rsa::RsaPrivateKey::from_pkcs1_pem(pem)
368 .map_err(|_| Error::Key("not an RSA private key in PKCS#8 or PKCS#1 PEM".into()))
369}
370
371pub(crate) fn rsa_verify_pkcs1v15(
376 public_key_pem: &str,
377 message: &[u8],
378 sig_b64: &str,
379) -> Result<(), Error> {
380 use rsa::pkcs1v15::{Signature, VerifyingKey};
381 use rsa::signature::Verifier as _;
382
383 let sig_bytes = BASE64
384 .decode(sig_b64)
385 .map_err(|error| Error::Malformed(format!("signature is not base64: {error}")))?;
386 let public_key = rsa_public_key_from_pem(public_key_pem)?;
387 let verifying_key = VerifyingKey::<Sha256>::new(public_key);
388 let sig = Signature::try_from(sig_bytes.as_slice())
389 .map_err(|error| Error::Malformed(format!("RSA signature: {error}")))?;
390 verifying_key
391 .verify(message, &sig)
392 .map_err(|_| Error::Invalid)
393}
394
395pub(crate) fn rsa_public_key_from_pem(pem: &str) -> Result<rsa::RsaPublicKey, Error> {
400 use rsa::pkcs1::DecodeRsaPublicKey as _;
401 use rsa::pkcs8::DecodePublicKey as _;
402
403 let pem = pem.trim();
404 if let Ok(key) = rsa::RsaPublicKey::from_public_key_pem(pem) {
405 return Ok(key);
406 }
407 let unreadable = || Error::Key("not an RSA public key in SPKI or PKCS#1 PEM".into());
408 let (label, der) = pem_body(pem).ok_or_else(unreadable)?;
409 match label {
410 "PUBLIC KEY" => rsa::RsaPublicKey::from_public_key_der(&der).map_err(|_| unreadable()),
411 "RSA PUBLIC KEY" => rsa::RsaPublicKey::from_pkcs1_der(&der).map_err(|_| unreadable()),
412 _ => Err(Error::Key(format!("not an RSA public key: {label}"))),
413 }
414}
415
416fn pem_body(pem: &str) -> Option<(&str, Vec<u8>)> {
419 let rest = pem.strip_prefix("-----BEGIN ")?;
420 let (label, rest) = rest.split_once("-----")?;
421 let end = format!("-----END {label}-----");
422 let (body, _) = rest.split_once(end.as_str())?;
423 let body: String = body.split_whitespace().collect();
424 Some((label, BASE64.decode(body).ok()?))
425}
426
427pub(crate) fn parse_params(header: &str) -> BTreeMap<String, String> {
428 let mut map = BTreeMap::new();
429 for part in header.split(',') {
430 let part = part.trim();
431 if let Some(pos) = part.find('=') {
432 let key = part[..pos].trim().to_string();
433 let val = part[pos + 1..].trim().trim_matches('"').to_string();
434 map.insert(key, val);
435 }
436 }
437 map
438}
439
440pub fn generate_rsa_keypair(rng: &mut impl CryptoRng) -> Result<(String, String), Error> {
448 use rsa::pkcs8::{EncodePrivateKey as _, EncodePublicKey as _, LineEnding};
449
450 let key = |error: &dyn fmt::Display| Error::Key(format!("RSA key generation: {error}"));
451 let private = rsa::RsaPrivateKey::new(&mut Rsa06(rng), 2048).map_err(|error| key(&error))?;
452 let public = rsa::RsaPublicKey::from(&private);
453 Ok((
454 private
455 .to_pkcs8_pem(LineEnding::LF)
456 .map_err(|error| key(&error))?
457 .to_string(),
458 public
459 .to_public_key_pem(LineEnding::LF)
460 .map_err(|error| key(&error))?,
461 ))
462}
463
464pub(crate) struct Rsa06<'a, R: ?Sized>(pub(crate) &'a mut R);
468
469impl<R: CryptoRng + ?Sized> rand_core_06::RngCore for Rsa06<'_, R> {
470 fn next_u32(&mut self) -> u32 {
471 self.0.next_u32()
472 }
473
474 fn next_u64(&mut self) -> u64 {
475 self.0.next_u64()
476 }
477
478 fn fill_bytes(&mut self, dest: &mut [u8]) {
479 self.0.fill_bytes(dest);
480 }
481
482 fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core_06::Error> {
483 self.0.fill_bytes(dest);
486 Ok(())
487 }
488}
489
490impl<R: CryptoRng + ?Sized> rand_core_06::CryptoRng for Rsa06<'_, R> {}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495 use rsa::pkcs1::EncodeRsaPrivateKey as _;
496 use rsa::pkcs8::EncodePublicKey as _;
497
498 const NOW: i64 = 1_759_000_000;
500
501 #[test]
502 fn a_generated_key_pair_signs_and_verifies() {
503 let (private, public) = generate_rsa_keypair(&mut rand::rng()).unwrap();
504 assert!(private.starts_with("-----BEGIN PRIVATE KEY-----"));
505 assert!(public.starts_with("-----BEGIN PUBLIC KEY-----"));
506 let signed = sign_request(
507 "post",
508 "https://a.example/inbox",
509 b"{}",
510 "https://b.example/users/b#main-key",
511 &private,
512 &[],
513 NOW,
514 )
515 .unwrap();
516 let headers = [
517 ("host", "a.example"),
518 ("date", signed.date.as_str()),
519 ("digest", signed.digest.as_str()),
520 ("signature", signed.signature.as_str()),
521 ];
522 verify_request("post", "/inbox", &headers, b"{}", &public).unwrap();
523 }
524
525 #[test]
526 fn a_public_key_is_read_in_the_forms_actors_publish() {
527 use rsa::pkcs1::{DecodeRsaPrivateKey as _, EncodeRsaPublicKey as _};
528
529 let private = rsa::RsaPrivateKey::from_pkcs1_pem(include_str!(
530 "../tests/fixtures/rfc9421_test_key_rsa.pem"
531 ))
532 .unwrap();
533 let public = rsa::RsaPublicKey::from(&private);
534 let spki = public
535 .to_public_key_pem(rsa::pkcs8::LineEnding::LF)
536 .unwrap();
537 let pkcs1 = public.to_pkcs1_pem(rsa::pkcs1::LineEnding::LF).unwrap();
538 assert!(pkcs1.starts_with("-----BEGIN RSA PUBLIC KEY-----"));
539 let unwrapped = {
541 let body: String = spki
542 .lines()
543 .filter(|line| !line.starts_with("-----"))
544 .collect();
545 format!("\n -----BEGIN PUBLIC KEY-----\n{body}\n-----END PUBLIC KEY----- \n")
546 };
547 for pem in [spki.as_str(), pkcs1.as_str(), unwrapped.as_str()] {
548 assert_eq!(rsa_public_key_from_pem(pem).unwrap(), public, "{pem}");
549 }
550 assert!(
551 rsa_public_key_from_pem("-----BEGIN PRIVATE KEY-----\nAAAA\n-----END PRIVATE KEY-----")
552 .is_err()
553 );
554 }
555
556 fn keypair() -> (String, String) {
557 let priv_key = rsa::RsaPrivateKey::new(&mut Rsa06(&mut rand::rng()), 2048).unwrap();
558 let pub_key = rsa::RsaPublicKey::from(&priv_key);
559 (
560 priv_key
561 .to_pkcs1_pem(rsa::pkcs1::LineEnding::LF)
562 .unwrap()
563 .to_string(),
564 pub_key
565 .to_public_key_pem(rsa::pkcs8::LineEnding::LF)
566 .unwrap(),
567 )
568 }
569
570 #[test]
571 fn sign_then_verify_roundtrips() {
572 let (priv_pem, pub_pem) = keypair();
573 let body = br#"{"type":"Create"}"#;
574 let signed = sign_request(
575 "post",
576 "https://remote.example/users/bob/inbox",
577 body,
578 "https://a.test/users/alice#main-key",
579 &priv_pem,
580 &[],
581 NOW,
582 )
583 .unwrap();
584
585 assert_eq!(
586 key_id_from_header(&signed.signature),
587 Some("https://a.test/users/alice#main-key")
588 );
589
590 let headers = [
591 ("host", "remote.example"),
592 ("date", signed.date.as_str()),
593 ("digest", signed.digest.as_str()),
594 ("signature", signed.signature.as_str()),
595 ];
596 verify_request("post", "/users/bob/inbox", &headers, body, &pub_pem).unwrap();
597 }
598
599 #[test]
603 fn a_non_default_port_is_part_of_the_signed_host() {
604 let (priv_pem, pub_pem) = keypair();
605 let body = br#"{"type":"Create"}"#;
606 let signed = sign_request(
607 "post",
608 "http://127.0.0.1:8080/inbox",
609 body,
610 "https://a.test/users/alice#main-key",
611 &priv_pem,
612 &[],
613 NOW,
614 )
615 .unwrap();
616 let headers = [
617 ("host", "127.0.0.1:8080"),
618 ("date", signed.date.as_str()),
619 ("digest", signed.digest.as_str()),
620 ("signature", signed.signature.as_str()),
621 ];
622 verify_request("post", "/inbox", &headers, body, &pub_pem).unwrap();
623
624 let url = url::Url::parse("https://remote.example:443/inbox").unwrap();
626 assert_eq!(host_header(&url), "remote.example");
627 }
628
629 #[test]
630 fn sign_get_then_verify_roundtrips() {
631 let (priv_pem, pub_pem) = keypair();
632 let signed = sign_get(
633 "https://remote.example/users/bob",
634 "https://a.test/actor#main-key",
635 &priv_pem,
636 NOW,
637 )
638 .unwrap();
639
640 let headers = [
641 ("host", "remote.example"),
642 ("date", signed.date.as_str()),
643 ("signature", signed.signature.as_str()),
644 ];
645 verify_request("get", "/users/bob", &headers, b"", &pub_pem).unwrap();
647 }
648
649 #[test]
652 fn a_parsed_key_signs_as_its_pem_does() {
653 let (priv_pem, pub_pem) = keypair();
654 let key = PrivateKey::from_pem(&priv_pem).unwrap();
655 assert_eq!(
656 key.sign(b"signing string"),
657 rsa_sign_pkcs1v15(&priv_pem, b"signing string").unwrap()
658 );
659
660 for target in ["/users/bob", "/users/carol"] {
661 let url = format!("https://remote.example{target}");
662 let signed =
663 sign_get_with_key(&url, "https://a.test/actor#main-key", &key, NOW).unwrap();
664 let headers = [
665 ("host", "remote.example"),
666 ("date", signed.date.as_str()),
667 ("signature", signed.signature.as_str()),
668 ];
669 verify_request("get", target, &headers, b"", &pub_pem).unwrap();
670 }
671
672 let body = br#"{"type":"Like"}"#;
673 let signed = sign_request_with_key(
674 "post",
675 "https://remote.example/inbox",
676 body,
677 "https://a.test/actor#main-key",
678 &key,
679 &[],
680 NOW,
681 )
682 .unwrap();
683 let headers = [
684 ("host", "remote.example"),
685 ("date", signed.date.as_str()),
686 ("digest", signed.digest.as_str()),
687 ("signature", signed.signature.as_str()),
688 ];
689 verify_request("post", "/inbox", &headers, body, &pub_pem).unwrap();
690 }
691
692 #[test]
696 fn covers_what_mastodon_covers_in_the_order_mastodon_covers_it() {
697 let (priv_pem, _) = keypair();
698 let signed = sign_request(
699 "post",
700 "https://remote.example/inbox",
701 b"{}",
702 "https://a.test/users/alice#main-key",
703 &priv_pem,
704 &[("content-type", "application/activity+json")],
705 NOW,
706 )
707 .unwrap();
708
709 assert!(
710 signed
711 .signature
712 .contains(r#"headers="host date content-type digest (request-target)""#),
713 "unexpected covered headers: {}",
714 signed.signature
715 );
716 assert!(signed.signature.contains(r#"algorithm="rsa-sha256""#));
717 }
718
719 #[test]
721 fn a_signed_get_covers_host_date_and_the_request_target() {
722 let (priv_pem, _) = keypair();
723 let signed = sign_get(
724 "https://remote.example/users/bob",
725 "https://a.test/actor#main-key",
726 &priv_pem,
727 NOW,
728 )
729 .unwrap();
730
731 assert!(
732 signed
733 .signature
734 .contains(r#"headers="host date (request-target)""#),
735 "unexpected covered headers: {}",
736 signed.signature
737 );
738 }
739
740 #[test]
744 fn the_request_target_covers_the_query_string() {
745 let (priv_pem, pub_pem) = keypair();
746 let url = "https://remote.example/inbox?shared=true";
747 let body = b"{}";
748 let signed = sign_request(
749 "post",
750 url,
751 body,
752 "https://a.test/users/alice#main-key",
753 &priv_pem,
754 &[],
755 NOW,
756 )
757 .unwrap();
758
759 let headers = [
760 ("host", "remote.example"),
761 ("date", signed.date.as_str()),
762 ("digest", signed.digest.as_str()),
763 ("signature", signed.signature.as_str()),
764 ];
765
766 verify_request("post", "/inbox?shared=true", &headers, body, &pub_pem)
767 .expect("the signed path and query must verify");
768
769 assert!(
770 verify_request("post", "/inbox", &headers, body, &pub_pem).is_err(),
771 "dropping the query must not still verify"
772 );
773 assert!(
774 verify_request("post", "/inbox?shared=false", &headers, body, &pub_pem).is_err(),
775 "a different query must not verify"
776 );
777 }
778
779 #[test]
780 fn verify_rejects_tampered_body() {
781 let (priv_pem, pub_pem) = keypair();
782 let signed = sign_request(
783 "post",
784 "https://remote.example/inbox",
785 b"original",
786 "https://a.test/users/alice#main-key",
787 &priv_pem,
788 &[],
789 NOW,
790 )
791 .unwrap();
792 let headers = [
793 ("host", "remote.example"),
794 ("date", signed.date.as_str()),
795 ("digest", signed.digest.as_str()),
796 ("signature", signed.signature.as_str()),
797 ];
798 assert!(verify_request("post", "/inbox", &headers, b"tampered", &pub_pem).is_err());
799 }
800}