1use chrono::{DateTime, Utc};
10use http::Request;
11
12use crate::cavage::{CavageVerified, cavage_verify, cavage_verify_with_policy};
13use crate::error::Error;
14use crate::key::VerifyingKey;
15use crate::policy::VerifyPolicy;
16use crate::rfc9421::{
17 Rfc9421Verified, SIGNATURE_INPUT_HEADER, rfc9421_verify, rfc9421_verify_with_policy,
18};
19
20pub const REDACTED_HEADERS_DEFAULT: &[&str] = &["authorization", "cookie", "proxy-authorization"];
28
29#[derive(Debug, Clone)]
31#[non_exhaustive]
32pub enum Verified {
33 Cavage(CavageVerified),
35 Rfc9421(Rfc9421Verified),
37}
38
39impl Verified {
40 #[must_use]
42 pub fn key_id(&self) -> &str {
43 match self {
44 Self::Cavage(c) => &c.key_id,
45 Self::Rfc9421(r) => r.input.keyid.as_deref().unwrap_or_default(),
46 }
47 }
48
49 #[must_use]
59 pub fn signature_base(&self) -> &str {
60 match self {
61 Self::Cavage(c) => &c.signature_base,
62 Self::Rfc9421(r) => &r.signature_base,
63 }
64 }
65
66 #[must_use]
77 pub fn signature_base_redacted(&self, sensitive_headers: &[&str]) -> String {
78 let base = self.signature_base();
79 let mut out = String::with_capacity(base.len());
80 for line in base.split_inclusive('\n') {
81 out.push_str(&redact_line(line, sensitive_headers));
82 }
83 out
84 }
85}
86
87fn redact_line(line: &str, sensitive: &[&str]) -> String {
88 let trimmed = line.trim_end_matches('\n');
89 let has_newline = line.ends_with('\n');
90 let sensitive_hit = sensitive.iter().any(|h| line_header_matches(trimmed, h));
91 let Some((prefix, _)) = trimmed.split_once(':').filter(|_| sensitive_hit) else {
92 return line.to_owned();
93 };
94 let mut out = String::with_capacity(prefix.len() + 16);
95 out.push_str(prefix);
96 out.push_str(": <redacted>");
97 if has_newline {
98 out.push('\n');
99 }
100 out
101}
102
103fn line_header_matches(line: &str, name: &str) -> bool {
107 let stripped = line
108 .strip_prefix('"')
109 .and_then(|s| s.split_once("\":"))
110 .map(|(n, _)| n);
111 let cavage = line.split_once(':').map(|(n, _)| n);
112 stripped
113 .or(cavage)
114 .is_some_and(|found| found.eq_ignore_ascii_case(name))
115}
116
117pub fn verify<B, F>(req: &Request<B>, mut resolve_key: F) -> Result<Verified, Error>
130where
131 F: FnMut(&str) -> Result<VerifyingKey, Error>,
132{
133 if req.headers().contains_key(SIGNATURE_INPUT_HEADER) {
134 return rfc9421_verify(req, &mut resolve_key).map(Verified::Rfc9421);
135 }
136 cavage_verify(req, |kid| resolve_key(kid)).map(Verified::Cavage)
137}
138
139pub fn verify_with_policy<B, F>(
150 req: &Request<B>,
151 policy: &VerifyPolicy,
152 now: DateTime<Utc>,
153 mut resolve_key: F,
154) -> Result<Verified, Error>
155where
156 F: FnMut(&str) -> Result<VerifyingKey, Error>,
157{
158 if req.headers().contains_key(SIGNATURE_INPUT_HEADER) {
159 return rfc9421_verify_with_policy(req, policy, now, &mut resolve_key)
160 .map(Verified::Rfc9421);
161 }
162 cavage_verify_with_policy(req, policy, now, |kid| resolve_key(kid)).map(Verified::Cavage)
163}
164
165#[cfg(test)]
166mod tests {
167 use http::{Method, Request};
168 use pretty_assertions::assert_eq;
169
170 use super::*;
171 use crate::cavage::CavageSigner;
172 use crate::content_digest::content_digest_header;
173 use crate::digest::sha256_digest_header;
174 use crate::key::SigningKey;
175 use crate::rfc9421::Rfc9421Signer;
176
177 fn base_request(body: &[u8]) -> Request<Vec<u8>> {
178 Request::builder()
183 .method(Method::POST)
184 .uri("https://example.com/inbox")
185 .header("host", "example.com")
186 .header("date", "Sun, 05 Jan 2014 21:31:40 GMT")
187 .header("digest", sha256_digest_header(body))
188 .header("content-digest", content_digest_header(body))
189 .header("content-type", "application/activity+json")
190 .body(body.to_vec())
191 .expect("valid")
192 }
193
194 #[test]
195 fn cavage_signed_request_is_dispatched_to_cavage_verifier() {
196 let key = SigningKey::generate_ed25519();
197 let public = key.verifying_key();
198 let mut req = base_request(b"{}");
199 CavageSigner::new(&key, "https://example.com/actor#kid")
200 .sign(&mut req)
201 .expect("sign");
202
203 let report = verify(&req, |_| Ok(public.clone())).expect("verify");
204 assert!(matches!(report, Verified::Cavage(_)));
205 assert_eq!(report.key_id(), "https://example.com/actor#kid");
206 }
207
208 #[test]
209 fn rfc9421_signed_request_is_dispatched_to_rfc9421_verifier() {
210 let key = SigningKey::generate_ed25519();
211 let public = key.verifying_key();
212 let mut req = base_request(b"{}");
213 Rfc9421Signer::new(&key, "https://example.com/actor#kid")
214 .sign(&mut req)
215 .expect("sign");
216
217 let report = verify(&req, |_| Ok(public.clone())).expect("verify");
218 assert!(matches!(report, Verified::Rfc9421(_)));
219 assert_eq!(report.key_id(), "https://example.com/actor#kid");
220 }
221
222 #[test]
223 fn rfc9421_takes_precedence_over_cavage_when_both_are_present() {
224 let key = SigningKey::generate_ed25519();
227 let public = key.verifying_key();
228 let mut req = base_request(b"{}");
229 CavageSigner::new(&key, "cavage-kid")
230 .sign(&mut req)
231 .expect("sign cavage");
232 Rfc9421Signer::new(&key, "rfc9421-kid")
233 .sign(&mut req)
234 .expect("sign 9421");
235
236 let report = verify(&req, |_| Ok(public.clone())).expect("verify");
237 assert!(matches!(report, Verified::Rfc9421(_)));
238 assert_eq!(report.key_id(), "rfc9421-kid");
239 }
240
241 #[test]
242 fn unsigned_request_returns_missing_header_error() {
243 let req = base_request(b"{}");
244 let err =
245 verify(&req, |_| panic!("resolver must not be called")).expect_err("unsigned request");
246 assert!(matches!(err, Error::MissingHeader(_)));
247 }
248
249 #[test]
250 fn policy_rejects_cavage_signature_older_than_max_age() {
251 let key = SigningKey::generate_ed25519();
252 let public = key.verifying_key();
253 let mut req = base_request(b"{}");
254 CavageSigner::new(&key, "kid")
255 .with_created(1_700_000_000)
256 .sign(&mut req)
257 .expect("sign");
258
259 let now = DateTime::<Utc>::from_timestamp(1_700_000_000 + 20 * 3600, 0).expect("valid");
261 let err = verify_with_policy(&req, &VerifyPolicy::mastodon(), now, |_| Ok(public.clone()))
262 .expect_err("stale signature must be rejected");
263 assert!(matches!(err, Error::TimestampTooOld { .. }));
264 }
265
266 #[test]
267 fn policy_rejects_rfc9421_signature_in_the_future() {
268 let key = SigningKey::generate_ed25519();
269 let public = key.verifying_key();
270 let mut req = base_request(b"{}");
271 Rfc9421Signer::new(&key, "kid")
273 .with_created(1_700_000_000 + 15 * 60)
274 .sign(&mut req)
275 .expect("sign");
276
277 let now = DateTime::<Utc>::from_timestamp(1_700_000_000, 0).expect("valid");
278 let err = verify_with_policy(&req, &VerifyPolicy::mastodon(), now, |_| Ok(public.clone()))
279 .expect_err("future-dated signature must be rejected");
280 assert!(matches!(err, Error::TimestampInFuture { .. }));
281 }
282
283 #[test]
284 fn signature_base_redacted_masks_sensitive_header_values_for_cavage() {
285 let key = SigningKey::generate_ed25519();
286 let public = key.verifying_key();
287 let secret = "Bearer s3cr3t-token";
288 let mut req = base_request(b"{}");
289 req.headers_mut()
290 .insert("authorization", secret.parse().unwrap());
291
292 CavageSigner::new(&key, "kid")
293 .with_headers(["(request-target)", "host", "date", "authorization"])
294 .sign(&mut req)
295 .expect("sign");
296
297 let report = verify(&req, |_| Ok(public.clone())).expect("verify");
298 let redacted = report.signature_base_redacted(REDACTED_HEADERS_DEFAULT);
299 assert!(!redacted.contains(secret), "token must be scrubbed");
300 assert!(
301 redacted.contains("authorization: <redacted>"),
302 "redaction marker must be emitted: {redacted}",
303 );
304 assert!(
305 report.signature_base().contains(secret),
306 "non-redacted accessor must still expose the original value",
307 );
308 }
309
310 #[test]
311 fn signature_base_redacted_masks_sensitive_header_values_for_rfc9421() {
312 use crate::rfc9421::Component;
313
314 let key = SigningKey::generate_ed25519();
315 let public = key.verifying_key();
316 let secret = "SessionID=opaque";
317 let mut req = base_request(b"{}");
318 req.headers_mut().insert("cookie", secret.parse().unwrap());
319
320 Rfc9421Signer::new(&key, "kid")
321 .with_components(vec![
322 Component::Method,
323 Component::TargetUri,
324 Component::Header("cookie".into()),
325 ])
326 .sign(&mut req)
327 .expect("sign");
328
329 let report = verify(&req, |_| Ok(public.clone())).expect("verify");
330 let redacted = report.signature_base_redacted(REDACTED_HEADERS_DEFAULT);
331 assert!(!redacted.contains(secret), "cookie must be scrubbed");
332 assert!(
333 redacted.contains("\"cookie\": <redacted>"),
334 "RFC 9421 quoted-name lines must be recognised: {redacted}",
335 );
336 }
337
338 #[test]
339 fn policy_accepts_signature_within_skew_tolerance() {
340 let key = SigningKey::generate_ed25519();
341 let public = key.verifying_key();
342 let mut req = base_request(b"{}");
343 Rfc9421Signer::new(&key, "kid")
345 .with_created(1_700_000_000 + 60)
346 .sign(&mut req)
347 .expect("sign");
348
349 let now = DateTime::<Utc>::from_timestamp(1_700_000_000, 0).expect("valid");
350 verify_with_policy(&req, &VerifyPolicy::mastodon(), now, |_| Ok(public.clone()))
351 .expect("signature within skew tolerance must verify");
352 }
353}