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