Skip to main content

ojak_sig/
signature.rs

1//! HTTP Signature (draft-cavage-http-signatures) for ActivityPub federation.
2//!
3//! Runtime-agnostic: it works with plain byte slices and `(name, value)` header
4//! pairs, so it can be used with any HTTP framework, and is given the time
5//! rather than reading a clock.
6
7use 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/// Headers produced by [`sign_request`] that must be attached to the outgoing request.
14#[derive(Debug, Clone)]
15pub struct SignedHeaders {
16    /// Value for the `Signature` header.
17    pub signature: String,
18    /// Value for the `Date` header.
19    pub date: String,
20    /// Value for the `Digest` header.
21    pub digest: String,
22}
23
24/// The `(request-target)` pseudo-header: the method and the path *including any
25/// query string*.
26///
27/// Mastodon's `HttpSignatureDraft#request_target` appends `?query` when there
28/// is one. Signing only the path would produce a signature no correct verifier
29/// could reproduce for such a URL, and would accept two different URLs as the
30/// same one.
31/// The `Host` header a request to `url` carries: the host, and the port when
32/// it is not the scheme's default, which is what an HTTP client sends and so
33/// what the receiver rebuilds the signing string from.
34fn 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
49/// `now`, seconds since the Unix epoch, as a `Date` header writes it.
50fn 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
60/// Build a signing string and the `headers` parameter naming its lines.
61///
62/// The covered set and its order follow Mastodon: the headers the request
63/// carries, in the order it carries them, with `(request-target)` appended
64/// last. Any verifier reconstructs from the `headers` parameter, so the order
65/// is not a correctness matter — but matching what the rest of the network
66/// emits keeps eunha out of the way of anything that verifies more strictly
67/// than it should.
68fn 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
82/// Sign an outgoing HTTP POST request body using RSA-SHA256.
83///
84/// Returns [`SignedHeaders`] that the caller must attach to the request.
85///
86/// Covers `host`, `date`, any `extra_headers` given, `digest`, and
87/// `(request-target)` — in that order, as Mastodon does. `extra_headers` are
88/// headers the caller will send and wants covered, such as `Content-Type`;
89/// Mastodon covers every header on the request except `User-Agent`, `Accept`
90/// and `Accept-Encoding`.
91///
92/// # Arguments
93/// * `method`          – HTTP method in any case (will be lowercased)
94/// * `url`             – Full URL of the target inbox
95/// * `body`            – Serialized activity body (JSON bytes)
96/// * `key_id`          – `keyId` URI, typically `https://actor/url#main-key`
97/// * `private_key_pem` – PKCS#8 or PKCS#1 PEM-encoded RSA private key
98/// * `extra_headers`   – Additional headers to cover, in the order sent
99/// * `now`             – The time, in seconds since the Unix epoch, which
100///   becomes the `Date` header
101///
102/// # Errors
103/// Returns an error if the URL cannot be parsed or the key cannot sign.
104pub 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
117/// [`sign_request`] with a key parsed once by [`PrivateKey::from_pem`].
118///
119/// Parsing an RSA key costs more than signing with it, so a caller that signs
120/// often with the same key should parse it once and keep it.
121///
122/// # Errors
123/// Returns an error if the URL cannot be parsed.
124pub 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/// Headers produced by [`sign_get`] for an authorized-fetch GET request.
162#[derive(Debug, Clone)]
163pub struct SignedGet {
164    /// Value for the `Signature` header.
165    pub signature: String,
166    /// Value for the `Date` header.
167    pub date: String,
168}
169
170/// Sign an outgoing HTTP GET request using RSA-SHA256, for "authorized fetch"
171/// (secure mode) servers that require a signed dereference.
172///
173/// A GET has no body, so unlike [`sign_request`] it signs only
174/// `(request-target) host date` and produces no `Digest`.
175///
176/// # Arguments
177/// * `url`             – Full URL being fetched
178/// * `key_id`          – `keyId` URI, typically `https://domain/actor#main-key`
179/// * `private_key_pem` – PKCS#8 or PKCS#1 PEM-encoded RSA private key
180/// * `now`             – The time, in seconds since the Unix epoch
181///
182/// # Errors
183/// Returns an error if the URL or the key cannot be parsed.
184pub 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
194/// [`sign_get`] with a key parsed once by [`PrivateKey::from_pem`].
195///
196/// # Errors
197/// Returns an error if the URL cannot be parsed.
198pub 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    // Same covered set and order as Mastodon: no body, so no digest.
210    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
225/// Verify the HTTP Signature on an incoming POST request.
226///
227/// This checks the signature and, when there is a `Digest` header, the body
228/// against it, and nothing more: not which headers were signed, nor when, nor
229/// for which host. [`verification`](super::verification) holds a request to
230/// that policy before it calls this.
231///
232/// # Arguments
233/// * `method`         – HTTP method in any case
234/// * `path`           – Request path (e.g., `/inbox`)
235/// * `headers`        – All request headers as `(lowercase-name, value)` pairs
236/// * `body`           – Raw request body bytes
237/// * `public_key_pem` – SPKI PEM-encoded RSA public key of the signing actor
238///
239/// # Errors
240/// When the signature cannot be read, the body does not match its digest, a
241/// covered header was not sent, or the signature does not verify.
242pub 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                // The signer covered the path *and* any query string, so the
279                // verifier has to reconstruct both or it rebuilds a different
280                // request than the one that was signed.
281                "(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/// Extract the `keyId` URI from a raw `Signature` header value.
302#[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// ── RSA helpers ───────────────────────────────────────────────────────────────
314
315/// An RSA private key, parsed and ready to sign with.
316///
317/// Parsing a PEM decodes the key and precomputes its CRT values, which costs
318/// more than a signature does. The `sign_*` functions that take a PEM parse it
319/// every call; keep one of these instead to sign many requests with one key.
320#[derive(Clone)]
321pub struct PrivateKey(rsa::pkcs1v15::SigningKey<Sha256>);
322
323impl PrivateKey {
324    /// Parse a PKCS#8 (`BEGIN PRIVATE KEY`) or PKCS#1 (`BEGIN RSA PRIVATE KEY`)
325    /// PEM-encoded RSA private key.
326    ///
327    /// # Errors
328    /// Returns an error if the PEM is neither.
329    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    /// RSASSA-PKCS1-v1_5 over SHA-256, base64-encoded.
335    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
349/// Sign `message` with RSASSA-PKCS1-v1_5 over SHA-256, base64-encoded.
350///
351/// Shared with [`crate::rfc9421`], which signs a different string with the same
352/// primitive.
353pub(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
357/// Parse an RSA private key from either PKCS#8 (`BEGIN PRIVATE KEY`) or
358/// PKCS#1 (`BEGIN RSA PRIVATE KEY`) PEM format.
359fn 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
371/// Verify an RSASSA-PKCS1-v1_5 SHA-256 signature, base64-encoded.
372///
373/// Shared with [`crate::rfc9421`], which verifies a different string with the
374/// same primitive.
375pub(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
395/// An RSA public key from a PEM as actors publish them: SubjectPublicKeyInfo
396/// (`PUBLIC KEY`) or PKCS#1 (`RSA PUBLIC KEY`), with whatever line length and
397/// surrounding whitespace it was written with. The PEM parser holds a key to
398/// 64-character lines, which not every implementation writes.
399pub(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
416/// The label and decoded body of a PEM block, read leniently: any line
417/// length, and whitespace anywhere in the body.
418fn 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
440/// A new RSA key pair for an actor, 2048 bits: the private key as PKCS#8
441/// PEM and the public key as SPKI PEM, the forms actor documents publish and
442/// [`PrivateKey::from_pem`] reads.
443///
444/// # Errors
445///
446/// When `rng` fails, or the key cannot be encoded.
447pub 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
464/// A `rand_core` 0.10 generator, as the `rand_core` 0.6 one `rsa` 0.9 makes
465/// keys with. Every draw is the caller's generator's own; nothing is added or
466/// kept. Until a stable `rsa` takes 0.10, which ojak's callers already use.
467pub(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        // A rand_core 0.10 `CryptoRng` cannot fail: a fallible source is
484        // only one once its caller has decided what failing means.
485        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    /// When the tests sign: any time will do, as nothing here checks it.
499    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        // One long line, and indented, as some servers write them.
540        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    /// A request to a non-default port carries `Host: host:port`, and the
600    /// signature has to cover that, not the bare host, or the receiver
601    /// rebuilds a different signing string.
602    #[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        // The default port is not written, as no client writes it.
625        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        // A GET has no body; verification must pass without a Digest header.
646        verify_request("get", "/users/bob", &headers, b"", &pub_pem).unwrap();
647    }
648
649    /// A key parsed once signs exactly as its PEM does — PKCS#1 v1.5 is
650    /// deterministic — and keeps doing so when reused.
651    #[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    /// The covered set and its order, as Mastodon's `HttpSignatureDraft`
693    /// produces them: the headers the request carries, then `(request-target)`
694    /// last. Pinned so this cannot drift back apart without a test saying so.
695    #[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    /// A GET carries no body, so no digest — otherwise the same shape.
720    #[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    /// A URL with a query signs the query too, and verifies only against the
741    /// same query. Mastodon's `request_target` appends `?query`; covering only
742    /// the path would make two different URLs indistinguishable to a verifier.
743    #[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}