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;
28
29use crate::error::SigError;
30
31#[derive(Debug, Clone)]
33pub struct SignedRequest {
34 pub method: String,
35 pub path: String,
36 pub headers: HashMap<String, String>,
39 pub body: Vec<u8>,
40}
41
42impl SignedRequest {
43 pub fn new(method: impl Into<String>, path: impl Into<String>, body: Vec<u8>) -> Self {
44 Self {
45 method: method.into(),
46 path: path.into(),
47 headers: HashMap::new(),
48 body,
49 }
50 }
51 pub fn with_header(mut self, name: impl AsRef<str>, value: impl Into<String>) -> Self {
52 self.headers
53 .insert(name.as_ref().to_ascii_lowercase(), value.into());
54 self
55 }
56 fn get(&self, name: &str) -> Option<&str> {
57 self.headers.get(name).map(String::as_str)
58 }
59}
60
61#[derive(Debug, Clone)]
64pub struct OutboundRequest {
65 pub method: String,
66 pub url: String,
67 pub headers: Vec<(String, String)>,
68 pub body: Vec<u8>,
69}
70
71#[derive(Debug, Clone)]
74pub struct VerifiedActor {
75 pub key_id: String,
76 pub actor_url: String,
77 pub public_key_pem: String,
78}
79
80#[async_trait]
85pub trait ActorKeyResolver: Send + Sync {
86 async fn resolve(&self, key_id: &str) -> Result<VerifiedActor, SigError>;
87}
88
89pub struct HttpActorKeyResolver {
93 client: reqwest::Client,
94}
95
96impl Default for HttpActorKeyResolver {
97 fn default() -> Self {
98 Self {
99 client: reqwest::Client::builder()
100 .user_agent("solid-pod-rs-activitypub/0.4.0")
101 .build()
102 .expect("reqwest client builds"),
103 }
104 }
105}
106
107#[async_trait]
108impl ActorKeyResolver for HttpActorKeyResolver {
109 async fn resolve(&self, key_id: &str) -> Result<VerifiedActor, SigError> {
110 let actor_url = key_id
111 .split_once('#')
112 .map(|(u, _)| u.to_string())
113 .unwrap_or_else(|| key_id.to_string());
114 let resp = self
115 .client
116 .get(&actor_url)
117 .header(reqwest::header::ACCEPT, "application/activity+json")
118 .send()
119 .await
120 .map_err(|e| SigError::ActorFetch(actor_url.clone(), e.to_string()))?;
121 if !resp.status().is_success() {
122 return Err(SigError::ActorFetch(
123 actor_url.clone(),
124 format!("status {}", resp.status()),
125 ));
126 }
127 let doc: serde_json::Value = resp
128 .json()
129 .await
130 .map_err(|e| SigError::ActorFetch(actor_url.clone(), e.to_string()))?;
131 let pem = doc
132 .get("publicKey")
133 .and_then(|k| k.get("publicKeyPem"))
134 .and_then(|v| v.as_str())
135 .ok_or(SigError::NoPublicKey)?;
136 Ok(VerifiedActor {
137 key_id: key_id.to_string(),
138 actor_url,
139 public_key_pem: pem.to_string(),
140 })
141 }
142}
143
144#[derive(Debug, Clone, Default)]
149struct SignatureHeader {
150 key_id: String,
151 algorithm: String,
152 headers: Vec<String>,
153 signature_b64: String,
154}
155
156fn parse_signature_header(raw: &str) -> Result<SignatureHeader, SigError> {
159 let mut out = SignatureHeader::default();
160 let mut attrs: Vec<(String, String)> = Vec::new();
162 let mut cur_key = String::new();
163 let mut cur_val = String::new();
164 let mut in_val = false;
165 let mut in_quote = false;
166 let mut expecting_eq = false;
167 for ch in raw.chars() {
168 if !in_val {
169 match ch {
170 '=' => {
171 in_val = true;
172 expecting_eq = false;
173 }
174 ',' | ' ' | '\t' if cur_key.is_empty() => { }
175 c if c.is_ascii_whitespace() => {
176 expecting_eq = true;
177 }
178 _ if expecting_eq => {
179 cur_key.push(ch);
181 expecting_eq = false;
182 }
183 _ => cur_key.push(ch),
184 }
185 } else {
186 match ch {
187 '"' => {
188 if in_quote {
189 attrs.push((
191 std::mem::take(&mut cur_key).to_ascii_lowercase(),
192 std::mem::take(&mut cur_val),
193 ));
194 in_quote = false;
195 in_val = false;
196 } else {
197 in_quote = true;
198 }
199 }
200 ',' if !in_quote => {
201 if !cur_key.is_empty() {
202 attrs.push((
203 std::mem::take(&mut cur_key).to_ascii_lowercase(),
204 std::mem::take(&mut cur_val),
205 ));
206 }
207 in_val = false;
208 }
209 _ => {
210 if in_quote || !ch.is_ascii_whitespace() {
211 cur_val.push(ch);
212 }
213 }
214 }
215 }
216 }
217 if !cur_key.is_empty() && (in_val || !cur_val.is_empty()) {
218 attrs.push((cur_key.to_ascii_lowercase(), cur_val));
219 }
220
221 for (k, v) in attrs {
222 match k.as_str() {
223 "keyid" => out.key_id = v,
224 "algorithm" => out.algorithm = v.to_ascii_lowercase(),
225 "headers" => {
226 out.headers = v
227 .split_ascii_whitespace()
228 .map(|s| s.to_ascii_lowercase())
229 .collect();
230 }
231 "signature" => out.signature_b64 = v,
232 _ => {}
233 }
234 }
235 if out.key_id.is_empty() {
236 return Err(SigError::MissingKeyId);
237 }
238 if out.signature_b64.is_empty() {
239 return Err(SigError::MalformedSignature("missing signature= value".into()));
240 }
241 if out.algorithm.is_empty() {
242 out.algorithm = "rsa-sha256".to_string();
245 }
246 if out.headers.is_empty() {
247 out.headers = vec!["date".to_string()];
250 }
251 Ok(out)
252}
253
254fn build_signature_base(req: &SignedRequest, header_list: &[String]) -> Result<String, SigError> {
257 let mut lines = Vec::with_capacity(header_list.len());
258 for h in header_list {
259 match h.as_str() {
260 "(request-target)" => {
261 lines.push(format!(
262 "(request-target): {} {}",
263 req.method.to_ascii_lowercase(),
264 req.path
265 ));
266 }
267 name => {
268 let v = req
269 .get(name)
270 .ok_or_else(|| SigError::VerifyFailed(format!("missing covered header: {name}")))?;
271 lines.push(format!("{name}: {v}"));
272 }
273 }
274 }
275 Ok(lines.join("\n"))
276}
277
278pub fn digest_header(body: &[u8]) -> String {
280 let digest = Sha256::digest(body);
281 format!("SHA-256={}", B64.encode(digest))
282}
283
284pub async fn verify_request_signature(
288 req: &SignedRequest,
289 resolver: &dyn ActorKeyResolver,
290) -> Result<VerifiedActor, SigError> {
291 let sig_raw = req
292 .get("signature")
293 .ok_or(SigError::MissingHeader("signature"))?;
294 let parsed = parse_signature_header(sig_raw)?;
295 if parsed.algorithm != "rsa-sha256" && parsed.algorithm != "hs2019" {
296 return Err(SigError::UnsupportedAlgorithm(parsed.algorithm));
297 }
298
299 if parsed.headers.iter().any(|h| h == "digest") {
302 let received = req
303 .get("digest")
304 .ok_or(SigError::MissingHeader("digest"))?;
305 let computed = digest_header(&req.body);
306 if received != computed
309 && !received.eq_ignore_ascii_case(&computed)
310 {
311 let rfc9530 = {
312 let digest = Sha256::digest(&req.body);
313 format!("sha-256=:{}:", B64.encode(digest))
314 };
315 if received != rfc9530 {
316 return Err(SigError::DigestMismatch);
317 }
318 }
319 }
320
321 let actor = resolver.resolve(&parsed.key_id).await?;
322 let pub_key = RsaPublicKey::from_public_key_pem(&actor.public_key_pem)
323 .map_err(|e| SigError::Rsa(e.to_string()))?;
324 let vk = VerifyingKey::<Sha256>::new(pub_key);
325
326 let base = build_signature_base(req, &parsed.headers)?;
327 let sig_bytes = B64
328 .decode(parsed.signature_b64.as_bytes())
329 .map_err(|e| SigError::Base64(e.to_string()))?;
330 let sig = RsaSignature::try_from(sig_bytes.as_slice())
331 .map_err(|e| SigError::MalformedSignature(e.to_string()))?;
332 vk.verify(base.as_bytes(), &sig)
333 .map_err(|e| SigError::VerifyFailed(e.to_string()))?;
334 Ok(actor)
335}
336
337pub fn sign_request(
344 req: &mut OutboundRequest,
345 private_key_pem: &str,
346 key_id: &str,
347) -> Result<(), SigError> {
348 let url = url::Url::parse(&req.url).map_err(|e| SigError::Url(e.to_string()))?;
349 let host = url
350 .host_str()
351 .ok_or_else(|| SigError::Url("url has no host".into()))?;
352 let path = if let Some(q) = url.query() {
353 format!("{}?{}", url.path(), q)
354 } else {
355 url.path().to_string()
356 };
357 let date = httpdate::fmt_http_date(std::time::SystemTime::now());
358 let digest = digest_header(&req.body);
359
360 let covered = vec!["(request-target)", "host", "date", "digest"];
362 let mut base_lines: Vec<String> = Vec::new();
363 for h in &covered {
364 match *h {
365 "(request-target)" => base_lines.push(format!(
366 "(request-target): {} {}",
367 req.method.to_ascii_lowercase(),
368 path
369 )),
370 "host" => base_lines.push(format!("host: {host}")),
371 "date" => base_lines.push(format!("date: {date}")),
372 "digest" => base_lines.push(format!("digest: {digest}")),
373 _ => {}
374 }
375 }
376 let base = base_lines.join("\n");
377
378 let sk = RsaPrivateKey::from_pkcs8_pem(private_key_pem)
379 .map_err(|e| SigError::Rsa(e.to_string()))?;
380 let signer = SigningKey::<Sha256>::new(sk);
381 let sig: RsaSignature = signer.sign(base.as_bytes());
382 let sig_b64 = B64.encode(sig.to_bytes());
383
384 let signature_header = format!(
385 "keyId=\"{key_id}\",algorithm=\"rsa-sha256\",headers=\"{headers}\",signature=\"{sig}\"",
386 key_id = key_id,
387 headers = covered.join(" "),
388 sig = sig_b64,
389 );
390
391 req.headers.retain(|(n, _)| {
393 let ln = n.to_ascii_lowercase();
394 ln != "host"
395 && ln != "date"
396 && ln != "digest"
397 && ln != "signature"
398 });
399 req.headers.push(("Host".to_string(), host.to_string()));
400 req.headers.push(("Date".to_string(), date));
401 req.headers.push(("Digest".to_string(), digest));
402 req.headers.push(("Signature".to_string(), signature_header));
403 Ok(())
404}
405
406#[cfg(test)]
411mod tests {
412 use super::*;
413 use async_trait::async_trait;
414
415 struct StaticResolver {
416 pem: String,
417 }
418
419 #[async_trait]
420 impl ActorKeyResolver for StaticResolver {
421 async fn resolve(&self, key_id: &str) -> Result<VerifiedActor, SigError> {
422 Ok(VerifiedActor {
423 key_id: key_id.to_string(),
424 actor_url: key_id.trim_end_matches("#main-key").to_string(),
425 public_key_pem: self.pem.clone(),
426 })
427 }
428 }
429
430 fn fresh_keypair() -> (String, String) {
431 crate::actor::generate_actor_keypair().unwrap()
432 }
433
434 fn build_signed_inbound(
435 method: &str,
436 path: &str,
437 body: &[u8],
438 priv_pem: &str,
439 key_id: &str,
440 ) -> SignedRequest {
441 let host = "pod.example";
442 let date = httpdate::fmt_http_date(std::time::SystemTime::now());
443 let digest = digest_header(body);
444 let base = format!(
445 "(request-target): {} {}\nhost: {}\ndate: {}\ndigest: {}",
446 method.to_ascii_lowercase(),
447 path,
448 host,
449 date,
450 digest
451 );
452 let sk = RsaPrivateKey::from_pkcs8_pem(priv_pem).unwrap();
453 let signer = SigningKey::<Sha256>::new(sk);
454 let sig: RsaSignature = signer.sign(base.as_bytes());
455 let sig_b64 = B64.encode(sig.to_bytes());
456 let sig_header = format!(
457 "keyId=\"{key_id}\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest\",signature=\"{sig_b64}\""
458 );
459
460 SignedRequest::new(method, path, body.to_vec())
461 .with_header("host", host)
462 .with_header("date", date)
463 .with_header("digest", digest)
464 .with_header("signature", sig_header)
465 }
466
467 #[test]
468 fn parse_signature_header_valid() {
469 let raw = r#"keyId="https://a.example/actor#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="ZmFrZQ==""#;
470 let parsed = parse_signature_header(raw).unwrap();
471 assert_eq!(parsed.key_id, "https://a.example/actor#main-key");
472 assert_eq!(parsed.algorithm, "rsa-sha256");
473 assert_eq!(
474 parsed.headers,
475 vec![
476 "(request-target)".to_string(),
477 "host".to_string(),
478 "date".to_string(),
479 "digest".to_string()
480 ]
481 );
482 assert_eq!(parsed.signature_b64, "ZmFrZQ==");
483 }
484
485 #[test]
486 fn parse_signature_header_rejects_missing_keyid() {
487 let raw = r#"algorithm="rsa-sha256",signature="abc""#;
488 assert!(matches!(
489 parse_signature_header(raw),
490 Err(SigError::MissingKeyId)
491 ));
492 }
493
494 #[test]
495 fn digest_header_is_mastodon_shape() {
496 let d = digest_header(b"hello");
497 assert!(d.starts_with("SHA-256="));
498 }
499
500 #[tokio::test]
501 async fn http_signature_verify_accepts_valid_request() {
502 let (priv_pem, pub_pem) = fresh_keypair();
503 let key_id = "https://remote.example/actor#main-key";
504 let req = build_signed_inbound("POST", "/inbox", b"{}", &priv_pem, key_id);
505 let resolver = StaticResolver { pem: pub_pem };
506 let actor = verify_request_signature(&req, &resolver).await.unwrap();
507 assert_eq!(actor.key_id, key_id);
508 assert_eq!(actor.actor_url, "https://remote.example/actor");
509 }
510
511 #[tokio::test]
512 async fn http_signature_verify_rejects_tampered_body() {
513 let (priv_pem, pub_pem) = fresh_keypair();
514 let key_id = "https://remote.example/actor#main-key";
515 let mut req = build_signed_inbound("POST", "/inbox", b"{}", &priv_pem, key_id);
516 req.body = b"{\"tampered\":true}".to_vec();
518 let resolver = StaticResolver { pem: pub_pem };
519 let res = verify_request_signature(&req, &resolver).await;
520 assert!(
521 matches!(res, Err(SigError::DigestMismatch)),
522 "got {res:?}"
523 );
524 }
525
526 #[tokio::test]
527 async fn http_signature_verify_rejects_wrong_key() {
528 let (priv_pem, _pub_pem) = fresh_keypair();
529 let (_, other_pub_pem) = fresh_keypair();
530 let key_id = "https://remote.example/actor#main-key";
531 let req = build_signed_inbound("POST", "/inbox", b"{}", &priv_pem, key_id);
532 let resolver = StaticResolver {
533 pem: other_pub_pem,
534 };
535 let res = verify_request_signature(&req, &resolver).await;
536 assert!(matches!(res, Err(SigError::VerifyFailed(_))));
537 }
538
539 #[tokio::test]
540 async fn http_signature_verify_roundtrips_through_sign_request() {
541 let (priv_pem, pub_pem) = fresh_keypair();
542 let key_id = "https://pod.example/profile/card.jsonld#main-key";
543 let body = br#"{"type":"Follow"}"#.to_vec();
544 let mut out = OutboundRequest {
545 method: "POST".into(),
546 url: "https://remote.example/inbox".into(),
547 headers: vec![("Content-Type".into(), "application/activity+json".into())],
548 body: body.clone(),
549 };
550 sign_request(&mut out, &priv_pem, key_id).unwrap();
551
552 let url = url::Url::parse(&out.url).unwrap();
554 let path = url.path().to_string();
555 let mut inbound = SignedRequest::new("POST", &path, body);
556 for (k, v) in &out.headers {
557 inbound.headers.insert(k.to_ascii_lowercase(), v.clone());
558 }
559 let resolver = StaticResolver { pem: pub_pem };
560 let actor = verify_request_signature(&inbound, &resolver).await.unwrap();
561 assert_eq!(actor.key_id, key_id);
562 }
563}