Skip to main content

ojak_sig/
rfc9421.rs

1//! HTTP Message Signatures ([RFC 9421]).
2//!
3//! The successor to the draft-cavage signatures ActivityPub grew up on. Where
4//! the draft signs a list of header names against an ad-hoc string, RFC 9421
5//! signs *components* — including derived ones like `@method` and
6//! `@target-uri` — against a canonical signature base, and carries the covered
7//! list in its own `Signature-Input` header so a verifier reconstructs exactly
8//! what was signed.
9//!
10//! Mastodon 4.7 sends these as a fallback: a request goes out with a cavage
11//! signature first, and is retried with an RFC 9421 one if the peer answers
12//! 400 or 401. It covers `("@method" "@target-uri" "content-digest")` for a
13//! request with a body and `("@method" "@target-uri")` for one without, which
14//! is what [`sign_request`] produces.
15//!
16//! [RFC 9421]: https://www.rfc-editor.org/rfc/rfc9421.html
17
18use crate::Error;
19use crate::signature::PrivateKey;
20use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
21use sha2::{Digest as _, Sha256};
22
23/// Headers produced by [`sign_request`], to attach to the outgoing request.
24#[derive(Debug, Clone)]
25pub struct SignedHeaders {
26    /// Value for the `Signature-Input` header.
27    pub signature_input: String,
28    /// Value for the `Signature` header.
29    pub signature: String,
30    /// Value for the `Content-Digest` header ([RFC 9530]), when the request has
31    /// a body. It is a covered component, so it must be sent as given.
32    ///
33    /// [RFC 9530]: https://www.rfc-editor.org/rfc/rfc9530.html
34    pub content_digest: Option<String>,
35}
36
37/// A signature algorithm this module can produce and check.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Algorithm {
40    /// RSASSA-PKCS1-v1_5 over SHA-256. What Mastodon signs with today, because
41    /// it is what its RSA actor keys support.
42    RsaV1_5Sha256,
43    /// Ed25519, for actors whose keys are Ed25519.
44    Ed25519,
45}
46
47impl Algorithm {
48    /// The `alg` parameter value, as RFC 9421 registers it.
49    #[must_use]
50    pub fn as_str(self) -> &'static str {
51        match self {
52            Self::RsaV1_5Sha256 => "rsa-v1_5-sha256",
53            Self::Ed25519 => "ed25519",
54        }
55    }
56
57    fn from_str(value: &str) -> Option<Self> {
58        match value {
59            "rsa-v1_5-sha256" => Some(Self::RsaV1_5Sha256),
60            "ed25519" => Some(Self::Ed25519),
61            _ => None,
62        }
63    }
64}
65
66/// The private key a signature is made with.
67pub enum SigningKey<'a> {
68    /// PKCS#8 or PKCS#1 PEM-encoded RSA private key, parsed each time.
69    RsaPem(&'a str),
70    /// An RSA private key parsed once by [`PrivateKey::from_pem`].
71    Rsa(&'a PrivateKey),
72    /// A raw 32-byte Ed25519 seed.
73    Ed25519(&'a [u8; 32]),
74}
75
76impl SigningKey<'_> {
77    fn algorithm(&self) -> Algorithm {
78        match self {
79            Self::RsaPem(_) | Self::Rsa(_) => Algorithm::RsaV1_5Sha256,
80            Self::Ed25519(_) => Algorithm::Ed25519,
81        }
82    }
83
84    fn sign(&self, message: &[u8]) -> Result<String, Error> {
85        match self {
86            Self::RsaPem(pem) => crate::signature::rsa_sign_pkcs1v15(pem, message),
87            Self::Rsa(key) => Ok(key.sign(message)),
88            Self::Ed25519(seed) => {
89                use ed25519_dalek::Signer as _;
90
91                let key = ed25519_dalek::SigningKey::from_bytes(seed);
92                Ok(BASE64.encode(key.sign(message).to_bytes()))
93            }
94        }
95    }
96}
97
98/// The public key a signature is checked against.
99pub enum VerifyingKey<'a> {
100    /// PEM-encoded RSA public key.
101    RsaPem(&'a str),
102    /// A raw 32-byte Ed25519 public key.
103    Ed25519(&'a [u8; 32]),
104}
105
106impl VerifyingKey<'_> {
107    fn algorithm(&self) -> Algorithm {
108        match self {
109            Self::RsaPem(_) => Algorithm::RsaV1_5Sha256,
110            Self::Ed25519(_) => Algorithm::Ed25519,
111        }
112    }
113
114    fn verify(&self, message: &[u8], signature_b64: &str) -> Result<(), Error> {
115        match self {
116            Self::RsaPem(pem) => crate::signature::rsa_verify_pkcs1v15(pem, message, signature_b64),
117            Self::Ed25519(key) => {
118                use ed25519_dalek::Verifier as _;
119
120                let bytes = BASE64
121                    .decode(signature_b64)
122                    .map_err(|e| Error::Malformed(format!("signature is not base64: {e}")))?;
123                let signature = ed25519_dalek::Signature::from_slice(&bytes)
124                    .map_err(|e| Error::Malformed(format!("Ed25519 signature: {e}")))?;
125                ed25519_dalek::VerifyingKey::from_bytes(key)
126                    .map_err(|e| Error::Key(format!("Ed25519 public key: {e}")))?
127                    .verify(message, &signature)
128                    .map_err(|_| Error::Invalid)
129            }
130        }
131    }
132}
133
134/// The label a signature is published under. Any label works; a message can
135/// carry several, which is how a proxy adds its own alongside the client's.
136const LABEL: &str = "sig1";
137
138/// Sign an outgoing request.
139///
140/// Covers `@method` and `@target-uri`, plus `content-digest` when there is a
141/// body — the components Mastodon covers, so that a Mastodon peer verifies
142/// what it expects to. The algorithm follows from the key.
143///
144/// # Arguments
145/// * `method` – HTTP method; canonicalised to uppercase for `@method`
146/// * `url`    – full request URL, which becomes `@target-uri`
147/// * `body`   – request body, or `None` for a bodyless request
148/// * `key_id` – `keyid` parameter, the URI identifying the key
149/// * `key`    – the private key to sign with
150/// * `now`    – the time, in seconds since the Unix epoch, which becomes
151///   `created`
152///
153/// # Errors
154/// Returns an error if the key cannot be parsed or the signature cannot be made.
155pub fn sign_request(
156    method: &str,
157    url: &str,
158    body: Option<&[u8]>,
159    key_id: &str,
160    key: &SigningKey<'_>,
161    now: i64,
162) -> Result<SignedHeaders, Error> {
163    let content_digest = body.map(content_digest);
164
165    let mut components = vec![
166        ("@method".to_string(), method.to_uppercase()),
167        ("@target-uri".to_string(), url.to_string()),
168    ];
169    if let Some(digest) = &content_digest {
170        components.push(("content-digest".to_string(), digest.clone()));
171    }
172
173    let params = signature_params(
174        &components
175            .iter()
176            .map(|(name, _)| name.as_str())
177            .collect::<Vec<_>>(),
178        now,
179        key_id,
180        key.algorithm(),
181    );
182    let base = signature_base(&components, &params);
183    let signature = key.sign(base.as_bytes())?;
184
185    Ok(SignedHeaders {
186        signature_input: format!("{LABEL}={params}"),
187        signature: format!("{LABEL}=:{signature}:"),
188        content_digest,
189    })
190}
191
192/// The `Content-Digest` field value for a body: a structured-field dictionary
193/// whose `sha-256` member holds the digest as a byte sequence (RFC 9530).
194pub fn content_digest(body: &[u8]) -> String {
195    format!("sha-256=:{}:", BASE64.encode(Sha256::digest(body)))
196}
197
198/// The `@signature-params` value: the covered component list and its
199/// parameters, which is both sent as `Signature-Input` and signed as the last
200/// line of the base.
201fn signature_params(
202    components: &[&str],
203    created: i64,
204    key_id: &str,
205    algorithm: Algorithm,
206) -> String {
207    let covered = components
208        .iter()
209        .map(|name| format!("\"{name}\""))
210        .collect::<Vec<_>>()
211        .join(" ");
212    // No `alg`. RFC 9421 makes it optional, and Mastodon's signer (Linzer)
213    // emits only `created` and `keyid`; a verifier that knows the key knows the
214    // algorithm. It is still honoured on the way in, where a peer that does
215    // send it must not have it ignored.
216    let _ = algorithm;
217    format!("({covered});created={created};keyid=\"{key_id}\"")
218}
219
220/// Build the signature base (RFC 9421 §2.5).
221///
222/// One line per covered component, `"name": value`, then `"@signature-params"`
223/// with the parameters. Lines are separated by a newline and the last one — the
224/// parameters — carries no trailing newline.
225fn signature_base(components: &[(String, String)], params: &str) -> String {
226    let mut base = String::new();
227    for (name, value) in components {
228        base.push_str(&format!("\"{name}\": {value}\n"));
229    }
230    base.push_str(&format!("\"@signature-params\": {params}"));
231    base
232}
233
234/// Verify an [RFC 9421] signature on an inbound request.
235///
236/// The caller supplies the request as it saw it: the method, the URI it was
237/// made to, the `Signature-Input` and `Signature` field values, the
238/// `Content-Digest` if one was sent, and its header fields. A covered header
239/// field is taken from `headers` as RFC 9421 §2.1 reads one — every value it
240/// was sent with, trimmed and joined by `, ` — and the derived components a
241/// federation request can be rebuilt from are `@method`, `@target-uri`,
242/// `@authority`, `@scheme`, `@path`, `@query` and `@request-target`. A
243/// signature covering anything else, or a header that was not sent, is
244/// rejected rather than verified against a base guessed from what is missing.
245///
246/// Fedify covers `host` and `date` besides the derived components; a verifier
247/// that read no header fields refused every request it signed this way.
248///
249/// The digest is checked against `body`, so a signature covering
250/// `content-digest` binds the body it was sent with.
251///
252/// [RFC 9421]: https://www.rfc-editor.org/rfc/rfc9421.html
253///
254/// # Errors
255/// When the signature cannot be read, covers what cannot be rebuilt or was
256/// not sent, states an algorithm the key is not, the body does not match its
257/// digest, or the signature does not verify.
258#[allow(clippy::too_many_arguments)]
259pub fn verify_request(
260    method: &str,
261    target_uri: &str,
262    signature_input: &str,
263    signature: &str,
264    content_digest: Option<&str>,
265    body: &[u8],
266    headers: &[(&str, &str)],
267    key: &VerifyingKey<'_>,
268) -> Result<(), Error> {
269    let input = Input::parse(signature_input)?;
270    let sig_b64 = BASE64.encode(signature_bytes(signature, &input.label)?);
271    let Input {
272        covered, params, ..
273    } = &input;
274
275    // A stated algorithm must be one this module knows *and* the one the key
276    // can check: a signature claiming `ed25519` must not be waved through an
277    // RSA verification, or vice versa.
278    if let Some(alg) = input.string("alg") {
279        let stated = Algorithm::from_str(&alg)
280            .ok_or_else(|| Error::Unsupported(format!("algorithm {alg:?}")))?;
281        if stated != key.algorithm() {
282            return Err(Error::WrongKey(format!(
283                "signature claims {} but the key is {}",
284                stated.as_str(),
285                key.algorithm().as_str()
286            )));
287        }
288    }
289
290    let url = url::Url::parse(target_uri)
291        .map_err(|error| Error::Malformed(format!("target URI {target_uri:?}: {error}")))?;
292    let mut components = Vec::with_capacity(covered.len());
293    for name in covered {
294        let value = match name.as_str() {
295            "@method" => method.to_uppercase(),
296            "@target-uri" => target_uri.to_string(),
297            // The host, and the port when it is not the scheme's default.
298            "@authority" => {
299                let host = url
300                    .host_str()
301                    .ok_or_else(|| Error::Malformed("target URI has no authority".into()))?;
302                match url.port() {
303                    Some(port) => format!("{host}:{port}"),
304                    None => host.to_string(),
305                }
306            }
307            "@path" => url.path().to_string(),
308            "@scheme" => url.scheme().to_string(),
309            "@query" => format!("?{}", url.query().unwrap_or("")),
310            "@request-target" => match url.query() {
311                Some(query) => format!("{}?{query}", url.path()),
312                None => url.path().to_string(),
313            },
314            "content-digest" => {
315                let sent = content_digest.ok_or_else(|| Error::Missing("content-digest".into()))?;
316                if !crate::digest::content_digest_matches(sent, body) {
317                    return Err(Error::DigestMismatch);
318                }
319                sent.to_string()
320            }
321            derived if derived.starts_with('@') => {
322                return Err(Error::Unsupported(format!("component {derived:?}")));
323            }
324            field => {
325                let values: Vec<&str> = headers
326                    .iter()
327                    .filter(|(name, _)| name.eq_ignore_ascii_case(field))
328                    .map(|(_, value)| value.trim())
329                    .collect();
330                if values.is_empty() {
331                    return Err(Error::Missing(field.to_owned()));
332                }
333                values.join(", ")
334            }
335        };
336        components.push((name.clone(), value));
337    }
338
339    let base = signature_base(&components, params);
340    key.verify(base.as_bytes(), &sig_b64)
341}
342
343/// The `keyid` parameter of a `Signature-Input`: the URI identifying the key
344/// that signed, which the verifier resolves to a public key.
345pub fn key_id(signature_input: &str) -> Option<String> {
346    Input::parse(signature_input).ok()?.string("keyid")
347}
348
349/// The components a `Signature-Input` claims to cover.
350///
351/// Callers check that a signature covers what their protocol requires before
352/// spending anything on verifying it.
353pub fn covered_components(signature_input: &str) -> Option<Vec<String>> {
354    Some(Input::parse(signature_input).ok()?.covered)
355}
356
357/// The `created` parameter of a `Signature-Input`, as a Unix timestamp.
358///
359/// Callers bound a signature's age with it; RFC 9421 leaves the policy to the
360/// application.
361pub fn created_at(signature_input: &str) -> Option<i64> {
362    Input::parse(signature_input).ok()?.integer("created")
363}
364
365/// The `expires` parameter of a `Signature-Input`, as a Unix timestamp.
366pub fn expires_at(signature_input: &str) -> Option<i64> {
367    Input::parse(signature_input).ok()?.integer("expires")
368}
369
370/// Sign a response over `@status` and `content-digest`, as Linzer signs one
371/// (`Linzer.sign!(response, key:, components: %w(@status content-digest))`)
372/// and the Fediverse Auxiliary Service Provider protocol has both sides sign
373/// theirs.
374///
375/// `content_digest` is the response's `Content-Digest` as it is sent
376/// ([`content_digest`] of its body). The signature names its key only when
377/// given `key_id`: a response answers a request, and its key may be one both
378/// sides already know.
379///
380/// # Errors
381/// Returns an error if the key cannot be parsed or the signature cannot be made.
382pub fn sign_response(
383    status: u16,
384    content_digest: &str,
385    key_id: Option<&str>,
386    key: &SigningKey<'_>,
387    now: i64,
388) -> Result<SignedHeaders, Error> {
389    let components = vec![
390        ("@status".to_string(), status.to_string()),
391        ("content-digest".to_string(), content_digest.to_string()),
392    ];
393    let mut params = format!("(\"@status\" \"content-digest\");created={now}");
394    if let Some(key_id) = key_id {
395        params.push_str(&format!(";keyid=\"{key_id}\""));
396    }
397    let base = signature_base(&components, &params);
398    let signature = key.sign(base.as_bytes())?;
399    Ok(SignedHeaders {
400        signature_input: format!("{LABEL}={params}"),
401        signature: format!("{LABEL}=:{signature}:"),
402        content_digest: Some(content_digest.to_string()),
403    })
404}
405
406/// Verify an [RFC 9421] signature on a response, given its status, the
407/// `Signature-Input` and `Signature` field values, and its header fields.
408///
409/// The derived component a response can be rebuilt from is `@status`; a
410/// covered header field, `content-digest` among them, is taken from
411/// `headers` as [`verify_request`] takes one. Whether `Content-Digest`
412/// describes the body, and whether the signature has expired
413/// ([`expires_at`]) or is too old ([`created_at`]), are the caller's to
414/// check: the policy is the protocol's.
415///
416/// [RFC 9421]: https://www.rfc-editor.org/rfc/rfc9421.html
417///
418/// # Errors
419/// When the signature cannot be read, covers what cannot be rebuilt or was
420/// not sent, states an algorithm the key is not, or does not verify.
421pub fn verify_response(
422    status: u16,
423    signature_input: &str,
424    signature: &str,
425    headers: &[(&str, &str)],
426    key: &VerifyingKey<'_>,
427) -> Result<(), Error> {
428    let input = Input::parse(signature_input)?;
429    let sig_b64 = BASE64.encode(signature_bytes(signature, &input.label)?);
430    if let Some(alg) = input.string("alg") {
431        let stated = Algorithm::from_str(&alg)
432            .ok_or_else(|| Error::Unsupported(format!("algorithm {alg:?}")))?;
433        if stated != key.algorithm() {
434            return Err(Error::WrongKey(format!(
435                "signature claims {} but the key is {}",
436                stated.as_str(),
437                key.algorithm().as_str()
438            )));
439        }
440    }
441    let mut components = Vec::with_capacity(input.covered.len());
442    for name in &input.covered {
443        let value = match name.as_str() {
444            "@status" => status.to_string(),
445            derived if derived.starts_with('@') => {
446                return Err(Error::Unsupported(format!("component {derived:?}")));
447            }
448            field => {
449                let values: Vec<&str> = headers
450                    .iter()
451                    .filter(|(name, _)| name.eq_ignore_ascii_case(field))
452                    .map(|(_, value)| value.trim())
453                    .collect();
454                if values.is_empty() {
455                    return Err(Error::Missing(field.to_owned()));
456                }
457                values.join(", ")
458            }
459        };
460        components.push((name.clone(), value));
461    }
462    let base = signature_base(&components, &input.params);
463    key.verify(base.as_bytes(), &sig_b64)
464}
465
466/// The signature a `Signature-Input` describes, read as the structured-field
467/// dictionary it is (RFC 8941). A message may carry several signatures, as
468/// when a proxy adds its own beside the sender's; the first is the one read,
469/// and its label is where its value is found in `Signature`.
470struct Input {
471    label: String,
472    /// The covered components, in order.
473    covered: Vec<String>,
474    /// The `@signature-params` line: the component list and its parameters,
475    /// serialized as RFC 8941 serializes them, which is what the signer
476    /// signed however the field was spaced on the wire.
477    params: String,
478    parameters: sfv::Parameters,
479}
480
481impl Input {
482    fn parse(header: &str) -> Result<Self, Error> {
483        let malformed = |why: String| Error::Malformed(format!("Signature-Input: {why}"));
484        let dictionary = sfv::Parser::new(header)
485            .parse::<sfv::Dictionary>()
486            .map_err(|error| malformed(error.to_string()))?;
487        let (label, entry) = dictionary
488            .first()
489            .ok_or_else(|| malformed("no signature".into()))?;
490        let sfv::ListEntry::InnerList(list) = entry else {
491            return Err(malformed("not a component list".into()));
492        };
493        let mut covered = Vec::with_capacity(list.items.len());
494        for item in &list.items {
495            let name = item
496                .bare_item
497                .as_string()
498                .ok_or_else(|| malformed("a covered component is not a string".into()))?;
499            // Parameters select a part of a component (`;sf`, `;key`, `;req`),
500            // which is not rebuilt here, so a signature using them is refused
501            // rather than verified against the whole.
502            if !item.params.is_empty() {
503                return Err(Error::Unsupported(format!(
504                    "component {:?} with parameters",
505                    name.as_str()
506                )));
507            }
508            let name = name.as_str().to_owned();
509            if covered.contains(&name) {
510                return Err(malformed(format!("{name:?} is covered twice")));
511            }
512            covered.push(name);
513        }
514        let mut serializer = sfv::ListSerializer::new();
515        serializer.members([entry]);
516        let params = serializer
517            .finish()
518            .ok_or_else(|| malformed("no signature".into()))?;
519        Ok(Self {
520            label: label.as_str().to_owned(),
521            covered,
522            params,
523            parameters: list.params.clone(),
524        })
525    }
526
527    fn string(&self, name: &str) -> Option<String> {
528        let key = sfv::KeyRef::from_str(name).ok()?;
529        Some(self.parameters.get(key)?.as_string()?.as_str().to_owned())
530    }
531
532    fn integer(&self, name: &str) -> Option<i64> {
533        let key = sfv::KeyRef::from_str(name).ok()?;
534        Some(self.parameters.get(key)?.as_integer()?.into())
535    }
536}
537
538/// The signature published under `label` in a `Signature` field.
539fn signature_bytes(header: &str, label: &str) -> Result<Vec<u8>, Error> {
540    let malformed = |why: String| Error::Malformed(format!("Signature: {why}"));
541    let dictionary = sfv::Parser::new(header)
542        .parse::<sfv::Dictionary>()
543        .map_err(|error| malformed(error.to_string()))?;
544    let key = sfv::KeyRef::from_str(label).map_err(|error| malformed(error.to_string()))?;
545    let not_bytes = || malformed("not a byte sequence".into());
546    match dictionary.get(key) {
547        Some(sfv::ListEntry::Item(item)) => Ok(item
548            .bare_item
549            .as_byte_sequence()
550            .ok_or_else(not_bytes)?
551            .to_vec()),
552        Some(sfv::ListEntry::InnerList(_)) => Err(not_bytes()),
553        None => Err(malformed(format!("no signature labelled {label:?}"))),
554    }
555}
556
557#[cfg(test)]
558mod tests {
559    use super::*;
560
561    /// The `test-key-rsa` private key from RFC 9421 Appendix B.1.1.
562    const TEST_KEY_RSA: &str = include_str!("../tests/fixtures/rfc9421_test_key_rsa.pem");
563
564    /// When the tests sign: any time will do, as verifying does not check it.
565    const NOW: i64 = 1_759_000_000;
566
567    /// RFC 9421 Appendix B.4: a signature base covering derived components and
568    /// a content digest, built with `rsa-v1_5-sha256`. Reproducing the document
569    /// 's own base byte-for-byte is what proves the canonicalisation is right.
570    #[test]
571    fn builds_the_signature_base_from_the_rfc() {
572        let components = vec![
573            ("@method".to_string(), "POST".to_string()),
574            (
575                "@authority".to_string(),
576                "origin.host.internal.example".to_string(),
577            ),
578            ("@path".to_string(), "/foo".to_string()),
579            (
580                "content-digest".to_string(),
581                "sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:".to_string(),
582            ),
583            ("content-type".to_string(), "application/json".to_string()),
584            ("content-length".to_string(), "18".to_string()),
585            (
586                "forwarded".to_string(),
587                "for=192.0.2.123;host=example.com;proto=https".to_string(),
588            ),
589        ];
590        let params = "(\"@method\" \"@authority\" \"@path\" \"content-digest\" \
591             \"content-type\" \"content-length\" \"forwarded\")\
592             ;created=1618884480;keyid=\"test-key-rsa\";alg=\"rsa-v1_5-sha256\"\
593             ;expires=1618884540";
594
595        let base = signature_base(&components, params);
596
597        let expected = concat!(
598            "\"@method\": POST\n",
599            "\"@authority\": origin.host.internal.example\n",
600            "\"@path\": /foo\n",
601            "\"content-digest\": sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:\n",
602            "\"content-type\": application/json\n",
603            "\"content-length\": 18\n",
604            "\"forwarded\": for=192.0.2.123;host=example.com;proto=https\n",
605            "\"@signature-params\": (\"@method\" \"@authority\" \"@path\" \"content-digest\" \"content-type\" \"content-length\" \"forwarded\");created=1618884480;keyid=\"test-key-rsa\";alg=\"rsa-v1_5-sha256\";expires=1618884540",
606        );
607        assert_eq!(base, expected);
608    }
609
610    /// The same example's signature output. PKCS#1 v1.5 is deterministic, so
611    /// the bytes must match the ones the RFC publishes.
612    #[test]
613    fn reproduces_the_rfc_signature() {
614        let base = concat!(
615            "\"@method\": POST\n",
616            "\"@authority\": origin.host.internal.example\n",
617            "\"@path\": /foo\n",
618            "\"content-digest\": sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:\n",
619            "\"content-type\": application/json\n",
620            "\"content-length\": 18\n",
621            "\"forwarded\": for=192.0.2.123;host=example.com;proto=https\n",
622            "\"@signature-params\": (\"@method\" \"@authority\" \"@path\" \"content-digest\" \"content-type\" \"content-length\" \"forwarded\");created=1618884480;keyid=\"test-key-rsa\";alg=\"rsa-v1_5-sha256\";expires=1618884540",
623        );
624
625        let signature = crate::signature::rsa_sign_pkcs1v15(TEST_KEY_RSA, base.as_bytes()).unwrap();
626
627        let expected = concat!(
628            "S6ZzPXSdAMOPjN/6KXfXWNO/f7V6cHm7BXYUh3YD/fRad4BCaRZxP+JH+8XY1I6+8Cy",
629            "+CM5g92iHgxtRPz+MjniOaYmdkDcnL9cCpXJleXsOckpURl49GwiyUpZ10KHgOEe11s",
630            "x3G2gxI8S0jnxQB+Pu68U9vVcasqOWAEObtNKKZd8tSFu7LB5YAv0RAGhB8tmpv7sFn",
631            "Im9y+7X5kXQfi8NMaZaA8i2ZHwpBdg7a6CMfwnnrtflzvZdXAsD3LH2TwevU+/PBPv0",
632            "B6NMNk93wUs/vfJvye+YuI87HU38lZHowtznbLVdp770I6VHR6WfgS9ddzirrswsE1w",
633            "5o0LV/g==",
634        );
635        assert_eq!(signature, expected);
636    }
637
638    #[test]
639    fn signs_a_post_the_way_mastodon_expects() {
640        let body = br#"{"type":"Create"}"#;
641        let signed = sign_request(
642            "post",
643            "https://remote.example/inbox",
644            Some(body),
645            "https://local.example/users/alice#main-key",
646            &SigningKey::RsaPem(TEST_KEY_RSA),
647            NOW,
648        )
649        .unwrap();
650
651        assert!(
652            signed
653                .signature_input
654                .starts_with("sig1=(\"@method\" \"@target-uri\" \"content-digest\");created=")
655        );
656        assert!(
657            signed
658                .signature_input
659                .contains("keyid=\"https://local.example/users/alice#main-key\"")
660        );
661        assert!(
662            signed
663                .signature_input
664                .ends_with("keyid=\"https://local.example/users/alice#main-key\"")
665        );
666        assert!(signed.signature.starts_with("sig1=:"));
667        assert!(signed.signature.ends_with(':'));
668        assert_eq!(signed.content_digest, Some(content_digest(body)));
669    }
670
671    #[test]
672    fn a_request_without_a_body_covers_no_digest() {
673        let signed = sign_request(
674            "get",
675            "https://remote.example/users/bob",
676            None,
677            "https://local.example/actor#main-key",
678            &SigningKey::RsaPem(TEST_KEY_RSA),
679            NOW,
680        )
681        .unwrap();
682
683        assert!(
684            signed
685                .signature_input
686                .starts_with("sig1=(\"@method\" \"@target-uri\");created=")
687        );
688        assert_eq!(signed.content_digest, None);
689    }
690
691    /// The public half of `test-key-rsa`, from RFC 9421 Appendix B.1.1.
692    const TEST_KEY_RSA_PUBLIC: &str =
693        include_str!("../tests/fixtures/rfc9421_test_key_rsa_public.pem");
694
695    #[test]
696    fn verifies_what_it_signs() {
697        let body = br#"{"type":"Create"}"#;
698        let signed = sign_request(
699            "post",
700            "https://remote.example/inbox",
701            Some(body),
702            "https://local.example/users/alice#main-key",
703            &SigningKey::RsaPem(TEST_KEY_RSA),
704            NOW,
705        )
706        .unwrap();
707
708        verify_request(
709            "POST",
710            "https://remote.example/inbox",
711            &signed.signature_input,
712            &signed.signature,
713            signed.content_digest.as_deref(),
714            body,
715            &[],
716            &VerifyingKey::RsaPem(TEST_KEY_RSA_PUBLIC),
717        )
718        .expect("a signature this module produced should verify");
719    }
720
721    #[test]
722    fn rejects_a_body_the_digest_does_not_cover() {
723        let signed = sign_request(
724            "post",
725            "https://remote.example/inbox",
726            Some(b"original"),
727            "https://local.example/users/alice#main-key",
728            &SigningKey::RsaPem(TEST_KEY_RSA),
729            NOW,
730        )
731        .unwrap();
732
733        let err = verify_request(
734            "POST",
735            "https://remote.example/inbox",
736            &signed.signature_input,
737            &signed.signature,
738            signed.content_digest.as_deref(),
739            b"swapped",
740            &[],
741            &VerifyingKey::RsaPem(TEST_KEY_RSA_PUBLIC),
742        )
743        .unwrap_err();
744        assert_eq!(err, Error::DigestMismatch);
745    }
746
747    #[test]
748    fn rejects_a_request_sent_somewhere_else() {
749        let body = b"body";
750        let signed = sign_request(
751            "post",
752            "https://remote.example/inbox",
753            Some(body),
754            "https://local.example/users/alice#main-key",
755            &SigningKey::RsaPem(TEST_KEY_RSA),
756            NOW,
757        )
758        .unwrap();
759
760        assert!(
761            verify_request(
762                "POST",
763                "https://elsewhere.example/inbox",
764                &signed.signature_input,
765                &signed.signature,
766                signed.content_digest.as_deref(),
767                body,
768                &[],
769                &VerifyingKey::RsaPem(TEST_KEY_RSA_PUBLIC),
770            )
771            .is_err()
772        );
773    }
774
775    #[test]
776    fn reads_the_first_signature_when_a_proxy_has_added_its_own() {
777        let body = br#"{"type":"Create"}"#;
778        let signed = sign_request(
779            "post",
780            "https://remote.example/inbox",
781            Some(body),
782            "https://local.example/users/alice#main-key",
783            &SigningKey::RsaPem(TEST_KEY_RSA),
784            NOW,
785        )
786        .unwrap();
787        // Spaced as RFC 8941 allows, which is not how it was signed.
788        let input = signed
789            .signature_input
790            .replacen("sig1=(", "sig1=(  ", 1)
791            .replacen(");", "  );", 1);
792        let input = format!("{input}, proxy=(\"@method\");created=1;keyid=\"p;)=\"");
793        let signature = format!("{}, proxy=:AAAA:", signed.signature);
794
795        verify_request(
796            "POST",
797            "https://remote.example/inbox",
798            &input,
799            &signature,
800            signed.content_digest.as_deref(),
801            body,
802            &[],
803            &VerifyingKey::RsaPem(TEST_KEY_RSA_PUBLIC),
804        )
805        .expect("the sender's signature verifies beside the proxy's");
806        assert_eq!(
807            key_id(&input).as_deref(),
808            Some("https://local.example/users/alice#main-key")
809        );
810    }
811
812    #[test]
813    fn the_authority_has_the_port_when_it_is_not_the_default() {
814        let key = SigningKey::RsaPem(TEST_KEY_RSA);
815        let params = "(\"@method\" \"@authority\");created=1618884480;keyid=\"k\"";
816        let base = signature_base(
817            &[
818                ("@method".into(), "GET".into()),
819                ("@authority".into(), "remote.example:8443".into()),
820            ],
821            params,
822        );
823        let signature = key.sign(base.as_bytes()).unwrap();
824
825        verify_request(
826            "GET",
827            "https://remote.example:8443/users/bob",
828            &format!("sig1={params}"),
829            &format!("sig1=:{signature}:"),
830            None,
831            b"",
832            &[],
833            &VerifyingKey::RsaPem(TEST_KEY_RSA_PUBLIC),
834        )
835        .expect("the authority is the host and its port");
836    }
837
838    #[test]
839    fn rejects_a_component_covered_twice_or_in_part() {
840        for input in [
841            "sig1=(\"@method\" \"@method\");created=1;keyid=\"k\"",
842            "sig1=(\"@method\" \"x-dict\";key=\"a\");created=1;keyid=\"k\"",
843        ] {
844            assert!(
845                verify_request(
846                    "GET",
847                    "https://remote.example/",
848                    input,
849                    "sig1=:AAAA:",
850                    None,
851                    b"",
852                    &[("x-dict", "a=1")],
853                    &VerifyingKey::RsaPem(TEST_KEY_RSA_PUBLIC),
854                )
855                .is_err(),
856                "{input}"
857            );
858            assert_eq!(covered_components(input), None, "{input}");
859        }
860    }
861
862    #[test]
863    fn rejects_components_it_cannot_rebuild() {
864        // A signature covering a header this verifier does not have must not be
865        // waved through by rebuilding a shorter base.
866        let err = verify_request(
867            "POST",
868            "https://remote.example/inbox",
869            "sig1=(\"@method\" \"x-custom\");created=1618884480;keyid=\"k\"",
870            "sig1=:AAAA:",
871            None,
872            b"",
873            &[],
874            &VerifyingKey::RsaPem(TEST_KEY_RSA_PUBLIC),
875        )
876        .unwrap_err();
877        assert_eq!(err, Error::Missing("x-custom".into()));
878    }
879
880    #[test]
881    fn reads_the_created_parameter() {
882        assert_eq!(
883            created_at("sig1=(\"@method\");created=1618884480;keyid=\"k\""),
884            Some(1618884480)
885        );
886        assert_eq!(created_at("sig1=(\"@method\");keyid=\"k\""), None);
887    }
888
889    /// `test-key-ed25519` from RFC 9421 Appendix B.1.4, as raw bytes.
890    const TEST_KEY_ED25519_SEED: [u8; 32] = [
891        0x9f, 0x83, 0x62, 0xf8, 0x7a, 0x48, 0x4a, 0x95, 0x4e, 0x6e, 0x74, 0x0c, 0x5b, 0x4c, 0x0e,
892        0x84, 0x22, 0x91, 0x39, 0xa2, 0x0a, 0xa8, 0xab, 0x56, 0xff, 0x66, 0x58, 0x6f, 0x6a, 0x7d,
893        0x29, 0xc5,
894    ];
895    const TEST_KEY_ED25519_PUBLIC: [u8; 32] = [
896        0x26, 0xb4, 0x0b, 0x8f, 0x93, 0xff, 0xf3, 0xd8, 0x97, 0x11, 0x2f, 0x7e, 0xbc, 0x58, 0x2b,
897        0x23, 0x2d, 0xbd, 0x72, 0x51, 0x7d, 0x08, 0x2f, 0xe8, 0x3c, 0xfb, 0x30, 0xdd, 0xce, 0x43,
898        0xd1, 0xbb,
899    ];
900
901    /// RFC 9421 Appendix B.2.6, signed with `test-key-ed25519`. Ed25519 is
902    /// deterministic, so the signature must come out byte-identical to the
903    /// one the document publishes.
904    #[test]
905    fn reproduces_the_rfc_ed25519_signature() {
906        let base = concat!(
907            "\"date\": Tue, 20 Apr 2021 02:07:55 GMT\n",
908            "\"@method\": POST\n",
909            "\"@path\": /foo\n",
910            "\"@authority\": example.com\n",
911            "\"content-type\": application/json\n",
912            "\"content-length\": 18\n",
913            "\"@signature-params\": (\"date\" \"@method\" \"@path\" \"@authority\" \"content-type\" \"content-length\");created=1618884473;keyid=\"test-key-ed25519\"",
914        );
915
916        let signature = SigningKey::Ed25519(&TEST_KEY_ED25519_SEED)
917            .sign(base.as_bytes())
918            .unwrap();
919
920        assert_eq!(
921            signature,
922            concat!(
923                "wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1",
924                "u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw==",
925            )
926        );
927
928        // And the published signature verifies against the published key.
929        VerifyingKey::Ed25519(&TEST_KEY_ED25519_PUBLIC)
930            .verify(base.as_bytes(), &signature)
931            .expect("the RFC's own signature must verify");
932    }
933
934    /// RFC 9421 Appendix B.2.6 end to end: the published signature over
935    /// header fields as well as derived components verifies from the request
936    /// it was made for.
937    #[test]
938    fn verifies_the_rfc_signature_over_header_fields() {
939        verify_request(
940            "POST",
941            "https://example.com/foo?param=Value&Pet=dog",
942            "sig-b26=(\"date\" \"@method\" \"@path\" \"@authority\" \"content-type\" \"content-length\");created=1618884473;keyid=\"test-key-ed25519\"",
943            "sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw==:",
944            None,
945            br#"{"hello": "world"}"#,
946            &[
947                ("Host", "example.com"),
948                ("Date", "Tue, 20 Apr 2021 02:07:55 GMT"),
949                ("Content-Type", "application/json"),
950                ("Content-Length", "18"),
951            ],
952            &VerifyingKey::Ed25519(&TEST_KEY_ED25519_PUBLIC),
953        )
954        .expect("the RFC's signature over header fields verifies");
955    }
956
957    /// A request signed as Fedify signs one — `host` and `date` covered
958    /// alongside the derived components — verifies, and does not once a
959    /// covered header has changed on the way.
960    #[test]
961    fn verifies_a_signature_covering_host_and_date_as_fedify_makes_them() {
962        let body = br#"{"type":"Create"}"#;
963        let digest = content_digest(body);
964        let target = "https://seoul.earth/users/alice/inbox";
965        let params = "(\"@method\" \"@target-uri\" \"@authority\" \"host\" \"date\" \"content-digest\");created=1759000000;keyid=\"https://hollo.example/@bob#main-key\";alg=\"rsa-v1_5-sha256\"";
966        let date = "Sun, 28 Sep 2025 00:00:00 GMT";
967        let base = signature_base(
968            &[
969                ("@method".into(), "POST".into()),
970                ("@target-uri".into(), target.into()),
971                ("@authority".into(), "seoul.earth".into()),
972                ("host".into(), "seoul.earth".into()),
973                ("date".into(), date.into()),
974                ("content-digest".into(), digest.clone()),
975            ],
976            params,
977        );
978        let signature = SigningKey::RsaPem(TEST_KEY_RSA)
979            .sign(base.as_bytes())
980            .unwrap();
981        let input = format!("sig1={params}");
982        let signed = format!("sig1=:{signature}:");
983        let headers = [("host", "seoul.earth"), ("date", date)];
984
985        verify_request(
986            "POST",
987            target,
988            &input,
989            &signed,
990            Some(&digest),
991            body,
992            &headers,
993            &VerifyingKey::RsaPem(TEST_KEY_RSA_PUBLIC),
994        )
995        .expect("a signature over host and date verifies");
996
997        let changed = [
998            ("host", "seoul.earth"),
999            ("date", "Mon, 29 Sep 2025 00:00:00 GMT"),
1000        ];
1001        assert!(
1002            verify_request(
1003                "POST",
1004                target,
1005                &input,
1006                &signed,
1007                Some(&digest),
1008                body,
1009                &changed,
1010                &VerifyingKey::RsaPem(TEST_KEY_RSA_PUBLIC),
1011            )
1012            .is_err(),
1013            "a covered header that changed must not verify"
1014        );
1015    }
1016
1017    #[test]
1018    fn signs_and_verifies_a_request_with_ed25519() {
1019        let body = br#"{"type":"Create"}"#;
1020        let signed = sign_request(
1021            "post",
1022            "https://remote.example/inbox",
1023            Some(body),
1024            "https://local.example/users/alice#ed25519-key",
1025            &SigningKey::Ed25519(&TEST_KEY_ED25519_SEED),
1026            NOW,
1027        )
1028        .unwrap();
1029
1030        assert!(
1031            !signed.signature_input.contains("alg="),
1032            "Mastodon's signer emits no alg parameter"
1033        );
1034
1035        verify_request(
1036            "POST",
1037            "https://remote.example/inbox",
1038            &signed.signature_input,
1039            &signed.signature,
1040            signed.content_digest.as_deref(),
1041            body,
1042            &[],
1043            &VerifyingKey::Ed25519(&TEST_KEY_ED25519_PUBLIC),
1044        )
1045        .expect("an Ed25519 signature this module made should verify");
1046    }
1047
1048    /// A peer that does state an algorithm must not have it ignored: a
1049    /// signature claiming one algorithm is refused against a key of another,
1050    /// rather than checked anyway.
1051    #[test]
1052    fn refuses_a_key_that_does_not_match_a_stated_algorithm() {
1053        let body = b"body";
1054        let signed = sign_request(
1055            "post",
1056            "https://remote.example/inbox",
1057            Some(body),
1058            "https://local.example/users/alice#ed25519-key",
1059            &SigningKey::Ed25519(&TEST_KEY_ED25519_SEED),
1060            NOW,
1061        )
1062        .unwrap();
1063
1064        // Eunha omits `alg`, as Mastodon does; state it the way a peer might.
1065        let with_alg = format!("{};alg=\"ed25519\"", signed.signature_input);
1066
1067        let err = verify_request(
1068            "POST",
1069            "https://remote.example/inbox",
1070            &with_alg,
1071            &signed.signature,
1072            signed.content_digest.as_deref(),
1073            body,
1074            &[],
1075            &VerifyingKey::RsaPem(TEST_KEY_RSA_PUBLIC),
1076        )
1077        .unwrap_err();
1078        assert!(
1079            matches!(&err, Error::WrongKey(why) if why.contains("claims ed25519")),
1080            "{err}"
1081        );
1082    }
1083
1084    /// An algorithm nobody here implements is refused outright, rather than
1085    /// verified with whatever key happens to be to hand.
1086    #[test]
1087    fn refuses_an_unknown_algorithm() {
1088        let err = verify_request(
1089            "POST",
1090            "https://remote.example/inbox",
1091            "sig1=(\"@method\" \"@target-uri\");created=1;keyid=\"k\";alg=\"rsa-pss-sha512\"",
1092            "sig1=:AAAA:",
1093            None,
1094            b"",
1095            &[],
1096            &VerifyingKey::RsaPem(TEST_KEY_RSA_PUBLIC),
1097        )
1098        .unwrap_err();
1099        assert!(matches!(err, Error::Unsupported(_)), "{err}");
1100    }
1101
1102    #[test]
1103    fn content_digest_is_an_rfc9530_byte_sequence() {
1104        // The empty SHA-256 digest, wrapped as a structured-field byte sequence.
1105        assert_eq!(
1106            content_digest(b""),
1107            "sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:"
1108        );
1109    }
1110
1111    #[test]
1112    fn verifies_the_responses_it_signs() {
1113        let digest = content_digest(b"{}");
1114        let signed = sign_response(
1115            201,
1116            &digest,
1117            None,
1118            &SigningKey::Ed25519(&TEST_KEY_ED25519_SEED),
1119            1_700_000_000,
1120        )
1121        .unwrap();
1122        assert_eq!(
1123            signed.signature_input,
1124            "sig1=(\"@status\" \"content-digest\");created=1700000000"
1125        );
1126        let key = VerifyingKey::Ed25519(&TEST_KEY_ED25519_PUBLIC);
1127        let headers = [("Content-Digest", digest.as_str())];
1128        verify_response(
1129            201,
1130            &signed.signature_input,
1131            &signed.signature,
1132            &headers,
1133            &key,
1134        )
1135        .unwrap();
1136        assert!(
1137            verify_response(
1138                200,
1139                &signed.signature_input,
1140                &signed.signature,
1141                &headers,
1142                &key
1143            )
1144            .is_err()
1145        );
1146        let other = content_digest(b"[]");
1147        assert!(
1148            verify_response(
1149                201,
1150                &signed.signature_input,
1151                &signed.signature,
1152                &[("content-digest", other.as_str())],
1153                &key,
1154            )
1155            .is_err()
1156        );
1157        assert!(matches!(
1158            verify_response(201, &signed.signature_input, &signed.signature, &[], &key),
1159            Err(Error::Missing(_))
1160        ));
1161
1162        let named = sign_response(
1163            200,
1164            &digest,
1165            Some("https://a.example/key"),
1166            &SigningKey::Ed25519(&TEST_KEY_ED25519_SEED),
1167            1_700_000_000,
1168        )
1169        .unwrap();
1170        assert_eq!(
1171            key_id(&named.signature_input).as_deref(),
1172            Some("https://a.example/key")
1173        );
1174        verify_response(
1175            200,
1176            &named.signature_input,
1177            &named.signature,
1178            &headers,
1179            &key,
1180        )
1181        .unwrap();
1182    }
1183
1184    #[test]
1185    fn a_response_signature_covers_only_what_a_response_has() {
1186        let input = "sig1=(\"@method\" \"@status\");created=1";
1187        let signature = "sig1=:AAAA:";
1188        assert!(matches!(
1189            verify_response(
1190                200,
1191                input,
1192                signature,
1193                &[],
1194                &VerifyingKey::Ed25519(&TEST_KEY_ED25519_PUBLIC)
1195            ),
1196            Err(Error::Unsupported(_))
1197        ));
1198    }
1199}