1#![deny(missing_docs)]
2pub mod components;
20
21pub mod keyring;
24
25pub mod message_signatures;
27
28use components::CoveredComponent;
29use message_signatures::{MessageVerifier, ParsedLabel, SignatureTiming, SignedMessage};
30
31use data_url::DataUrl;
32use keyring::{Algorithm, JSONWebKeySet, KeyRing};
33
34use crate::components::{HTTPField, HTTPFieldParameters};
35
36#[derive(Debug)]
38pub enum ImplementationError {
39 ImpossibleSfvError(sfv::Error),
44 ParsingError(String),
48 LookupError(CoveredComponent),
52 UnsupportedAlgorithm(Algorithm),
57 NoSuchKey,
60 InvalidKeyLength,
63 InvalidSignatureLength,
66 FailedToVerify(ed25519_dalek::SignatureError),
69 NonAsciiContentFound,
75 SignatureParamsSerialization,
80 WebBotAuth(WebBotAuthError),
82}
83
84impl core::fmt::Display for ImplementationError {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 match self {
87 ImplementationError::ImpossibleSfvError(e) => {
88 write!(f, "impossible structured field value error: {e}")
89 }
90 ImplementationError::ParsingError(s) => write!(f, "parsing error: {s}"),
91 ImplementationError::LookupError(component) => {
92 write!(f, "lookup error: component not found: {component:?}")
93 }
94 ImplementationError::UnsupportedAlgorithm(alg) => {
95 write!(f, "unsupported algorithm: {alg:?}")
96 }
97 ImplementationError::NoSuchKey => write!(f, "no such key"),
98 ImplementationError::InvalidKeyLength => write!(f, "invalid key length"),
99 ImplementationError::InvalidSignatureLength => write!(f, "invalid signature length"),
100 ImplementationError::FailedToVerify(e) => {
101 write!(f, "signature verification failed: {e}")
102 }
103 ImplementationError::NonAsciiContentFound => {
104 write!(f, "non-ASCII content found in signature base")
105 }
106 ImplementationError::SignatureParamsSerialization => {
107 write!(f, "failed to serialize signature params")
108 }
109 ImplementationError::WebBotAuth(e) => write!(f, "web bot auth error: {e}"),
110 }
111 }
112}
113
114impl core::error::Error for ImplementationError {
115 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
116 match self {
117 ImplementationError::ImpossibleSfvError(e) => Some(e),
118 ImplementationError::FailedToVerify(e) => Some(e),
119 ImplementationError::WebBotAuth(e) => Some(e),
120 _ => None,
121 }
122 }
123}
124
125#[derive(Debug)]
127pub enum WebBotAuthError {
128 SignatureIsExpired,
131}
132
133impl core::fmt::Display for WebBotAuthError {
134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 match self {
136 WebBotAuthError::SignatureIsExpired => write!(f, "signature is expired"),
137 }
138 }
139}
140
141impl core::error::Error for WebBotAuthError {}
142
143#[derive(Clone, Debug)]
145pub struct WebBotAuthVerifier {
146 message_verifier: MessageVerifier,
147 parsed_directories: Vec<SignatureAgentLink>,
149}
150
151#[derive(Eq, PartialEq, Debug, Clone)]
153pub enum SignatureAgentLink {
154 Inline(JSONWebKeySet),
156 External(String),
158}
159
160impl WebBotAuthVerifier {
161 pub fn parse(message: &impl SignedMessage) -> Result<Self, ImplementationError> {
169 let signature_agents = message.lookup_component(&CoveredComponent::HTTP(HTTPField {
170 name: "signature-agent".to_string(),
171 parameters: components::HTTPFieldParametersSet(vec![]),
172 }));
173
174 let message_verifier =
175 MessageVerifier::parse(message, |(_, innerlist)| {
176 innerlist.params.contains_key("keyid")
177 && innerlist.params.contains_key("tag")
178 && innerlist.params.contains_key("expires")
179 && innerlist.params.contains_key("created")
180 && innerlist
181 .params
182 .get("tag")
183 .and_then(|tag| tag.as_string())
184 .is_some_and(|tag| tag.as_str() == "web-bot-auth")
185 && (innerlist.items.iter().any(|item| {
186 *item == sfv::Item::new(sfv::StringRef::constant("@authority"))
187 }) || innerlist.items.iter().any(|item| {
188 *item == sfv::Item::new(sfv::StringRef::constant("@target-uri"))
189 }))
190 && (if !signature_agents.is_empty() {
191 innerlist.items.iter().any(|item| {
192 item.bare_item
193 .as_string()
194 .is_some_and(|i| i == sfv::StringRef::constant("signature-agent"))
195 })
196 } else {
197 true
198 })
199 })?;
200
201 let mut signature_agent_key: Option<String> = None;
202 'outer_loop: for (component, _) in message_verifier.parsed.base.components.iter() {
203 if let CoveredComponent::HTTP(HTTPField { name, parameters }) = component
204 && name == "signature-agent"
205 {
206 for parameter in parameters.0.iter() {
207 if let HTTPFieldParameters::Key(key) = parameter {
208 signature_agent_key = Some(key.clone());
209 break 'outer_loop;
210 }
211 }
212 }
213 }
214
215 let parse_link = |link: &sfv::StringRef| {
216 let link_str = link.as_str();
217 if link_str.starts_with("https://") || link_str.starts_with("http://") {
218 return Some(SignatureAgentLink::External(String::from(link_str)));
219 }
220
221 if let Ok(url) = DataUrl::process(link_str) {
222 let mediatype = url.mime_type();
223 if mediatype.type_ == "application"
224 && mediatype.subtype == "http-message-signatures-directory"
225 && let Ok((body, _)) = url.decode_to_vec()
226 && let Ok(jwks) = serde_json::from_slice::<JSONWebKeySet>(&body)
227 {
228 return Some(SignatureAgentLink::Inline(jwks));
229 }
230 }
231
232 None
233 };
234
235 let parsed_directories = match signature_agent_key {
236 Some(key) => signature_agents
237 .iter()
238 .filter_map(|header| sfv::Parser::new(header).parse_dictionary().ok())
239 .reduce(|mut acc, sig_agent| {
240 acc.extend(sig_agent);
241 acc
242 })
243 .ok_or(ImplementationError::ParsingError(
244 "Failed to parse `Signature-Agent` into valid sfv::Dictionary".to_string(),
245 ))?
246 .into_iter()
247 .filter_map(|(label, listentry)| match listentry {
248 sfv::ListEntry::Item(item) => Some((label, item)),
249 sfv::ListEntry::InnerList(_) => None,
250 })
251 .filter_map(|(label, item)| {
252 if label.as_str() != key {
253 return None;
254 }
255 let as_string = item.bare_item.as_string();
256 as_string.and_then(parse_link)
257 })
258 .collect(),
259 None => signature_agents
260 .iter()
261 .map(|header| {
262 sfv::Parser::new(header).parse_item().map_err(|e| {
263 ImplementationError::ParsingError(format!(
264 "Failed to parse `Signature-Agent` into valid sfv::Item: {e}"
265 ))
266 })
267 })
268 .collect::<Result<Vec<_>, _>>()?
269 .iter()
270 .flat_map(|item| {
271 let as_string = item.bare_item.as_string();
272 as_string.and_then(parse_link)
273 })
274 .collect(),
275 };
276
277 let web_bot_auth_verifier = Self {
278 message_verifier,
279 parsed_directories,
280 };
281
282 Ok(web_bot_auth_verifier)
283 }
284
285 pub fn get_signature_agents(&self) -> &Vec<SignatureAgentLink> {
288 &self.parsed_directories
289 }
290
291 pub fn verify(
296 self,
297 keyring: &KeyRing,
298 key_id: Option<String>,
299 ) -> Result<SignatureTiming, ImplementationError> {
300 self.message_verifier.verify(keyring, key_id)
301 }
302
303 pub fn get_parsed_label(&self) -> &ParsedLabel {
306 &self.message_verifier.parsed
307 }
308}
309
310#[cfg(test)]
311mod tests {
312
313 use std::time::Duration;
314
315 use components::DerivedComponent;
316 use indexmap::IndexMap;
317
318 use super::*;
319
320 struct StandardTestVector {}
321
322 impl SignedMessage for StandardTestVector {
323 fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
324 match name {
325 CoveredComponent::HTTP(HTTPField { name, .. }) => {
326 if name == "signature" {
327 return vec!["sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned()];
328 }
329
330 if name == "signature-input" {
331 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()];
332 }
333 vec![]
334 }
335 CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
336 vec!["example.com".to_string()]
337 }
338 _ => vec![],
339 }
340 }
341 }
342
343 #[test]
344 fn test_verifying_as_web_bot_auth() {
345 let test = StandardTestVector {};
346 let public_key: [u8; ed25519_dalek::PUBLIC_KEY_LENGTH] = [
347 0x26, 0xb4, 0x0b, 0x8f, 0x93, 0xff, 0xf3, 0xd8, 0x97, 0x11, 0x2f, 0x7e, 0xbc, 0x58,
348 0x2b, 0x23, 0x2d, 0xbd, 0x72, 0x51, 0x7d, 0x08, 0x2f, 0xe8, 0x3c, 0xfb, 0x30, 0xdd,
349 0xce, 0x43, 0xd1, 0xbb,
350 ];
351 let mut keyring = KeyRing::default();
352 keyring.import_raw(
353 "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U".to_string(),
354 Algorithm::Ed25519,
355 public_key.to_vec(),
356 );
357 let verifier = WebBotAuthVerifier::parse(&test).unwrap();
358 let advisory = verifier
359 .get_parsed_label()
360 .base
361 .parameters
362 .details
363 .possibly_insecure(|_| false);
364 assert!(advisory.is_expired.unwrap_or(true));
366 assert!(!advisory.nonce_is_invalid.unwrap_or(true));
367 let timing = verifier.verify(&keyring, None).unwrap();
368
369 assert!(timing.generation.as_nanos() > 0);
370 assert!(timing.verification.as_nanos() > 0);
371 }
372
373 #[test]
374 fn test_signing_then_verifying() {
375 struct MyTest {
376 signature_input: String,
377 signature_header: String,
378 }
379
380 impl message_signatures::UnsignedMessage for MyTest {
381 fn fetch_components_to_cover(&self) -> IndexMap<CoveredComponent, String> {
382 IndexMap::from_iter([(
383 CoveredComponent::Derived(DerivedComponent::Authority { req: false }),
384 "example.com".to_string(),
385 )])
386 }
387
388 fn register_header_contents(
389 &mut self,
390 signature_input: String,
391 signature_header: String,
392 ) {
393 self.signature_input = format!("sig1={signature_input}");
394 self.signature_header = format!("sig1={signature_header}");
395 }
396 }
397
398 impl SignedMessage for MyTest {
399 fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
400 match name {
401 CoveredComponent::HTTP(HTTPField { name, .. }) => {
402 if name == "signature" {
403 return vec![self.signature_header.clone()];
404 }
405
406 if name == "signature-input" {
407 return vec![self.signature_input.clone()];
408 }
409 vec![]
410 }
411 CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
412 vec!["example.com".to_string()]
413 }
414 _ => vec![],
415 }
416 }
417 }
418
419 let public_key: [u8; ed25519_dalek::PUBLIC_KEY_LENGTH] = [
420 0x26, 0xb4, 0x0b, 0x8f, 0x93, 0xff, 0xf3, 0xd8, 0x97, 0x11, 0x2f, 0x7e, 0xbc, 0x58,
421 0x2b, 0x23, 0x2d, 0xbd, 0x72, 0x51, 0x7d, 0x08, 0x2f, 0xe8, 0x3c, 0xfb, 0x30, 0xdd,
422 0xce, 0x43, 0xd1, 0xbb,
423 ];
424
425 let private_key: [u8; ed25519_dalek::SECRET_KEY_LENGTH] = [
426 0x9f, 0x83, 0x62, 0xf8, 0x7a, 0x48, 0x4a, 0x95, 0x4e, 0x6e, 0x74, 0x0c, 0x5b, 0x4c,
427 0x0e, 0x84, 0x22, 0x91, 0x39, 0xa2, 0x0a, 0xa8, 0xab, 0x56, 0xff, 0x66, 0x58, 0x6f,
428 0x6a, 0x7d, 0x29, 0xc5,
429 ];
430
431 let mut keyring = KeyRing::default();
432 keyring.import_raw(
433 "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U".to_string(),
434 Algorithm::Ed25519,
435 public_key.to_vec(),
436 );
437
438 let signer = message_signatures::MessageSigner {
439 keyid: "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U".into(),
440 nonce: "end-to-end-test".into(),
441 tag: "web-bot-auth".into(),
442 };
443
444 let mut mytest = MyTest {
445 signature_input: String::new(),
446 signature_header: String::new(),
447 };
448
449 signer
450 .generate_signature_headers_content(
451 &mut mytest,
452 Duration::from_secs(10),
453 Algorithm::Ed25519,
454 &private_key,
455 )
456 .unwrap();
457
458 let verifier = WebBotAuthVerifier::parse(&mytest).unwrap();
459 let advisory = verifier
460 .get_parsed_label()
461 .base
462 .parameters
463 .details
464 .possibly_insecure(|_| false);
465 assert!(!advisory.is_expired.unwrap_or(true));
466 assert!(!advisory.nonce_is_invalid.unwrap_or(true));
467
468 let timing = verifier.verify(&keyring, None).unwrap();
469 assert!(timing.generation.as_nanos() > 0);
470 assert!(timing.verification.as_nanos() > 0);
471 }
472
473 #[test]
474 fn test_missing_tags_break_web_bot_auth() {
475 struct MissingParametersTestVector {}
476
477 impl SignedMessage for MissingParametersTestVector {
478 fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
479 match name {
480 CoveredComponent::HTTP(HTTPField { name, .. }) => {
481 if name == "signature" {
482 return vec![
483 "sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned()
484 ];
485 }
486
487 if name == "signature-input" {
488 return vec![r#"sig1=("@authority");created=1735689600;keyid="poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U";alg="ed25519";expires=1735693200;nonce="gubxywVx7hzbYKatLgzuKDllDAIXAkz41PydU7aOY7vT+Mb3GJNxW0qD4zJ+IOQ1NVtg+BNbTCRUMt1Ojr5BgA==";tag="not-web-bot-auth""#.to_owned()];
489 }
490 vec![]
491 }
492 CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
493 vec!["example.com".to_string()]
494 }
495 _ => vec![],
496 }
497 }
498 }
499
500 let test = MissingParametersTestVector {};
501 WebBotAuthVerifier::parse(&test).expect_err("This should not have parsed");
502 }
503
504 #[test]
505 fn test_signature_agents_are_required_in_signature_input() {
506 struct MissingParametersTestVector {}
507
508 impl SignedMessage for MissingParametersTestVector {
509 fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
510 match name {
511 CoveredComponent::HTTP(HTTPField { name, .. }) => {
512 if name == "signature" {
513 return vec!["sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned()];
514 }
515
516 if name == "signature-input" {
517 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()];
518 }
519
520 if name == "signature-agent" {
521 return vec![String::from("\"https://myexample.com\"")];
522 }
523 vec![]
524 }
525 CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
526 vec!["example.com".to_string()]
527 }
528 _ => vec![],
529 }
530 }
531 }
532
533 let test = MissingParametersTestVector {};
534 WebBotAuthVerifier::parse(&test).expect_err("This should not have parsed");
535 }
536
537 #[test]
538 fn test_signature_agents_are_parsed_with_fallback() {
539 struct StandardTestVector {}
540
541 impl SignedMessage for StandardTestVector {
542 fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
543 match name {
544 CoveredComponent::HTTP(HTTPField { name, .. }) => {
545 if name == "signature" {
546 return vec!["sig1=:3q7S1TtbrFhQhpcZ1gZwHPCFHTvdKXNY1xngkp6lyaqqqv3QZupwpu/wQG5a7qybnrj2vZYMeVKuWepm+rNkDw==:".to_owned()];
547 }
548
549 if name == "signature-input" {
550 return vec![r#"sig1=("@authority" "signature-agent");alg="ed25519";keyid="poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U";nonce="ZO3/XMEZjrvSnLtAP9M7jK0WGQf3J+pbmQRUpKDhF9/jsNCWqUh2sq+TH4WTX3/GpNoSZUa8eNWMKqxWp2/c2g==";tag="web-bot-auth";created=1749331474;expires=1749331484"#.to_owned()];
551 }
552
553 if name == "signature-agent" {
554 return vec![
555 String::from("\"https://myexample.com\""),
556 String::from("\"https://myexample2.com\""),
557 ];
558 }
559 vec![]
560 }
561 CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
562 vec!["example.com".to_string()]
563 }
564 _ => vec![],
565 }
566 }
567 }
568
569 let public_key: [u8; ed25519_dalek::PUBLIC_KEY_LENGTH] = [
570 0x26, 0xb4, 0x0b, 0x8f, 0x93, 0xff, 0xf3, 0xd8, 0x97, 0x11, 0x2f, 0x7e, 0xbc, 0x58,
571 0x2b, 0x23, 0x2d, 0xbd, 0x72, 0x51, 0x7d, 0x08, 0x2f, 0xe8, 0x3c, 0xfb, 0x30, 0xdd,
572 0xce, 0x43, 0xd1, 0xbb,
573 ];
574 let mut keyring = KeyRing::default();
575 keyring.import_raw(
576 "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U".to_string(),
577 Algorithm::Ed25519,
578 public_key.to_vec(),
579 );
580
581 let test = StandardTestVector {};
582 let verifier = WebBotAuthVerifier::parse(&test).unwrap();
583 assert_eq!(verifier.get_signature_agents().len(), 2);
584 assert_eq!(
585 verifier.get_signature_agents()[0],
586 SignatureAgentLink::External("https://myexample.com".to_string())
587 );
588 }
589
590 #[test]
591 fn test_signature_agents_are_parsed_correctly() {
592 struct StandardTestVector {}
593
594 impl SignedMessage for StandardTestVector {
595 fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
596 match name {
597 CoveredComponent::HTTP(HTTPField { name, .. }) => {
598 if name == "signature" {
599 return vec!["sig1=:3q7S1TtbrFhQhpcZ1gZwHPCFHTvdKXNY1xngkp6lyaqqqv3QZupwpu/wQG5a7qybnrj2vZYMeVKuWepm+rNkDw==:".to_owned()];
600 }
601
602 if name == "signature-input" {
603 return vec![r#"sig1=("@authority" "signature-agent";key="agent1");alg="ed25519";keyid="poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U";nonce="ZO3/XMEZjrvSnLtAP9M7jK0WGQf3J+pbmQRUpKDhF9/jsNCWqUh2sq+TH4WTX3/GpNoSZUa8eNWMKqxWp2/c2g==";tag="web-bot-auth";created=1749331474;expires=1749331484"#.to_owned()];
604 }
605
606 if name == "signature-agent" {
607 return vec![
608 r#"agent1="https://myexample.com", agent2="https://example2.com""#
609 .to_owned(),
610 ];
611 }
612 vec![]
613 }
614 CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
615 vec!["example.com".to_string()]
616 }
617 _ => vec![],
618 }
619 }
620 }
621
622 let public_key: [u8; ed25519_dalek::PUBLIC_KEY_LENGTH] = [
623 0x26, 0xb4, 0x0b, 0x8f, 0x93, 0xff, 0xf3, 0xd8, 0x97, 0x11, 0x2f, 0x7e, 0xbc, 0x58,
624 0x2b, 0x23, 0x2d, 0xbd, 0x72, 0x51, 0x7d, 0x08, 0x2f, 0xe8, 0x3c, 0xfb, 0x30, 0xdd,
625 0xce, 0x43, 0xd1, 0xbb,
626 ];
627 let mut keyring = KeyRing::default();
628 keyring.import_raw(
629 "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U".to_string(),
630 Algorithm::Ed25519,
631 public_key.to_vec(),
632 );
633
634 let test = StandardTestVector {};
635 let verifier = WebBotAuthVerifier::parse(&test).unwrap();
636
637 assert_eq!(verifier.get_signature_agents().len(), 1);
638 assert_eq!(
639 verifier.get_signature_agents()[0],
640 SignatureAgentLink::External("https://myexample.com".to_string())
641 );
642 }
643}