1use async_trait::async_trait;
21use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
22use rsa::pkcs1v15::{Signature as RsaSignature, SigningKey, VerifyingKey};
23use rsa::pkcs8::{DecodePrivateKey, DecodePublicKey};
24use rsa::signature::{SignatureEncoding, Signer, Verifier};
25use rsa::{RsaPrivateKey, RsaPublicKey};
26use sha2::{Digest, Sha256};
27use std::collections::HashMap;
28use std::time::{Duration, SystemTime};
29
30use crate::error::SigError;
31use crate::ssrf::resolve_ssrf_safe;
32
33#[derive(Debug, Clone)]
35pub struct SignedRequest {
36 pub method: String,
37 pub path: String,
38 pub headers: HashMap<String, String>,
41 pub body: Vec<u8>,
42}
43
44impl SignedRequest {
45 pub fn new(method: impl Into<String>, path: impl Into<String>, body: Vec<u8>) -> Self {
46 Self {
47 method: method.into(),
48 path: path.into(),
49 headers: HashMap::new(),
50 body,
51 }
52 }
53 pub fn with_header(mut self, name: impl AsRef<str>, value: impl Into<String>) -> Self {
54 self.headers
55 .insert(name.as_ref().to_ascii_lowercase(), value.into());
56 self
57 }
58 fn get(&self, name: &str) -> Option<&str> {
59 self.headers.get(name).map(String::as_str)
60 }
61}
62
63#[derive(Debug, Clone)]
66pub struct OutboundRequest {
67 pub method: String,
68 pub url: String,
69 pub headers: Vec<(String, String)>,
70 pub body: Vec<u8>,
71}
72
73#[derive(Debug, Clone)]
76pub struct VerifiedActor {
77 pub key_id: String,
78 pub actor_url: String,
79 pub public_key_pem: String,
80}
81
82#[async_trait]
87pub trait ActorKeyResolver: Send + Sync {
88 async fn resolve(&self, key_id: &str) -> Result<VerifiedActor, SigError>;
89}
90
91pub struct HttpActorKeyResolver {
95 user_agent: String,
96}
97
98impl Default for HttpActorKeyResolver {
99 fn default() -> Self {
100 Self {
101 user_agent: "solid-pod-rs-activitypub/0.4.0".to_string(),
102 }
103 }
104}
105
106#[async_trait]
107impl ActorKeyResolver for HttpActorKeyResolver {
108 async fn resolve(&self, key_id: &str) -> Result<VerifiedActor, SigError> {
109 let actor_url = key_id
110 .split_once('#')
111 .map(|(u, _)| u.to_string())
112 .unwrap_or_else(|| key_id.to_string());
113
114 let (host, pinned) = resolve_ssrf_safe(&actor_url)?;
122 let client = reqwest::Client::builder()
123 .user_agent(self.user_agent.clone())
124 .redirect(reqwest::redirect::Policy::none())
125 .resolve(&host, pinned)
126 .build()
127 .map_err(|e| SigError::ActorFetch(actor_url.clone(), e.to_string()))?;
128
129 let resp = client
130 .get(&actor_url)
131 .header(reqwest::header::ACCEPT, "application/activity+json")
132 .send()
133 .await
134 .map_err(|e| SigError::ActorFetch(actor_url.clone(), e.to_string()))?;
135 if !resp.status().is_success() {
136 return Err(SigError::ActorFetch(
137 actor_url.clone(),
138 format!("status {}", resp.status()),
139 ));
140 }
141 let doc: serde_json::Value = resp
142 .json()
143 .await
144 .map_err(|e| SigError::ActorFetch(actor_url.clone(), e.to_string()))?;
145 let pem = doc
146 .get("publicKey")
147 .and_then(|k| k.get("publicKeyPem"))
148 .and_then(|v| v.as_str())
149 .ok_or(SigError::NoPublicKey)?;
150 Ok(VerifiedActor {
151 key_id: key_id.to_string(),
152 actor_url,
153 public_key_pem: pem.to_string(),
154 })
155 }
156}
157
158#[derive(Debug, Clone, Default)]
163struct SignatureHeader {
164 key_id: String,
165 algorithm: String,
166 headers: Vec<String>,
167 signature_b64: String,
168}
169
170fn parse_signature_header(raw: &str) -> Result<SignatureHeader, SigError> {
173 let mut out = SignatureHeader::default();
174 let mut attrs: Vec<(String, String)> = Vec::new();
176 let mut cur_key = String::new();
177 let mut cur_val = String::new();
178 let mut in_val = false;
179 let mut in_quote = false;
180 let mut expecting_eq = false;
181 for ch in raw.chars() {
182 if !in_val {
183 match ch {
184 '=' => {
185 in_val = true;
186 expecting_eq = false;
187 }
188 ',' | ' ' | '\t' if cur_key.is_empty() => { }
189 c if c.is_ascii_whitespace() => {
190 expecting_eq = true;
191 }
192 _ if expecting_eq => {
193 cur_key.push(ch);
195 expecting_eq = false;
196 }
197 _ => cur_key.push(ch),
198 }
199 } else {
200 match ch {
201 '"' => {
202 if in_quote {
203 attrs.push((
205 std::mem::take(&mut cur_key).to_ascii_lowercase(),
206 std::mem::take(&mut cur_val),
207 ));
208 in_quote = false;
209 in_val = false;
210 } else {
211 in_quote = true;
212 }
213 }
214 ',' if !in_quote => {
215 if !cur_key.is_empty() {
216 attrs.push((
217 std::mem::take(&mut cur_key).to_ascii_lowercase(),
218 std::mem::take(&mut cur_val),
219 ));
220 }
221 in_val = false;
222 }
223 _ => {
224 if in_quote || !ch.is_ascii_whitespace() {
225 cur_val.push(ch);
226 }
227 }
228 }
229 }
230 }
231 if !cur_key.is_empty() && (in_val || !cur_val.is_empty()) {
232 attrs.push((cur_key.to_ascii_lowercase(), cur_val));
233 }
234
235 for (k, v) in attrs {
236 match k.as_str() {
237 "keyid" => out.key_id = v,
238 "algorithm" => out.algorithm = v.to_ascii_lowercase(),
239 "headers" => {
240 out.headers = v
241 .split_ascii_whitespace()
242 .map(|s| s.to_ascii_lowercase())
243 .collect();
244 }
245 "signature" => out.signature_b64 = v,
246 _ => {}
247 }
248 }
249 if out.key_id.is_empty() {
250 return Err(SigError::MissingKeyId);
251 }
252 if out.signature_b64.is_empty() {
253 return Err(SigError::MalformedSignature(
254 "missing signature= value".into(),
255 ));
256 }
257 if out.algorithm.is_empty() {
258 out.algorithm = "rsa-sha256".to_string();
261 }
262 if out.headers.is_empty() {
263 out.headers = vec!["date".to_string()];
266 }
267 Ok(out)
268}
269
270fn build_signature_base(req: &SignedRequest, header_list: &[String]) -> Result<String, SigError> {
273 let mut lines = Vec::with_capacity(header_list.len());
274 for h in header_list {
275 match h.as_str() {
276 "(request-target)" => {
277 lines.push(format!(
278 "(request-target): {} {}",
279 req.method.to_ascii_lowercase(),
280 req.path
281 ));
282 }
283 name => {
284 let v = req.get(name).ok_or_else(|| {
285 SigError::VerifyFailed(format!("missing covered header: {name}"))
286 })?;
287 lines.push(format!("{name}: {v}"));
288 }
289 }
290 }
291 Ok(lines.join("\n"))
292}
293
294pub fn digest_header(body: &[u8]) -> String {
296 let digest = Sha256::digest(body);
297 format!("SHA-256={}", B64.encode(digest))
298}
299
300const DATE_MAX_AGE_SECS: u64 = 300; const DATE_MAX_FUTURE_SECS: u64 = 30;
306
307fn check_date_freshness(date_str: &str) -> Result<(), SigError> {
310 let parsed = httpdate::parse_http_date(date_str).map_err(|e| {
311 SigError::DateNotFresh(format!("unparseable Date header '{}': {}", date_str, e))
312 })?;
313 let now = SystemTime::now();
314
315 if let Ok(age) = now.duration_since(parsed) {
317 if age > Duration::from_secs(DATE_MAX_AGE_SECS) {
318 return Err(SigError::DateNotFresh(format!(
319 "Date '{}' is {} seconds old (max {})",
320 date_str,
321 age.as_secs(),
322 DATE_MAX_AGE_SECS
323 )));
324 }
325 }
326
327 if let Ok(ahead) = parsed.duration_since(now) {
329 if ahead > Duration::from_secs(DATE_MAX_FUTURE_SECS) {
330 return Err(SigError::DateNotFresh(format!(
331 "Date '{}' is {} seconds in the future (max {})",
332 date_str,
333 ahead.as_secs(),
334 DATE_MAX_FUTURE_SECS
335 )));
336 }
337 }
338
339 Ok(())
340}
341
342pub async fn verify_request_signature(
346 req: &SignedRequest,
347 resolver: &dyn ActorKeyResolver,
348) -> Result<VerifiedActor, SigError> {
349 let sig_raw = req
350 .get("signature")
351 .ok_or(SigError::MissingHeader("signature"))?;
352 let parsed = parse_signature_header(sig_raw)?;
353 if parsed.algorithm != "rsa-sha256" && parsed.algorithm != "hs2019" {
354 return Err(SigError::UnsupportedAlgorithm(parsed.algorithm));
355 }
356
357 if let Some(date_val) = req.get("date") {
360 check_date_freshness(date_val)?;
361 } else if parsed.headers.iter().any(|h| h == "date") {
362 return Err(SigError::MissingHeader("date"));
363 }
364
365 if parsed.headers.iter().any(|h| h == "digest") {
368 let received = req.get("digest").ok_or(SigError::MissingHeader("digest"))?;
369 let computed = digest_header(&req.body);
370 if received != computed && !received.eq_ignore_ascii_case(&computed) {
373 let rfc9530 = {
374 let digest = Sha256::digest(&req.body);
375 format!("sha-256=:{}:", B64.encode(digest))
376 };
377 if received != rfc9530 {
378 return Err(SigError::DigestMismatch);
379 }
380 }
381 }
382
383 let actor = resolver.resolve(&parsed.key_id).await?;
384 let pub_key = RsaPublicKey::from_public_key_pem(&actor.public_key_pem)
385 .map_err(|e| SigError::Rsa(e.to_string()))?;
386 let vk = VerifyingKey::<Sha256>::new(pub_key);
387
388 let base = build_signature_base(req, &parsed.headers)?;
389 let sig_bytes = B64
390 .decode(parsed.signature_b64.as_bytes())
391 .map_err(|e| SigError::Base64(e.to_string()))?;
392 let sig = RsaSignature::try_from(sig_bytes.as_slice())
393 .map_err(|e| SigError::MalformedSignature(e.to_string()))?;
394 vk.verify(base.as_bytes(), &sig)
395 .map_err(|e| SigError::VerifyFailed(e.to_string()))?;
396 Ok(actor)
397}
398
399pub fn sign_request(
406 req: &mut OutboundRequest,
407 private_key_pem: &str,
408 key_id: &str,
409) -> Result<(), SigError> {
410 let url = url::Url::parse(&req.url).map_err(|e| SigError::Url(e.to_string()))?;
411 let host = url
412 .host_str()
413 .ok_or_else(|| SigError::Url("url has no host".into()))?;
414 let path = if let Some(q) = url.query() {
415 format!("{}?{}", url.path(), q)
416 } else {
417 url.path().to_string()
418 };
419 let date = httpdate::fmt_http_date(std::time::SystemTime::now());
420 let digest = digest_header(&req.body);
421
422 let covered = vec!["(request-target)", "host", "date", "digest"];
424 let mut base_lines: Vec<String> = Vec::new();
425 for h in &covered {
426 match *h {
427 "(request-target)" => base_lines.push(format!(
428 "(request-target): {} {}",
429 req.method.to_ascii_lowercase(),
430 path
431 )),
432 "host" => base_lines.push(format!("host: {host}")),
433 "date" => base_lines.push(format!("date: {date}")),
434 "digest" => base_lines.push(format!("digest: {digest}")),
435 _ => {}
436 }
437 }
438 let base = base_lines.join("\n");
439
440 let sk =
441 RsaPrivateKey::from_pkcs8_pem(private_key_pem).map_err(|e| SigError::Rsa(e.to_string()))?;
442 let signer = SigningKey::<Sha256>::new(sk);
443 let sig: RsaSignature = signer.sign(base.as_bytes());
444 let sig_b64 = B64.encode(sig.to_bytes());
445
446 let signature_header = format!(
447 "keyId=\"{key_id}\",algorithm=\"rsa-sha256\",headers=\"{headers}\",signature=\"{sig}\"",
448 key_id = key_id,
449 headers = covered.join(" "),
450 sig = sig_b64,
451 );
452
453 req.headers.retain(|(n, _)| {
455 let ln = n.to_ascii_lowercase();
456 ln != "host" && ln != "date" && ln != "digest" && ln != "signature"
457 });
458 req.headers.push(("Host".to_string(), host.to_string()));
459 req.headers.push(("Date".to_string(), date));
460 req.headers.push(("Digest".to_string(), digest));
461 req.headers
462 .push(("Signature".to_string(), signature_header));
463 Ok(())
464}
465
466#[cfg(test)]
471mod tests {
472 use super::*;
473 use async_trait::async_trait;
474
475 struct StaticResolver {
476 pem: String,
477 }
478
479 #[async_trait]
480 impl ActorKeyResolver for StaticResolver {
481 async fn resolve(&self, key_id: &str) -> Result<VerifiedActor, SigError> {
482 Ok(VerifiedActor {
483 key_id: key_id.to_string(),
484 actor_url: key_id.trim_end_matches("#main-key").to_string(),
485 public_key_pem: self.pem.clone(),
486 })
487 }
488 }
489
490 fn fresh_keypair() -> (String, String) {
491 crate::actor::generate_actor_keypair().unwrap()
492 }
493
494 fn build_signed_inbound(
495 method: &str,
496 path: &str,
497 body: &[u8],
498 priv_pem: &str,
499 key_id: &str,
500 ) -> SignedRequest {
501 let host = "pod.example";
502 let date = httpdate::fmt_http_date(std::time::SystemTime::now());
503 let digest = digest_header(body);
504 let base = format!(
505 "(request-target): {} {}\nhost: {}\ndate: {}\ndigest: {}",
506 method.to_ascii_lowercase(),
507 path,
508 host,
509 date,
510 digest
511 );
512 let sk = RsaPrivateKey::from_pkcs8_pem(priv_pem).unwrap();
513 let signer = SigningKey::<Sha256>::new(sk);
514 let sig: RsaSignature = signer.sign(base.as_bytes());
515 let sig_b64 = B64.encode(sig.to_bytes());
516 let sig_header = format!(
517 "keyId=\"{key_id}\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest\",signature=\"{sig_b64}\""
518 );
519
520 SignedRequest::new(method, path, body.to_vec())
521 .with_header("host", host)
522 .with_header("date", date)
523 .with_header("digest", digest)
524 .with_header("signature", sig_header)
525 }
526
527 #[test]
528 fn parse_signature_header_valid() {
529 let raw = r#"keyId="https://a.example/actor#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="ZmFrZQ==""#;
530 let parsed = parse_signature_header(raw).unwrap();
531 assert_eq!(parsed.key_id, "https://a.example/actor#main-key");
532 assert_eq!(parsed.algorithm, "rsa-sha256");
533 assert_eq!(
534 parsed.headers,
535 vec![
536 "(request-target)".to_string(),
537 "host".to_string(),
538 "date".to_string(),
539 "digest".to_string()
540 ]
541 );
542 assert_eq!(parsed.signature_b64, "ZmFrZQ==");
543 }
544
545 #[test]
546 fn parse_signature_header_rejects_missing_keyid() {
547 let raw = r#"algorithm="rsa-sha256",signature="abc""#;
548 assert!(matches!(
549 parse_signature_header(raw),
550 Err(SigError::MissingKeyId)
551 ));
552 }
553
554 #[test]
555 fn digest_header_is_mastodon_shape() {
556 let d = digest_header(b"hello");
557 assert!(d.starts_with("SHA-256="));
558 }
559
560 #[tokio::test]
561 async fn http_signature_verify_accepts_valid_request() {
562 let (priv_pem, pub_pem) = fresh_keypair();
563 let key_id = "https://remote.example/actor#main-key";
564 let req = build_signed_inbound("POST", "/inbox", b"{}", &priv_pem, key_id);
565 let resolver = StaticResolver { pem: pub_pem };
566 let actor = verify_request_signature(&req, &resolver).await.unwrap();
567 assert_eq!(actor.key_id, key_id);
568 assert_eq!(actor.actor_url, "https://remote.example/actor");
569 }
570
571 #[tokio::test]
572 async fn http_signature_verify_rejects_tampered_body() {
573 let (priv_pem, pub_pem) = fresh_keypair();
574 let key_id = "https://remote.example/actor#main-key";
575 let mut req = build_signed_inbound("POST", "/inbox", b"{}", &priv_pem, key_id);
576 req.body = b"{\"tampered\":true}".to_vec();
578 let resolver = StaticResolver { pem: pub_pem };
579 let res = verify_request_signature(&req, &resolver).await;
580 assert!(matches!(res, Err(SigError::DigestMismatch)), "got {res:?}");
581 }
582
583 #[tokio::test]
584 async fn http_signature_verify_rejects_wrong_key() {
585 let (priv_pem, _pub_pem) = fresh_keypair();
586 let (_, other_pub_pem) = fresh_keypair();
587 let key_id = "https://remote.example/actor#main-key";
588 let req = build_signed_inbound("POST", "/inbox", b"{}", &priv_pem, key_id);
589 let resolver = StaticResolver { pem: other_pub_pem };
590 let res = verify_request_signature(&req, &resolver).await;
591 assert!(matches!(res, Err(SigError::VerifyFailed(_))));
592 }
593
594 #[tokio::test]
595 async fn http_signature_verify_roundtrips_through_sign_request() {
596 let (priv_pem, pub_pem) = fresh_keypair();
597 let key_id = "https://pod.example/profile/card.jsonld#main-key";
598 let body = br#"{"type":"Follow"}"#.to_vec();
599 let mut out = OutboundRequest {
600 method: "POST".into(),
601 url: "https://remote.example/inbox".into(),
602 headers: vec![("Content-Type".into(), "application/activity+json".into())],
603 body: body.clone(),
604 };
605 sign_request(&mut out, &priv_pem, key_id).unwrap();
606
607 let url = url::Url::parse(&out.url).unwrap();
609 let path = url.path().to_string();
610 let mut inbound = SignedRequest::new("POST", &path, body);
611 for (k, v) in &out.headers {
612 inbound.headers.insert(k.to_ascii_lowercase(), v.clone());
613 }
614 let resolver = StaticResolver { pem: pub_pem };
615 let actor = verify_request_signature(&inbound, &resolver).await.unwrap();
616 assert_eq!(actor.key_id, key_id);
617 }
618
619 #[test]
622 fn check_date_freshness_accepts_now() {
623 let now = httpdate::fmt_http_date(SystemTime::now());
624 assert!(check_date_freshness(&now).is_ok());
625 }
626
627 #[test]
628 fn check_date_freshness_rejects_stale_date() {
629 let old = SystemTime::now() - Duration::from_secs(600);
630 let date = httpdate::fmt_http_date(old);
631 let result = check_date_freshness(&date);
632 assert!(
633 matches!(result, Err(SigError::DateNotFresh(_))),
634 "expected DateNotFresh for 10-minute-old Date, got {result:?}"
635 );
636 }
637
638 #[test]
639 fn check_date_freshness_rejects_future_date() {
640 let future = SystemTime::now() + Duration::from_secs(120);
641 let date = httpdate::fmt_http_date(future);
642 let result = check_date_freshness(&date);
643 assert!(
644 matches!(result, Err(SigError::DateNotFresh(_))),
645 "expected DateNotFresh for 2-minute-future Date, got {result:?}"
646 );
647 }
648
649 #[test]
650 fn check_date_freshness_accepts_slight_future() {
651 let slight_future = SystemTime::now() + Duration::from_secs(10);
653 let date = httpdate::fmt_http_date(slight_future);
654 assert!(check_date_freshness(&date).is_ok());
655 }
656
657 #[test]
658 fn check_date_freshness_rejects_garbage() {
659 let result = check_date_freshness("not-a-date");
660 assert!(
661 matches!(result, Err(SigError::DateNotFresh(_))),
662 "expected DateNotFresh for garbage date, got {result:?}"
663 );
664 }
665
666 #[tokio::test]
667 async fn verify_rejects_stale_date_header() {
668 let (priv_pem, pub_pem) = fresh_keypair();
669 let key_id = "https://remote.example/actor#main-key";
670 let host = "pod.example";
672 let old_time = SystemTime::now() - Duration::from_secs(600);
673 let date = httpdate::fmt_http_date(old_time);
674 let body = b"{}";
675 let digest = digest_header(body);
676 let base = format!(
677 "(request-target): post /inbox\nhost: {}\ndate: {}\ndigest: {}",
678 host, date, digest
679 );
680 let sk = RsaPrivateKey::from_pkcs8_pem(&priv_pem).unwrap();
681 let signer = SigningKey::<Sha256>::new(sk);
682 let sig: RsaSignature = signer.sign(base.as_bytes());
683 let sig_b64 = B64.encode(sig.to_bytes());
684 let sig_header = format!(
685 "keyId=\"{key_id}\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest\",signature=\"{sig_b64}\""
686 );
687 let req = SignedRequest::new("POST", "/inbox", body.to_vec())
688 .with_header("host", host)
689 .with_header("date", &date)
690 .with_header("digest", &digest)
691 .with_header("signature", &sig_header);
692
693 let resolver = StaticResolver { pem: pub_pem };
694 let result = verify_request_signature(&req, &resolver).await;
695 assert!(
696 matches!(result, Err(SigError::DateNotFresh(_))),
697 "expected DateNotFresh for stale request, got {result:?}"
698 );
699 }
700}