1use chrono::{DateTime, Utc};
4use http::Request;
5
6use crate::error::Error;
7use crate::key::{Algorithm, VerifyingKey};
8use crate::policy::VerifyPolicy;
9use crate::rfc9421::components::{Component, 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#[derive(Debug, Clone)]
17#[non_exhaustive]
18pub struct Rfc9421Verified {
19 pub label: String,
21 pub input: SignatureInput,
23 pub signature_base: String,
25}
26
27pub 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
52pub 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 if let Err(e) =
128 enforce_required_components(&input.components, policy.rfc9421_required_components)
129 {
130 last_err = Some(e);
131 continue;
132 }
133
134 if let Err(e) = policy.check(input.created, input.expires, date_header.as_deref(), now) {
137 last_err = Some(e);
138 continue;
139 }
140
141 let Some(key_id) = input.keyid.as_deref() else {
142 last_err = Some(Error::MissingSignatureParameter("keyid"));
143 continue;
144 };
145
146 let key = match resolve_key(key_id) {
147 Ok(k) => k,
148 Err(e) => {
149 last_err = Some(Error::KeyResolution(e.to_string()));
150 continue;
151 }
152 };
153
154 if let Some(hint) = input.algorithm.as_deref() {
155 match parse_alg_hint(hint) {
156 Ok(Some(hinted)) if hinted != key.algorithm() => {
157 last_err = Some(Error::VerificationFailed);
158 continue;
159 }
160 Ok(_) => {}
161 Err(e) => {
162 last_err = Some(e);
163 continue;
164 }
165 }
166 }
167
168 let inner_list = input.serialise_inner_list();
169 let base = build_signature_base(req, &input.components, &inner_list)?;
170
171 if key.verify(base.as_bytes(), sig_bytes).is_err() {
172 last_err = Some(Error::VerificationFailed);
173 continue;
174 }
175
176 return Ok(Rfc9421Verified {
177 label,
178 input,
179 signature_base: base,
180 });
181 }
182
183 Err(last_err.unwrap_or(Error::VerificationFailed))
184}
185
186fn parse_alg_hint(hint: &str) -> Result<Option<Algorithm>, Error> {
197 Algorithm::parse(hint)
198}
199
200fn enforce_required_components(signed: &[Component], required: &[&str]) -> Result<(), Error> {
205 for needed in required {
206 let present = signed
207 .iter()
208 .any(|c| c.identifier().eq_ignore_ascii_case(needed));
209 if !present {
210 return Err(Error::RequiredHeaderAbsent((*needed).to_owned()));
211 }
212 }
213 Ok(())
214}
215
216#[cfg(test)]
217mod tests {
218 use http::{Method, Request};
219 use pretty_assertions::assert_eq;
220
221 use super::*;
222 use crate::content_digest::content_digest_header;
223 use crate::key::{RsaBits, SigningKey};
224 use crate::rfc9421::sign::Rfc9421Signer;
225
226 fn signed_request(key: &SigningKey) -> Request<Vec<u8>> {
227 let body = b"{}";
228 let mut req = Request::builder()
229 .method(Method::POST)
230 .uri("https://example.com/inbox?a=1")
231 .header("host", "example.com")
232 .header("date", "Sun, 05 Jan 2014 21:31:40 GMT")
233 .header("content-digest", content_digest_header(body))
234 .body(body.to_vec())
235 .expect("valid");
236 Rfc9421Signer::new(key, "https://example.com/actor#sig")
237 .with_created(1_700_000_000)
238 .sign(&mut req)
239 .expect("sign");
240 req
241 }
242
243 #[test]
244 fn ed25519_roundtrips_sign_then_verify() {
245 let key = SigningKey::generate_ed25519();
246 let public = key.verifying_key();
247 let req = signed_request(&key);
248
249 let report = rfc9421_verify(&req, |kid| {
250 assert_eq!(kid, "https://example.com/actor#sig");
251 Ok(public.clone())
252 })
253 .expect("verify");
254
255 assert_eq!(report.label, "sig1");
256 assert!(report.signature_base.contains(r#""@method": POST"#));
257 }
258
259 #[test]
260 fn rsa_sha256_roundtrips_sign_then_verify() {
261 let key = SigningKey::generate_rsa(RsaBits::Rsa2048).expect("rng");
262 let public = key.verifying_key();
263 let req = signed_request(&key);
264 rfc9421_verify(&req, |_| Ok(public.clone())).expect("verify");
265 }
266
267 #[test]
268 fn tampered_date_header_fails_verification() {
269 let key = SigningKey::generate_ed25519();
270 let public = key.verifying_key();
271 let mut req = signed_request(&key);
272 req.headers_mut().insert(
273 "date",
274 "Mon, 06 Jan 2014 00:00:00 GMT".parse().expect("valid"),
275 );
276 let err =
277 rfc9421_verify(&req, |_| Ok(public.clone())).expect_err("tampered date must fail");
278 assert!(matches!(err, Error::VerificationFailed));
279 }
280
281 #[test]
282 fn parse_alg_hint_accepts_legacy_hs2019_as_key_derived() {
283 assert_eq!(
293 parse_alg_hint("hs2019").expect("hs2019 must be accepted"),
294 None
295 );
296 assert_eq!(
298 parse_alg_hint("rsa-v1_5-sha256").expect("parse"),
299 Some(Algorithm::RsaSha256),
300 );
301 assert_eq!(
302 parse_alg_hint("ed25519").expect("parse"),
303 Some(Algorithm::Ed25519),
304 );
305 }
306
307 #[test]
308 fn algorithm_mismatch_between_hint_and_key_is_rejected() {
309 let key = SigningKey::generate_ed25519();
310 let rsa_public = SigningKey::generate_rsa(RsaBits::Rsa2048)
312 .expect("rng")
313 .verifying_key();
314 let req = signed_request(&key);
315 let err =
316 rfc9421_verify(&req, |_| Ok(rsa_public.clone())).expect_err("mismatched alg must fail");
317 assert!(matches!(err, Error::VerificationFailed));
318 }
319
320 #[test]
321 fn missing_input_header_is_reported() {
322 let key = SigningKey::generate_ed25519();
323 let mut req = signed_request(&key);
324 req.headers_mut().remove(SIGNATURE_INPUT_HEADER);
325 let err = rfc9421_verify(&req, |_| panic!("resolver must not be called"))
326 .expect_err("missing input");
327 assert!(matches!(err, Error::MissingHeader(SIGNATURE_INPUT_HEADER)));
328 }
329
330 #[test]
331 fn missing_signature_header_is_reported() {
332 let key = SigningKey::generate_ed25519();
333 let mut req = signed_request(&key);
334 req.headers_mut().remove(SIGNATURE_HEADER);
335 let err = rfc9421_verify(&req, |_| panic!("resolver must not be called"))
336 .expect_err("missing signature");
337 assert!(matches!(err, Error::MissingHeader(SIGNATURE_HEADER)));
338 }
339
340 #[test]
341 fn multi_label_signature_input_is_rejected_by_default() {
342 let key = SigningKey::generate_ed25519();
346 let public = key.verifying_key();
347 let mut req = signed_request(&key);
348 let input_raw = req
350 .headers()
351 .get(SIGNATURE_INPUT_HEADER)
352 .unwrap()
353 .to_str()
354 .unwrap()
355 .to_owned()
356 + r", attacker=()";
357 req.headers_mut()
358 .insert(SIGNATURE_INPUT_HEADER, input_raw.parse().unwrap());
359
360 let err = rfc9421_verify(&req, |_| Ok(public.clone()))
361 .expect_err("multiple labels must be rejected");
362 assert!(matches!(err, Error::MalformedSignatureHeader(_)));
363 }
364
365 #[test]
366 fn multi_label_signature_input_is_accepted_when_policy_allows_it() {
367 use chrono::DateTime;
371
372 let key = SigningKey::generate_ed25519();
373 let public = key.verifying_key();
374 let mut req = signed_request(&key);
375 let input_raw = req
376 .headers()
377 .get(SIGNATURE_INPUT_HEADER)
378 .unwrap()
379 .to_str()
380 .unwrap()
381 .to_owned()
382 + r", attacker=()";
383 req.headers_mut()
384 .insert(SIGNATURE_INPUT_HEADER, input_raw.parse().unwrap());
385
386 let policy = VerifyPolicy {
387 allow_multiple_signatures: true,
388 ..VerifyPolicy::no_freshness_check()
389 };
390 rfc9421_verify_with_policy(
391 &req,
392 &policy,
393 DateTime::<Utc>::from_timestamp(1_700_000_000, 0).unwrap(),
394 |_| Ok(public.clone()),
395 )
396 .expect("the valid sig1 label must still verify");
397 }
398
399 #[test]
400 fn mastodon_policy_rejects_signature_without_target_uri_component() {
401 use chrono::DateTime;
406
407 use crate::rfc9421::Component;
408
409 let key = SigningKey::generate_ed25519();
410 let public = key.verifying_key();
411 let body = b"{}";
412 let mut req = Request::builder()
413 .method(Method::POST)
414 .uri("https://example.com/inbox?a=1")
415 .header("host", "example.com")
416 .header("date", "Sun, 05 Jan 2014 21:31:40 GMT")
417 .header("content-digest", content_digest_header(body))
418 .body(body.to_vec())
419 .expect("valid");
420 Rfc9421Signer::new(&key, "kid")
421 .with_components(vec![
422 Component::Method,
423 Component::Header("content-digest".into()),
424 ])
425 .with_created(1_700_000_000)
426 .sign(&mut req)
427 .expect("sign");
428
429 let err = rfc9421_verify_with_policy(
430 &req,
431 &VerifyPolicy::mastodon(),
432 DateTime::<Utc>::from_timestamp(1_700_000_000, 0).unwrap(),
433 |_| Ok(public.clone()),
434 )
435 .expect_err("missing `@target-uri` must be rejected by the Mastodon policy");
436 assert!(
437 matches!(&err, Error::RequiredHeaderAbsent(name) if name == "@target-uri"),
438 "unexpected error variant: {err:?}",
439 );
440 }
441
442 #[test]
443 fn mastodon_policy_rejects_signature_without_content_digest_component() {
444 use chrono::DateTime;
448
449 use crate::rfc9421::Component;
450
451 let key = SigningKey::generate_ed25519();
452 let public = key.verifying_key();
453 let body = b"{}";
454 let mut req = Request::builder()
455 .method(Method::POST)
456 .uri("https://example.com/inbox?a=1")
457 .header("host", "example.com")
458 .header("date", "Sun, 05 Jan 2014 21:31:40 GMT")
459 .header("content-digest", content_digest_header(body))
460 .body(body.to_vec())
461 .expect("valid");
462 Rfc9421Signer::new(&key, "kid")
463 .with_components(vec![Component::Method, Component::TargetUri])
464 .with_created(1_700_000_000)
465 .sign(&mut req)
466 .expect("sign");
467
468 let err = rfc9421_verify_with_policy(
469 &req,
470 &VerifyPolicy::mastodon(),
471 DateTime::<Utc>::from_timestamp(1_700_000_000, 0).unwrap(),
472 |_| Ok(public.clone()),
473 )
474 .expect_err("missing `content-digest` must be rejected");
475 assert!(
476 matches!(&err, Error::RequiredHeaderAbsent(name) if name == "content-digest"),
477 "unexpected: {err:?}",
478 );
479 }
480
481 #[test]
482 fn no_freshness_check_policy_tolerates_minimal_covered_components() {
483 use crate::rfc9421::Component;
488
489 let key = SigningKey::generate_ed25519();
490 let public = key.verifying_key();
491 let body = b"{}";
492 let mut req = Request::builder()
493 .method(Method::POST)
494 .uri("https://example.com/inbox")
495 .header("host", "example.com")
496 .header("date", "Sun, 05 Jan 2014 21:31:40 GMT")
497 .body(body.to_vec())
498 .expect("valid");
499 Rfc9421Signer::new(&key, "kid")
500 .with_components(vec![Component::Method])
501 .sign(&mut req)
502 .expect("sign");
503
504 rfc9421_verify(&req, |_| Ok(public.clone()))
505 .expect("no_freshness_check preset must not enforce required components");
506 }
507
508 #[test]
509 fn unknown_alg_hint_does_not_short_circuit_multi_label_verification() {
510 let key = SigningKey::generate_ed25519();
515 let public = key.verifying_key();
516 let mut req = signed_request(&key);
517
518 let input_raw = req
524 .headers()
525 .get(SIGNATURE_INPUT_HEADER)
526 .unwrap()
527 .to_str()
528 .unwrap()
529 .replace(r#"alg="ed25519""#, r#"alg="bogus-alg""#);
530 req.headers_mut()
531 .insert(SIGNATURE_INPUT_HEADER, input_raw.parse().unwrap());
532
533 let err = rfc9421_verify(&req, |_| Ok(public.clone()))
534 .expect_err("unknown alg hint must surface as the last recorded error");
535 assert!(matches!(err, Error::UnsupportedAlgorithm(_)));
536 }
537}