|
1 | | -use crate::components::CoveredComponent; |
| 1 | +use super::ImplementationError; |
| 2 | +use crate::components::{self, CoveredComponent, HTTPField}; |
2 | 3 | use crate::keyring::{Algorithm, KeyRing}; |
3 | 4 | use indexmap::IndexMap; |
| 5 | +use regex::bytes::Regex; |
4 | 6 | use sfv::SerializeValue; |
5 | 7 | use std::fmt::Write as _; |
| 8 | +use std::sync::LazyLock; |
6 | 9 | use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; |
7 | | - |
8 | | -use super::ImplementationError; |
| 10 | +static OBSOLETE_LINE_FOLDING: LazyLock<Regex> = |
| 11 | + LazyLock::new(|| Regex::new(r"\s*\r\n\s+").unwrap()); |
9 | 12 |
|
10 | 13 | /// The component parameters associated with the signature in `Signature-Input` |
11 | 14 | #[derive(Clone, Debug)] |
@@ -153,8 +156,26 @@ impl SignatureBaseBuilder { |
153 | 156 | self.components |
154 | 157 | .into_iter() |
155 | 158 | .map(|component| match message.lookup_component(&component) { |
156 | | - Some(serialized_value) => Ok((component, serialized_value)), |
157 | | - None => Err(ImplementationError::LookupError(component)), |
| 159 | + v if v.len() == 1 => Ok((component, v[0].to_owned())), |
| 160 | + v if v.len() > 1 && matches!(component, CoveredComponent::HTTP(_)) => { |
| 161 | + let mut register: Vec<String> = vec![]; |
| 162 | + |
| 163 | + for header_value in v.into_iter() { |
| 164 | + register.push( |
| 165 | + // replace leading / trailing whitespace and obsolete line folding, |
| 166 | + // per HTTP message signature spec |
| 167 | + String::from_utf8( |
| 168 | + OBSOLETE_LINE_FOLDING |
| 169 | + .replace_all(header_value.as_bytes().trim_ascii(), b" ") |
| 170 | + .into_owned(), |
| 171 | + ) |
| 172 | + .map_err(|_| ImplementationError::NonAsciiContentFound)?, |
| 173 | + ); |
| 174 | + } |
| 175 | + |
| 176 | + Ok((component, register.join(", "))) |
| 177 | + } |
| 178 | + _ => Err(ImplementationError::LookupError(component)), |
158 | 179 | }) |
159 | 180 | .collect::<Result<Vec<(CoveredComponent, String)>, ImplementationError>>()?, |
160 | 181 | ), |
@@ -215,31 +236,14 @@ impl SignatureBase { |
215 | 236 | /// Trait that messages seeking verification should implement to facilitate looking up |
216 | 237 | /// raw values from the underlying message. |
217 | 238 | pub trait SignedMessage { |
218 | | - /// Obtain every `Signature` header in the message. Despite the name, you can omit |
219 | | - /// `Signature` that are known to be invalid ahead of time. However, each `Signature-` |
220 | | - /// header should be unparsed and be a valid sfv::Item::Dictionary value. You should |
221 | | - /// separately implement looking this up in `lookup_component` as an HTTP header with |
222 | | - /// multiple values, although including these as signature components when signing is |
223 | | - /// NOT recommended. During verification, invalid values (those that cannot be |
224 | | - /// parsed as an sfv::Dictionary) will be skipped without raising an error. |
225 | | - fn fetch_all_signature_headers(&self) -> Vec<String>; |
226 | | - /// Obtain every `Signature-Input` header in the message. Despite the name, you |
227 | | - /// can omit `Signature-Input` that are known to be invalid ahead of time. However, |
228 | | - /// each `Signature-Input` header should be unparsed and be a valid sfv::Item::Dictionary |
229 | | - /// value (meaning it should be encased in double quotes). You should separately implement |
230 | | - /// looking this up in `lookup_component` as an HTTP header with multiple values, although |
231 | | - /// including these as signature components when signing is NOT recommended. During |
232 | | - /// verification, invalid values (those that cannot be parsed as an sfv::Dictionary) will |
233 | | - /// be skipped will be skipped without raising an error. |
234 | | - fn fetch_all_signature_inputs(&self) -> Vec<String>; |
235 | | - /// Obtain the serialized value of a covered component. Implementations should |
| 239 | + /// Retrieve the raw value(s) of a covered component. Implementations should |
236 | 240 | /// respect any parameter values set on the covered component per the message |
237 | | - /// signature spec. Component values that cannot be found must return None. |
| 241 | + /// signature spec. Component values that cannot be found must return an empty vector. |
238 | 242 | /// `CoveredComponent::HTTP` fields are guaranteed to have lowercase ASCII names, so |
239 | 243 | /// care should be taken to ensure HTTP field names in the message are checked in a |
240 | | - /// case-insensitive way. HTTP fields with multiple values should be combined into a |
241 | | - /// single string in the manner described in <https://www.rfc-editor.org/rfc/rfc9421#name-http-fields>. |
242 | | - fn lookup_component(&self, name: &CoveredComponent) -> Option<String>; |
| 244 | + /// case-insensitive way. Only `CoveredComponent::Http` should return a vector with |
| 245 | + /// more than one element. |
| 246 | + fn lookup_component(&self, name: &CoveredComponent) -> Vec<String>; |
243 | 247 | } |
244 | 248 |
|
245 | 249 | /// Trait that messages seeking signing should implement to generate `Signature-Input` |
@@ -386,6 +390,8 @@ impl MessageSigner { |
386 | 390 | /// of the chosen labl and its components. |
387 | 391 | #[derive(Clone, Debug)] |
388 | 392 | pub struct ParsedLabel { |
| 393 | + /// The label that was chosen. |
| 394 | + pub label: sfv::Key, |
389 | 395 | /// The signature obtained from the message that verifiers will verify |
390 | 396 | pub signature: Vec<u8>, |
391 | 397 | /// The signature base obtained from the message, containining both the chosen |
@@ -424,27 +430,33 @@ impl MessageVerifier { |
424 | 430 | P: Fn(&(sfv::Key, sfv::InnerList)) -> bool, |
425 | 431 | { |
426 | 432 | let signature_input = message |
427 | | - .fetch_all_signature_inputs() |
| 433 | + .lookup_component(&CoveredComponent::HTTP(HTTPField { |
| 434 | + name: "signature-input".to_string(), |
| 435 | + parameters: components::HTTPFieldParametersSet(vec![]), |
| 436 | + })) |
428 | 437 | .into_iter() |
429 | 438 | .filter_map(|sig_input| sfv::Parser::new(&sig_input).parse_dictionary().ok()) |
430 | 439 | .reduce(|mut acc, sig_input| { |
431 | 440 | acc.extend(sig_input); |
432 | 441 | acc |
433 | 442 | }) |
434 | 443 | .ok_or(ImplementationError::ParsingError( |
435 | | - "No `Signature-Input` headers found".to_string(), |
| 444 | + "No validly-formatted `Signature-Input` headers found".to_string(), |
436 | 445 | ))?; |
437 | 446 |
|
438 | 447 | let mut signature_header = message |
439 | | - .fetch_all_signature_headers() |
| 448 | + .lookup_component(&CoveredComponent::HTTP(HTTPField { |
| 449 | + name: "signature".to_string(), |
| 450 | + parameters: components::HTTPFieldParametersSet(vec![]), |
| 451 | + })) |
440 | 452 | .into_iter() |
441 | 453 | .filter_map(|sig_input| sfv::Parser::new(&sig_input).parse_dictionary().ok()) |
442 | 454 | .reduce(|mut acc, sig_input| { |
443 | 455 | acc.extend(sig_input); |
444 | 456 | acc |
445 | 457 | }) |
446 | 458 | .ok_or(ImplementationError::ParsingError( |
447 | | - "No `Signature` headers found".to_string(), |
| 459 | + "No validly-formatted `Signature` headers found".to_string(), |
448 | 460 | ))?; |
449 | 461 |
|
450 | 462 | let (label, innerlist) = signature_input |
@@ -483,7 +495,11 @@ impl MessageVerifier { |
483 | 495 | let base = builder.into_signature_base(message)?; |
484 | 496 |
|
485 | 497 | Ok(MessageVerifier { |
486 | | - parsed: ParsedLabel { signature, base }, |
| 498 | + parsed: ParsedLabel { |
| 499 | + label, |
| 500 | + signature, |
| 501 | + base, |
| 502 | + }, |
487 | 503 | }) |
488 | 504 | } |
489 | 505 |
|
@@ -550,18 +566,22 @@ mod tests { |
550 | 566 | struct StandardTestVector {} |
551 | 567 |
|
552 | 568 | impl SignedMessage for StandardTestVector { |
553 | | - fn fetch_all_signature_headers(&self) -> Vec<String> { |
554 | | - vec!["sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned()] |
555 | | - } |
556 | | - fn fetch_all_signature_inputs(&self) -> Vec<String> { |
557 | | - vec![r#"sig1=("@authority");created=1735689600;keyid="poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U";alg="ed25519";expires=1735693200;nonce="gubxywVx7hzbYKatLgzuKDllDAIXAkz41PydU7aOY7vT+Mb3GJNxW0qD4zJ+IOQ1NVtg+BNbTCRUMt1Ojr5BgA==";tag="web-bot-auth""#.to_owned()] |
558 | | - } |
559 | | - fn lookup_component(&self, name: &CoveredComponent) -> Option<String> { |
560 | | - match *name { |
| 569 | + fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> { |
| 570 | + match name { |
| 571 | + CoveredComponent::HTTP(HTTPField { name, .. }) => { |
| 572 | + if name == "signature" { |
| 573 | + return vec!["sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned()]; |
| 574 | + } |
| 575 | + |
| 576 | + if name == "signature-input" { |
| 577 | + return vec![r#"sig1=("@authority");created=1735689600;keyid="poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U";alg="ed25519";expires=1735693200;nonce="gubxywVx7hzbYKatLgzuKDllDAIXAkz41PydU7aOY7vT+Mb3GJNxW0qD4zJ+IOQ1NVtg+BNbTCRUMt1Ojr5BgA==";tag="web-bot-auth""#.to_owned()]; |
| 578 | + } |
| 579 | + vec![] |
| 580 | + } |
561 | 581 | CoveredComponent::Derived(DerivedComponent::Authority { .. }) => { |
562 | | - Some("example.com".to_string()) |
| 582 | + vec!["example.com".to_string()] |
563 | 583 | } |
564 | | - _ => None, |
| 584 | + _ => vec![], |
565 | 585 | } |
566 | 586 | } |
567 | 587 | } |
|
0 commit comments