netget/server/activitypub/
sig.rs1use anyhow::{bail, ensure, Context, Result};
5use base64::Engine;
6use rsa::pkcs1::DecodeRsaPublicKey;
7use rsa::pkcs1v15::{Signature, SigningKey, VerifyingKey};
8use rsa::pkcs8::{DecodePublicKey, EncodePublicKey, LineEnding};
9use rsa::signature::{SignatureEncoding, Signer, Verifier};
10use rsa::{RsaPrivateKey, RsaPublicKey};
11use sha2::{Digest, Sha256};
12use std::collections::HashMap;
13
14pub const MAX_CLOCK_SKEW_SECS: i64 = 12 * 3600;
16pub const MAX_SIGNATURE_HEADER: usize = 4096;
18pub const KEY_BITS: usize = 2048;
19
20fn b64() -> base64::engine::GeneralPurpose {
21 base64::engine::general_purpose::STANDARD
22}
23
24pub fn generate_key() -> Result<RsaPrivateKey> {
25 Ok(RsaPrivateKey::new(&mut rand::rngs::OsRng, KEY_BITS)?)
26}
27
28pub fn public_pem(key: &RsaPrivateKey) -> Result<String> {
29 Ok(RsaPublicKey::from(key).to_public_key_pem(LineEnding::LF)?)
30}
31
32pub fn parse_public_pem(pem: &str) -> Result<RsaPublicKey> {
34 RsaPublicKey::from_public_key_pem(pem.trim())
35 .or_else(|_| RsaPublicKey::from_pkcs1_pem(pem.trim()))
36 .context("publicKeyPem is not an RSA public key")
37}
38
39pub fn digest(body: &[u8]) -> String {
41 format!("SHA-256={}", b64().encode(Sha256::digest(body)))
42}
43
44pub fn http_date() -> String {
45 httpdate_now()
46}
47
48fn httpdate_now() -> String {
49 chrono::Utc::now()
50 .format("%a, %d %b %Y %H:%M:%S GMT")
51 .to_string()
52}
53
54fn signing_string(
56 headers: &[&str],
57 method: &str,
58 path: &str,
59 values: &HashMap<String, String>,
60) -> Result<String> {
61 let mut lines = Vec::new();
62 for h in headers {
63 if *h == "(request-target)" {
64 lines.push(format!(
65 "(request-target): {} {}",
66 method.to_lowercase(),
67 path
68 ));
69 } else {
70 let v = values
71 .get(*h)
72 .with_context(|| format!("signed header {h} is missing from the request"))?;
73 lines.push(format!("{h}: {v}"));
74 }
75 }
76 Ok(lines.join("\n"))
77}
78
79pub fn sign(
81 key: &RsaPrivateKey,
82 key_id: &str,
83 method: &str,
84 path: &str,
85 values: &HashMap<String, String>,
86) -> Result<String> {
87 let headers: Vec<&str> = if values.contains_key("digest") {
88 vec!["(request-target)", "host", "date", "digest"]
89 } else {
90 vec!["(request-target)", "host", "date"]
91 };
92 let text = signing_string(&headers, method, path, values)?;
93 let sig = SigningKey::<Sha256>::new(key.clone()).sign(text.as_bytes());
94 Ok(format!(
95 "keyId=\"{key_id}\",algorithm=\"rsa-sha256\",headers=\"{}\",signature=\"{}\"",
96 headers.join(" "),
97 b64().encode(sig.to_bytes())
98 ))
99}
100
101pub struct Parsed {
103 pub key_id: String,
104 pub headers: Vec<String>,
105 pub signature: Vec<u8>,
106}
107
108pub fn parse(header: &str) -> Result<Parsed> {
109 ensure!(
110 header.len() <= MAX_SIGNATURE_HEADER,
111 "Signature header too long"
112 );
113 let mut fields = HashMap::new();
114 for part in header.split(',') {
115 let (k, v) = part
116 .trim()
117 .split_once('=')
118 .context("malformed Signature header")?;
119 fields.insert(k.trim().to_string(), v.trim().trim_matches('"').to_string());
120 }
121 let algorithm = fields
122 .get("algorithm")
123 .map(String::as_str)
124 .unwrap_or("hs2019");
125 ensure!(
126 matches!(algorithm, "rsa-sha256" | "hs2019"),
127 "signature algorithm {algorithm} is not supported (rsa-sha256 only)"
128 );
129 Ok(Parsed {
130 key_id: fields
131 .get("keyId")
132 .cloned()
133 .context("Signature has no keyId")?,
134 headers: fields
135 .get("headers")
136 .map(|h| h.split_whitespace().map(str::to_lowercase).collect())
137 .unwrap_or_else(|| vec!["date".into()]),
138 signature: b64()
139 .decode(
140 fields
141 .get("signature")
142 .context("Signature has no signature")?,
143 )
144 .context("signature is not base64")?,
145 })
146}
147
148pub fn verify(
152 parsed: &Parsed,
153 key_pem: &str,
154 method: &str,
155 path: &str,
156 values: &HashMap<String, String>,
157 body: Option<&[u8]>,
158) -> Result<()> {
159 for must in ["(request-target)", "host", "date"] {
160 ensure!(
161 parsed.headers.iter().any(|h| h == must),
162 "the signature does not cover {must}"
163 );
164 }
165 if let Some(body) = body {
166 ensure!(
167 parsed.headers.iter().any(|h| h == "digest"),
168 "the signature does not cover the body's digest"
169 );
170 let claimed = values.get("digest").context("no Digest header")?;
171 let ours = digest(body);
172 ensure!(
173 claimed.split(',').any(|d| d.trim() == ours),
174 "the Digest header does not match the body"
175 );
176 }
177 let date = values.get("date").context("no Date header")?;
178 let when = chrono::DateTime::parse_from_rfc2822(date).context("Date is not an HTTP date")?;
179 let skew = (chrono::Utc::now().timestamp() - when.timestamp()).abs();
180 ensure!(skew <= MAX_CLOCK_SKEW_SECS, "Date is {skew} s from now");
181 let headers: Vec<&str> = parsed.headers.iter().map(String::as_str).collect();
182 let text = signing_string(&headers, method, path, values)?;
183 let key = parse_public_pem(key_pem)?;
184 let sig = Signature::try_from(parsed.signature.as_slice())
185 .context("signature has the wrong length")?;
186 if VerifyingKey::<Sha256>::new(key)
187 .verify(text.as_bytes(), &sig)
188 .is_err()
189 {
190 bail!("the signature does not verify against {}", parsed.key_id);
191 }
192 Ok(())
193}