Skip to main content

actpub_httpsig/rfc9421/
verify.rs

1//! RFC 9421 request verifier.
2
3use chrono::{DateTime, Utc};
4use http::Request;
5
6use crate::error::Error;
7use crate::key::{Algorithm, VerifyingKey};
8use crate::policy::VerifyPolicy;
9use crate::rfc9421::components::build_signature_base;
10use crate::rfc9421::signature::{SIGNATURE_HEADER, parse_signature_dict};
11use crate::rfc9421::signature_input::{
12    SIGNATURE_INPUT_HEADER, SignatureInput, parse_signature_input_dict,
13};
14
15/// Successful RFC 9421 verification report.
16#[derive(Debug, Clone)]
17#[non_exhaustive]
18pub struct Rfc9421Verified {
19    /// Label of the signature that matched.
20    pub label: String,
21    /// Parsed `Signature-Input:` entry for that label.
22    pub input: SignatureInput,
23    /// Rebuilt signature base string, for audit / logging.
24    pub signature_base: String,
25}
26
27/// Verifies an RFC 9421-signed request against a key returned by
28/// `resolve_key(key_id)`.
29///
30/// When multiple labels are present, this function picks the **first
31/// label whose key the resolver accepts** and returns its report. If the
32/// resolver fails for every label the last error is returned.
33///
34/// # Errors
35///
36/// Returns [`Error::MissingHeader`] if either header is absent, and
37/// [`Error::VerificationFailed`] if no label produces a valid signature.
38/// See also [`Error::MalformedSignatureHeader`] and
39/// [`Error::KeyResolution`].
40pub fn rfc9421_verify<B, F>(req: &Request<B>, resolve_key: F) -> Result<Rfc9421Verified, Error>
41where
42    F: FnMut(&str) -> Result<VerifyingKey, Error>,
43{
44    rfc9421_verify_with_policy(
45        req,
46        &VerifyPolicy::no_freshness_check(),
47        Utc::now(),
48        resolve_key,
49    )
50}
51
52/// Verifies an RFC 9421-signed request **with replay-protection**.
53///
54/// Equivalent to [`rfc9421_verify`] except that `policy` is consulted
55/// for every candidate label to reject stale, future-dated or expired
56/// timestamps against `now`.
57///
58/// # Errors
59///
60/// Same as [`rfc9421_verify`] plus [`Error::TimestampTooOld`],
61/// [`Error::TimestampInFuture`], [`Error::TimestampExpired`] and
62/// [`Error::TimestampMissing`] when the policy is violated.
63pub fn rfc9421_verify_with_policy<B, F>(
64    req: &Request<B>,
65    policy: &VerifyPolicy,
66    now: DateTime<Utc>,
67    mut resolve_key: F,
68) -> Result<Rfc9421Verified, Error>
69where
70    F: FnMut(&str) -> Result<VerifyingKey, Error>,
71{
72    let date_header = req
73        .headers()
74        .get(http::header::DATE)
75        .and_then(|v| v.to_str().ok())
76        .map(str::to_owned);
77    let input_raw = req
78        .headers()
79        .get(SIGNATURE_INPUT_HEADER)
80        .ok_or(Error::MissingHeader(SIGNATURE_INPUT_HEADER))?
81        .to_str()
82        .map_err(|e| Error::InvalidHeader {
83            name: SIGNATURE_INPUT_HEADER,
84            reason: e.to_string(),
85        })?;
86    let sig_raw = req
87        .headers()
88        .get(SIGNATURE_HEADER)
89        .ok_or(Error::MissingHeader(SIGNATURE_HEADER))?
90        .to_str()
91        .map_err(|e| Error::InvalidHeader {
92            name: SIGNATURE_HEADER,
93            reason: e.to_string(),
94        })?;
95
96    let inputs = parse_signature_input_dict(input_raw)?;
97    let sigs = parse_signature_dict(sig_raw)?;
98
99    if inputs.is_empty() {
100        return Err(Error::MalformedSignatureHeader(
101            "empty Signature-Input dictionary".into(),
102        ));
103    }
104
105    if !policy.allow_multiple_signatures && inputs.len() > 1 {
106        return Err(Error::MalformedSignatureHeader(format!(
107            "Signature-Input carries {} labels but policy allows only one",
108            inputs.len()
109        )));
110    }
111
112    let mut last_err: Option<Error> = None;
113    for (label, input) in inputs {
114        let Some((_, sig_bytes)) = sigs.iter().find(|(l, _)| l == &label) else {
115            last_err = Some(Error::MalformedSignatureHeader(format!(
116                "no Signature entry for label `{label}`"
117            )));
118            continue;
119        };
120
121        // Freshness check on a per-label basis so that one rotated
122        // label does not invalidate a sibling signature.
123        if let Err(e) = policy.check(input.created, input.expires, date_header.as_deref(), now) {
124            last_err = Some(e);
125            continue;
126        }
127
128        let Some(key_id) = input.keyid.as_deref() else {
129            last_err = Some(Error::MissingSignatureParameter("keyid"));
130            continue;
131        };
132
133        let key = match resolve_key(key_id) {
134            Ok(k) => k,
135            Err(e) => {
136                last_err = Some(Error::KeyResolution(e.to_string()));
137                continue;
138            }
139        };
140
141        if let Some(hint) = input.algorithm.as_deref() {
142            match parse_alg_hint(hint) {
143                Ok(Some(hinted)) if hinted != key.algorithm() => {
144                    last_err = Some(Error::VerificationFailed);
145                    continue;
146                }
147                Ok(_) => {}
148                Err(e) => {
149                    last_err = Some(e);
150                    continue;
151                }
152            }
153        }
154
155        let inner_list = input.serialise_inner_list();
156        let base = build_signature_base(req, &input.components, &inner_list)?;
157
158        if key.verify(base.as_bytes(), sig_bytes).is_err() {
159            last_err = Some(Error::VerificationFailed);
160            continue;
161        }
162
163        return Ok(Rfc9421Verified {
164            label,
165            input,
166            signature_base: base,
167        });
168    }
169
170    Err(last_err.unwrap_or(Error::VerificationFailed))
171}
172
173fn parse_alg_hint(hint: &str) -> Result<Option<Algorithm>, Error> {
174    match hint {
175        "rsa-v1_5-sha256" | "rsa-sha256" => Ok(Some(Algorithm::RsaSha256)),
176        "ed25519" => Ok(Some(Algorithm::Ed25519)),
177        other => Err(Error::UnsupportedAlgorithm(other.to_owned())),
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use http::{Method, Request};
184    use pretty_assertions::assert_eq;
185
186    use super::*;
187    use crate::content_digest::content_digest_header;
188    use crate::key::{RsaBits, SigningKey};
189    use crate::rfc9421::sign::Rfc9421Signer;
190
191    fn signed_request(key: &SigningKey) -> Request<Vec<u8>> {
192        let body = b"{}";
193        let mut req = Request::builder()
194            .method(Method::POST)
195            .uri("https://example.com/inbox?a=1")
196            .header("host", "example.com")
197            .header("date", "Sun, 05 Jan 2014 21:31:40 GMT")
198            .header("content-digest", content_digest_header(body))
199            .body(body.to_vec())
200            .expect("valid");
201        Rfc9421Signer::new(key, "https://example.com/actor#sig")
202            .with_created(1_700_000_000)
203            .sign(&mut req)
204            .expect("sign");
205        req
206    }
207
208    #[test]
209    fn ed25519_roundtrips_sign_then_verify() {
210        let key = SigningKey::generate_ed25519();
211        let public = key.verifying_key();
212        let req = signed_request(&key);
213
214        let report = rfc9421_verify(&req, |kid| {
215            assert_eq!(kid, "https://example.com/actor#sig");
216            Ok(public.clone())
217        })
218        .expect("verify");
219
220        assert_eq!(report.label, "sig1");
221        assert!(report.signature_base.contains(r#""@method": POST"#));
222    }
223
224    #[test]
225    fn rsa_sha256_roundtrips_sign_then_verify() {
226        let key = SigningKey::generate_rsa(RsaBits::Rsa2048).expect("rng");
227        let public = key.verifying_key();
228        let req = signed_request(&key);
229        rfc9421_verify(&req, |_| Ok(public.clone())).expect("verify");
230    }
231
232    #[test]
233    fn tampered_date_header_fails_verification() {
234        let key = SigningKey::generate_ed25519();
235        let public = key.verifying_key();
236        let mut req = signed_request(&key);
237        req.headers_mut().insert(
238            "date",
239            "Mon, 06 Jan 2014 00:00:00 GMT".parse().expect("valid"),
240        );
241        let err =
242            rfc9421_verify(&req, |_| Ok(public.clone())).expect_err("tampered date must fail");
243        assert!(matches!(err, Error::VerificationFailed));
244    }
245
246    #[test]
247    fn algorithm_mismatch_between_hint_and_key_is_rejected() {
248        let key = SigningKey::generate_ed25519();
249        // Resolver returns an RSA public key — alg hint `ed25519` won't match.
250        let rsa_public = SigningKey::generate_rsa(RsaBits::Rsa2048)
251            .expect("rng")
252            .verifying_key();
253        let req = signed_request(&key);
254        let err =
255            rfc9421_verify(&req, |_| Ok(rsa_public.clone())).expect_err("mismatched alg must fail");
256        assert!(matches!(err, Error::VerificationFailed));
257    }
258
259    #[test]
260    fn missing_input_header_is_reported() {
261        let key = SigningKey::generate_ed25519();
262        let mut req = signed_request(&key);
263        req.headers_mut().remove(SIGNATURE_INPUT_HEADER);
264        let err = rfc9421_verify(&req, |_| panic!("resolver must not be called"))
265            .expect_err("missing input");
266        assert!(matches!(err, Error::MissingHeader(SIGNATURE_INPUT_HEADER)));
267    }
268
269    #[test]
270    fn missing_signature_header_is_reported() {
271        let key = SigningKey::generate_ed25519();
272        let mut req = signed_request(&key);
273        req.headers_mut().remove(SIGNATURE_HEADER);
274        let err = rfc9421_verify(&req, |_| panic!("resolver must not be called"))
275            .expect_err("missing signature");
276        assert!(matches!(err, Error::MissingHeader(SIGNATURE_HEADER)));
277    }
278
279    #[test]
280    fn multi_label_signature_input_is_rejected_by_default() {
281        // Mastodon and the RFC 9421 interop profile both expect a
282        // single label; attaching a second one opens a fallback an
283        // attacker can exploit to bypass policy.
284        let key = SigningKey::generate_ed25519();
285        let public = key.verifying_key();
286        let mut req = signed_request(&key);
287        // Append a second, empty inner list to produce `sig1=(...), attacker=()`.
288        let input_raw = req
289            .headers()
290            .get(SIGNATURE_INPUT_HEADER)
291            .unwrap()
292            .to_str()
293            .unwrap()
294            .to_owned()
295            + r", attacker=()";
296        req.headers_mut()
297            .insert(SIGNATURE_INPUT_HEADER, input_raw.parse().unwrap());
298
299        let err = rfc9421_verify(&req, |_| Ok(public.clone()))
300            .expect_err("multiple labels must be rejected");
301        assert!(matches!(err, Error::MalformedSignatureHeader(_)));
302    }
303
304    #[test]
305    fn multi_label_signature_input_is_accepted_when_policy_allows_it() {
306        // Interop escape hatch: some research / middle-box setups do
307        // attach multiple signatures. Flipping the policy knob must
308        // restore the historical tolerant behaviour.
309        use chrono::DateTime;
310
311        let key = SigningKey::generate_ed25519();
312        let public = key.verifying_key();
313        let mut req = signed_request(&key);
314        let input_raw = req
315            .headers()
316            .get(SIGNATURE_INPUT_HEADER)
317            .unwrap()
318            .to_str()
319            .unwrap()
320            .to_owned()
321            + r", attacker=()";
322        req.headers_mut()
323            .insert(SIGNATURE_INPUT_HEADER, input_raw.parse().unwrap());
324
325        let policy = VerifyPolicy {
326            allow_multiple_signatures: true,
327            ..VerifyPolicy::no_freshness_check()
328        };
329        rfc9421_verify_with_policy(
330            &req,
331            &policy,
332            DateTime::<Utc>::from_timestamp(1_700_000_000, 0).unwrap(),
333            |_| Ok(public.clone()),
334        )
335        .expect("the valid sig1 label must still verify");
336    }
337
338    #[test]
339    fn unknown_alg_hint_does_not_short_circuit_multi_label_verification() {
340        // Regression for the `?` short-circuit bug: when an earlier
341        // label carries an unrecognised `alg=` parameter the verifier
342        // must skip it and keep trying later labels, not abort the
343        // entire function.
344        let key = SigningKey::generate_ed25519();
345        let public = key.verifying_key();
346        let mut req = signed_request(&key);
347
348        // Tamper the produced Signature-Input header to claim an
349        // unknown algorithm for the single present label. The
350        // resolver still returns a valid key; prior to the fix the
351        // `?` on `parse_alg_hint` bubbled `UnsupportedAlgorithm` out
352        // of the function.
353        let input_raw = req
354            .headers()
355            .get(SIGNATURE_INPUT_HEADER)
356            .unwrap()
357            .to_str()
358            .unwrap()
359            .replace(r#"alg="ed25519""#, r#"alg="bogus-alg""#);
360        req.headers_mut()
361            .insert(SIGNATURE_INPUT_HEADER, input_raw.parse().unwrap());
362
363        let err = rfc9421_verify(&req, |_| Ok(public.clone()))
364            .expect_err("unknown alg hint must surface as the last recorded error");
365        assert!(matches!(err, Error::UnsupportedAlgorithm(_)));
366    }
367}