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    let mut last_err: Option<Error> = None;
106    for (label, input) in inputs {
107        let Some((_, sig_bytes)) = sigs.iter().find(|(l, _)| l == &label) else {
108            last_err = Some(Error::MalformedSignatureHeader(format!(
109                "no Signature entry for label `{label}`"
110            )));
111            continue;
112        };
113
114        // Freshness check on a per-label basis so that one rotated
115        // label does not invalidate a sibling signature.
116        if let Err(e) = policy.check(input.created, input.expires, date_header.as_deref(), now) {
117            last_err = Some(e);
118            continue;
119        }
120
121        let Some(key_id) = input.keyid.as_deref() else {
122            last_err = Some(Error::MissingSignatureParameter("keyid"));
123            continue;
124        };
125
126        let key = match resolve_key(key_id) {
127            Ok(k) => k,
128            Err(e) => {
129                last_err = Some(Error::KeyResolution(e.to_string()));
130                continue;
131            }
132        };
133
134        if let Some(hint) = input.algorithm.as_deref()
135            && let Some(hinted) = parse_alg_hint(hint)?
136            && hinted != key.algorithm()
137        {
138            last_err = Some(Error::VerificationFailed);
139            continue;
140        }
141
142        let inner_list = input.serialise_inner_list();
143        let base = build_signature_base(req, &input.components, &inner_list)?;
144
145        if key.verify(base.as_bytes(), sig_bytes).is_err() {
146            last_err = Some(Error::VerificationFailed);
147            continue;
148        }
149
150        return Ok(Rfc9421Verified {
151            label,
152            input,
153            signature_base: base,
154        });
155    }
156
157    Err(last_err.unwrap_or(Error::VerificationFailed))
158}
159
160fn parse_alg_hint(hint: &str) -> Result<Option<Algorithm>, Error> {
161    match hint {
162        "rsa-v1_5-sha256" | "rsa-sha256" => Ok(Some(Algorithm::RsaSha256)),
163        "ed25519" => Ok(Some(Algorithm::Ed25519)),
164        other => Err(Error::UnsupportedAlgorithm(other.to_owned())),
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use http::{Method, Request};
171    use pretty_assertions::assert_eq;
172
173    use super::*;
174    use crate::digest::sha256_digest_header;
175    use crate::key::{RsaBits, SigningKey};
176    use crate::rfc9421::sign::Rfc9421Signer;
177
178    fn signed_request(key: &SigningKey) -> Request<Vec<u8>> {
179        let body = b"{}";
180        let mut req = Request::builder()
181            .method(Method::POST)
182            .uri("https://example.com/inbox?a=1")
183            .header("host", "example.com")
184            .header("date", "Sun, 05 Jan 2014 21:31:40 GMT")
185            .header("digest", sha256_digest_header(body))
186            .body(body.to_vec())
187            .expect("valid");
188        Rfc9421Signer::new(key, "https://example.com/actor#sig")
189            .with_created(1_700_000_000)
190            .sign(&mut req)
191            .expect("sign");
192        req
193    }
194
195    #[test]
196    fn ed25519_roundtrips_sign_then_verify() {
197        let key = SigningKey::generate_ed25519();
198        let public = key.verifying_key();
199        let req = signed_request(&key);
200
201        let report = rfc9421_verify(&req, |kid| {
202            assert_eq!(kid, "https://example.com/actor#sig");
203            Ok(public.clone())
204        })
205        .expect("verify");
206
207        assert_eq!(report.label, "sig1");
208        assert!(report.signature_base.contains(r#""@method": POST"#));
209    }
210
211    #[test]
212    fn rsa_sha256_roundtrips_sign_then_verify() {
213        let key = SigningKey::generate_rsa(RsaBits::Rsa2048).expect("rng");
214        let public = key.verifying_key();
215        let req = signed_request(&key);
216        rfc9421_verify(&req, |_| Ok(public.clone())).expect("verify");
217    }
218
219    #[test]
220    fn tampered_date_header_fails_verification() {
221        let key = SigningKey::generate_ed25519();
222        let public = key.verifying_key();
223        let mut req = signed_request(&key);
224        req.headers_mut().insert(
225            "date",
226            "Mon, 06 Jan 2014 00:00:00 GMT".parse().expect("valid"),
227        );
228        let err =
229            rfc9421_verify(&req, |_| Ok(public.clone())).expect_err("tampered date must fail");
230        assert!(matches!(err, Error::VerificationFailed));
231    }
232
233    #[test]
234    fn algorithm_mismatch_between_hint_and_key_is_rejected() {
235        let key = SigningKey::generate_ed25519();
236        // Resolver returns an RSA public key — alg hint `ed25519` won't match.
237        let rsa_public = SigningKey::generate_rsa(RsaBits::Rsa2048)
238            .expect("rng")
239            .verifying_key();
240        let req = signed_request(&key);
241        let err =
242            rfc9421_verify(&req, |_| Ok(rsa_public.clone())).expect_err("mismatched alg must fail");
243        assert!(matches!(err, Error::VerificationFailed));
244    }
245
246    #[test]
247    fn missing_input_header_is_reported() {
248        let key = SigningKey::generate_ed25519();
249        let mut req = signed_request(&key);
250        req.headers_mut().remove(SIGNATURE_INPUT_HEADER);
251        let err = rfc9421_verify(&req, |_| panic!("resolver must not be called"))
252            .expect_err("missing input");
253        assert!(matches!(err, Error::MissingHeader(SIGNATURE_INPUT_HEADER)));
254    }
255
256    #[test]
257    fn missing_signature_header_is_reported() {
258        let key = SigningKey::generate_ed25519();
259        let mut req = signed_request(&key);
260        req.headers_mut().remove(SIGNATURE_HEADER);
261        let err = rfc9421_verify(&req, |_| panic!("resolver must not be called"))
262            .expect_err("missing signature");
263        assert!(matches!(err, Error::MissingHeader(SIGNATURE_HEADER)));
264    }
265}