1use crate::digest::{content_digest_matches, digest_matches};
25use crate::integrity::{self, PublicKey};
26use crate::{rfc9421, signature};
27use core::fmt;
28use serde_json::Value;
29
30#[derive(Clone, Copy, Debug)]
32pub struct Request<'a> {
33 pub method: &'a str,
35 pub path_and_query: &'a str,
37 pub headers: &'a [(&'a str, &'a str)],
39 pub body: &'a [u8],
40}
41
42impl Request<'_> {
43 fn header(&self, name: &str) -> Option<&str> {
44 self.headers
45 .iter()
46 .find(|(key, _)| key.eq_ignore_ascii_case(name))
47 .map(|(_, value)| *value)
48 }
49
50 fn has_body(&self) -> bool {
51 !self.method.eq_ignore_ascii_case("get") && !self.method.eq_ignore_ascii_case("head")
52 }
53}
54
55#[derive(Clone, Copy, Debug)]
57pub struct Policy<'a> {
58 pub hosts: &'a [&'a str],
63 pub max_skew_seconds: i64,
65}
66
67impl<'a> Policy<'a> {
68 #[must_use]
70 pub fn new(hosts: &'a [&'a str]) -> Self {
71 Self {
72 hosts,
73 max_skew_seconds: 3600,
74 }
75 }
76}
77
78#[derive(Clone, Copy, Debug, Eq, PartialEq)]
80pub enum Scheme {
81 DraftCavage,
82 Rfc9421,
83}
84
85#[derive(Clone, Debug, Eq, PartialEq)]
87pub struct Signature {
88 pub scheme: Scheme,
89 pub key_id: String,
91 pub covered: Vec<String>,
94 pub created: Option<i64>,
97 pub expires: Option<i64>,
100 signature: String,
101 signature_input: Option<String>,
102}
103
104#[derive(Clone, Debug, Eq, PartialEq)]
106pub enum Rejection {
107 Unsigned,
109 Malformed(String),
111 NotCovered(String),
113 MissingHeader(&'static str),
115 DigestMismatch,
117 Stale { seconds: i64 },
119 Expired { seconds: i64 },
121 WrongHost(String),
123 Invalid(String),
125}
126
127impl fmt::Display for Rejection {
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 match self {
130 Self::Unsigned => f.write_str("no Signature header"),
131 Self::Malformed(why) => write!(f, "malformed signature: {why}"),
132 Self::NotCovered(what) => write!(f, "signature does not cover {what}"),
133 Self::MissingHeader(name) => write!(f, "missing {name} header"),
134 Self::DigestMismatch => f.write_str("the body does not match its digest"),
135 Self::Stale { seconds } => write!(f, "signature made {seconds}s from now"),
136 Self::Expired { seconds } => write!(f, "signature expired {seconds}s ago"),
137 Self::WrongHost(host) => write!(f, "signed for another host: {host}"),
138 Self::Invalid(why) => write!(f, "signature does not verify: {why}"),
139 }
140 }
141}
142
143impl core::error::Error for Rejection {}
144
145#[derive(Clone, Copy, Debug)]
147pub enum Key<'a> {
148 RsaPem(&'a str),
150 Ed25519(&'a [u8; 32]),
152}
153
154pub fn parse(request: &Request<'_>) -> Result<Signature, Rejection> {
160 let signature = request
161 .header("signature")
162 .ok_or(Rejection::Unsigned)?
163 .to_owned();
164 if let Some(input) = request.header("signature-input") {
167 let key_id = rfc9421::key_id(input)
168 .ok_or_else(|| Rejection::Malformed("no keyid in Signature-Input".into()))?;
169 let covered = rfc9421::covered_components(input)
170 .ok_or_else(|| Rejection::Malformed("unreadable Signature-Input".into()))?;
171 return Ok(Signature {
172 scheme: Scheme::Rfc9421,
173 key_id,
174 covered,
175 created: rfc9421::created_at(input),
176 expires: rfc9421::expires_at(input),
177 signature,
178 signature_input: Some(input.to_owned()),
179 });
180 }
181
182 let params = signature::parse_params(&signature);
183 let key_id = params
184 .get("keyId")
185 .cloned()
186 .ok_or_else(|| Rejection::Malformed("no keyId".into()))?;
187 let covered: Vec<String> = params
188 .get("headers")
189 .map_or("date", String::as_str)
190 .split_whitespace()
191 .map(str::to_ascii_lowercase)
192 .collect();
193 let created = if covered.iter().any(|name| name == "(created)") {
197 Some(
198 params
199 .get("created")
200 .ok_or_else(|| Rejection::Malformed("(created) covered but not given".into()))?
201 .parse()
202 .map_err(|_| Rejection::Malformed("unreadable created".into()))?,
203 )
204 } else {
205 request.header("date").and_then(http_date)
206 };
207 let expires = if covered.iter().any(|name| name == "(expires)") {
208 Some(
209 params
210 .get("expires")
211 .ok_or_else(|| Rejection::Malformed("(expires) covered but not given".into()))?
212 .parse()
213 .map_err(|_| Rejection::Malformed("unreadable expires".into()))?,
214 )
215 } else {
216 None
217 };
218 Ok(Signature {
219 scheme: Scheme::DraftCavage,
220 key_id,
221 covered,
222 created,
223 expires,
224 signature,
225 signature_input: None,
226 })
227}
228
229pub fn check(
236 signature: &Signature,
237 request: &Request<'_>,
238 policy: &Policy<'_>,
239 now: i64,
240) -> Result<(), Rejection> {
241 let covers = |name: &str| signature.covered.iter().any(|covered| covered == name);
242 let require = |name: &str| {
243 if covers(name) {
244 Ok(())
245 } else {
246 Err(Rejection::NotCovered(name.to_owned()))
247 }
248 };
249 match signature.scheme {
250 Scheme::DraftCavage => {
251 require("(request-target)")?;
252 require("host")?;
253 if !covers("date") && !covers("(created)") {
254 return Err(Rejection::NotCovered("date".into()));
255 }
256 if request.has_body() {
257 require("digest")?;
258 let digest = request
259 .header("digest")
260 .ok_or(Rejection::MissingHeader("Digest"))?;
261 if !digest_matches(digest, request.body) {
262 return Err(Rejection::DigestMismatch);
263 }
264 }
265 }
266 Scheme::Rfc9421 => {
267 require("@method")?;
268 require("@target-uri")?;
269 if request.has_body() {
270 require("content-digest")?;
271 let digest = request
272 .header("content-digest")
273 .ok_or(Rejection::MissingHeader("Content-Digest"))?;
274 if !content_digest_matches(digest, request.body) {
275 return Err(Rejection::DigestMismatch);
276 }
277 }
278 }
279 }
280
281 let created = signature.created.ok_or(Rejection::MissingHeader("Date"))?;
282 let seconds = created - now;
283 if seconds.abs() > policy.max_skew_seconds {
284 return Err(Rejection::Stale { seconds });
285 }
286 if let Some(expires) = signature.expires
289 && expires < now
290 {
291 return Err(Rejection::Expired {
292 seconds: now - expires,
293 });
294 }
295
296 let host = request
297 .header("host")
298 .ok_or(Rejection::MissingHeader("Host"))?;
299 if !policy
300 .hosts
301 .iter()
302 .any(|accepted| host_matches(accepted, host))
303 {
304 return Err(Rejection::WrongHost(host.to_owned()));
305 }
306 Ok(())
307}
308
309fn host_matches(accepted: &str, host: &str) -> bool {
312 let normalise = |value: &str| value.trim().trim_end_matches('.').to_ascii_lowercase();
313 let (accepted, host) = (normalise(accepted), normalise(host));
314 if accepted == host {
315 return true;
316 }
317 if split_port(&accepted).is_some() {
318 return false;
319 }
320 split_port(&host).is_some_and(|name| name.trim_end_matches('.') == accepted)
321}
322
323fn split_port(value: &str) -> Option<&str> {
325 let (name, port) = value.rsplit_once(':')?;
326 if name.contains(':') && !name.ends_with(']') {
328 return None;
329 }
330 (!port.is_empty() && port.bytes().all(|b| b.is_ascii_digit())).then_some(name)
331}
332
333pub fn verify(signature: &Signature, request: &Request<'_>, key: Key<'_>) -> Result<(), Rejection> {
339 match signature.scheme {
340 Scheme::DraftCavage => {
341 let Key::RsaPem(pem) = key else {
342 return Err(Rejection::Invalid(
343 "a draft-cavage signature is checked with an RSA key".into(),
344 ));
345 };
346 signature::verify_request(
347 &request.method.to_ascii_lowercase(),
348 request.path_and_query,
349 request.headers,
350 request.body,
351 pem,
352 )
353 .map_err(|error| Rejection::Invalid(error.to_string()))
354 }
355 Scheme::Rfc9421 => {
356 let host = request
357 .header("host")
358 .ok_or(Rejection::MissingHeader("Host"))?;
359 let target = format!("https://{host}{}", request.path_and_query);
362 let key = match key {
363 Key::RsaPem(pem) => rfc9421::VerifyingKey::RsaPem(pem),
364 Key::Ed25519(bytes) => rfc9421::VerifyingKey::Ed25519(bytes),
365 };
366 rfc9421::verify_request(
367 request.method,
368 &target,
369 signature.signature_input.as_deref().unwrap_or_default(),
370 &signature.signature,
371 request.header("content-digest"),
372 request.body,
373 request.headers,
374 &key,
375 )
376 .map_err(|error| Rejection::Invalid(error.to_string()))
377 }
378 }
379}
380
381#[must_use]
385pub fn key_owner(key_id: &str) -> &str {
386 if let Some((owner, _)) = key_id.split_once('#') {
387 return owner;
388 }
389 key_id.strip_suffix("/main-key").unwrap_or(key_id)
390}
391
392#[must_use]
402pub fn published_key_pem(actor: &Value, key_id: &str) -> Option<String> {
403 let actor_id = actor.get("id").and_then(Value::as_str)?;
404 let keys: Vec<&Value> = match actor.get("publicKey")? {
405 Value::Array(keys) => keys.iter().collect(),
406 key => vec![key],
407 };
408 keys.into_iter().find_map(|key| {
409 let id = key.get("id").and_then(Value::as_str)?;
410 let owner = key.get("owner").and_then(Value::as_str);
411 let named =
412 id == key_id || (key_id == actor_id && id.strip_prefix(actor_id) == Some("#main-key"));
413 (named && owner.is_none_or(|owner| owner == actor_id))
414 .then(|| key.get("publicKeyPem").and_then(Value::as_str))
415 .flatten()
416 .map(str::to_owned)
417 })
418}
419
420#[derive(Clone, Debug, Eq, PartialEq)]
422pub enum PublishedKey {
423 RsaPem(String),
425 Ed25519([u8; 32]),
427}
428
429impl PublishedKey {
430 #[must_use]
432 pub fn as_key(&self) -> Key<'_> {
433 match self {
434 Self::RsaPem(pem) => Key::RsaPem(pem),
435 Self::Ed25519(bytes) => Key::Ed25519(bytes),
436 }
437 }
438}
439
440#[must_use]
444pub fn published_key(actor: &Value, key_id: &str) -> Option<PublishedKey> {
445 if let Some(pem) = published_key_pem(actor, key_id) {
446 return Some(PublishedKey::RsaPem(pem));
447 }
448 let actor_id = actor.get("id").and_then(Value::as_str)?;
449 let methods: Vec<&Value> = match actor.get("assertionMethod")? {
450 Value::Array(methods) => methods.iter().collect(),
451 method => vec![method],
452 };
453 methods.into_iter().find_map(|method| {
454 if method.get("id").and_then(Value::as_str) != Some(key_id)
455 || method.get("controller").and_then(Value::as_str) != Some(actor_id)
456 {
457 return None;
458 }
459 let multibase = method.get("publicKeyMultibase")?.as_str()?;
460 match integrity::decode_multikey(multibase).ok()? {
461 PublicKey::Ed25519(bytes) => Some(PublishedKey::Ed25519(*bytes)),
462 PublicKey::MlDsa44(_) => None,
463 }
464 })
465}
466
467fn http_date(value: &str) -> Option<i64> {
469 let value = value.trim();
470 chrono::NaiveDateTime::parse_from_str(value, "%a, %d %b %Y %H:%M:%S GMT")
471 .map(|date| date.and_utc().timestamp())
472 .or_else(|_| chrono::DateTime::parse_from_rfc2822(value).map(|date| date.timestamp()))
473 .ok()
474}
475
476#[cfg(test)]
477mod tests {
478 use super::host_matches;
479
480 #[test]
481 fn a_host_is_matched_with_or_without_its_port() {
482 assert!(host_matches("eunha.example", "eunha.example"));
483 assert!(host_matches("eunha.example", "EUNHA.example."));
484 assert!(host_matches("eunha.example", "eunha.example:8443"));
485 assert!(host_matches("localhost:3000", "localhost:3000"));
486 assert!(host_matches("[::1]", "[::1]:3000"));
487 assert!(!host_matches("localhost:3000", "localhost:4000"));
488 assert!(!host_matches("localhost:3000", "localhost"));
489 assert!(!host_matches("eunha.example", "other.example"));
490 assert!(!host_matches("eunha.example", "eunha.example.evil.example"));
491 assert!(!host_matches("eunha.example", "eunha.example:"));
492 assert!(!host_matches("eunha.example", "eunha.example:https"));
493 }
494}