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};
33use std::time::SystemTimeError;
34
35#[derive(Debug)]
37pub enum ImplementationError {
38 ImpossibleSfvError(sfv::Error),
43 ParsingError(String),
47 LookupError(CoveredComponent),
51 UnsupportedAlgorithm(Algorithm),
56 NoSuchKey,
59 InvalidKeyLength,
62 InvalidSignatureLength,
65 FailedToVerify(ed25519_dalek::SignatureError),
68 NonAsciiContentFound,
74 SignatureParamsSerialization,
79 TimeError(SystemTimeError),
83 WebBotAuth(WebBotAuthError),
85}
86
87#[derive(Debug)]
89pub enum WebBotAuthError {
90 SignatureIsExpired,
93}
94pub trait WebBotAuthSignedMessage: SignedMessage {
97 fn fetch_all_signature_agents(&self) -> Vec<String>;
103}
104
105#[derive(Clone, Debug)]
107pub struct WebBotAuthVerifier {
108 message_verifier: MessageVerifier,
109 parsed_directories: Vec<SignatureAgentLink>,
111}
112
113#[derive(Eq, PartialEq, Debug, Clone)]
115pub enum SignatureAgentLink {
116 Inline(JSONWebKeySet),
118 External(String),
120}
121
122impl WebBotAuthVerifier {
123 pub fn parse(message: &impl WebBotAuthSignedMessage) -> Result<Self, ImplementationError> {
131 let signature_agents = message.fetch_all_signature_agents();
132 let web_bot_auth_verifier = Self {
133 message_verifier: MessageVerifier::parse(message, |(_, innerlist)| {
134 innerlist.params.contains_key("keyid")
135 && innerlist.params.contains_key("tag")
136 && innerlist.params.contains_key("expires")
137 && innerlist.params.contains_key("created")
138 && innerlist
139 .params
140 .get("tag")
141 .and_then(|tag| tag.as_string())
142 .is_some_and(|tag| tag.as_str() == "web-bot-auth")
143 && innerlist
144 .items
145 .iter()
146 .any(|item| *item == sfv::Item::new(sfv::StringRef::constant("@authority")))
147 && (if !signature_agents.is_empty() {
148 innerlist.items.iter().any(|item| {
149 *item == sfv::Item::new(sfv::StringRef::constant("signature-agent"))
150 })
151 } else {
152 true
153 })
154 })?,
155 parsed_directories: signature_agents
156 .iter()
157 .map(|header| {
158 sfv::Parser::new(header).parse_item().map_err(|e| {
159 ImplementationError::ParsingError(format!(
160 "Failed to parse `Signature-Agent` into valid sfv::Item: {e}"
161 ))
162 })
163 })
164 .collect::<Result<Vec<_>, _>>()?
165 .iter()
166 .flat_map(|item| {
167 let as_string = item.bare_item.as_string();
168 as_string.and_then(|link| {
169 let link_str = link.as_str();
170 if link_str.starts_with("https://") || link_str.starts_with("http://") {
171 return Some(SignatureAgentLink::External(String::from(link_str)));
172 }
173
174 if let Ok(url) = DataUrl::process(link_str) {
175 let mediatype = url.mime_type();
176 if mediatype.type_ == "application"
177 && mediatype.subtype == "http-message-signatures-directory"
178 {
179 if let Ok((body, _)) = url.decode_to_vec() {
180 if let Ok(jwks) = serde_json::from_slice::<JSONWebKeySet>(&body)
181 {
182 return Some(SignatureAgentLink::Inline(jwks));
183 }
184 }
185 }
186 }
187
188 None
189 })
190 })
191 .collect(),
192 };
193
194 Ok(web_bot_auth_verifier)
195 }
196
197 pub fn get_signature_agents(&self) -> &Vec<SignatureAgentLink> {
200 &self.parsed_directories
201 }
202
203 pub fn verify(
208 self,
209 keyring: &KeyRing,
210 key_id: Option<String>,
211 ) -> Result<SignatureTiming, ImplementationError> {
212 self.message_verifier.verify(keyring, key_id)
213 }
214
215 pub fn get_parsed_label(&self) -> &ParsedLabel {
218 &self.message_verifier.parsed
219 }
220}
221
222#[cfg(test)]
223mod tests {
224
225 use components::DerivedComponent;
226 use indexmap::IndexMap;
227
228 use super::*;
229
230 struct StandardTestVector {}
231
232 impl SignedMessage for StandardTestVector {
233 fn fetch_all_signature_headers(&self) -> Vec<String> {
234 vec!["sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned()]
235 }
236 fn fetch_all_signature_inputs(&self) -> Vec<String> {
237 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()]
238 }
239 fn lookup_component(&self, name: &CoveredComponent) -> Option<String> {
240 match *name {
241 CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
242 Some("example.com".to_string())
243 }
244 _ => None,
245 }
246 }
247 }
248
249 impl WebBotAuthSignedMessage for StandardTestVector {
250 fn fetch_all_signature_agents(&self) -> Vec<String> {
251 vec![]
252 }
253 }
254
255 #[test]
256 fn test_verifying_as_web_bot_auth() {
257 let test = StandardTestVector {};
258 let public_key: [u8; ed25519_dalek::PUBLIC_KEY_LENGTH] = [
259 0x26, 0xb4, 0x0b, 0x8f, 0x93, 0xff, 0xf3, 0xd8, 0x97, 0x11, 0x2f, 0x7e, 0xbc, 0x58,
260 0x2b, 0x23, 0x2d, 0xbd, 0x72, 0x51, 0x7d, 0x08, 0x2f, 0xe8, 0x3c, 0xfb, 0x30, 0xdd,
261 0xce, 0x43, 0xd1, 0xbb,
262 ];
263 let mut keyring = KeyRing::default();
264 keyring.import_raw(
265 "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U".to_string(),
266 Algorithm::Ed25519,
267 public_key.to_vec(),
268 );
269 let verifier = WebBotAuthVerifier::parse(&test).unwrap();
270 let advisory = verifier
271 .get_parsed_label()
272 .base
273 .parameters
274 .details
275 .possibly_insecure(|_| false);
276 assert!(advisory.is_expired.unwrap_or(true));
278 assert!(!advisory.nonce_is_invalid.unwrap_or(true));
279 let timing = verifier.verify(&keyring, None).unwrap();
280 assert!(timing.generation.as_nanos() > 0);
281 assert!(timing.verification.as_nanos() > 0);
282 }
283
284 #[test]
285 fn test_signing_then_verifying() {
286 struct MyTest {
287 signature_input: String,
288 signature_header: String,
289 }
290
291 impl message_signatures::UnsignedMessage for MyTest {
292 fn fetch_components_to_cover(&self) -> IndexMap<CoveredComponent, String> {
293 IndexMap::from_iter([(
294 CoveredComponent::Derived(DerivedComponent::Authority { req: false }),
295 "example.com".to_string(),
296 )])
297 }
298
299 fn register_header_contents(
300 &mut self,
301 signature_input: String,
302 signature_header: String,
303 ) {
304 self.signature_input = format!("sig1={signature_input}");
305 self.signature_header = format!("sig1={signature_header}");
306 }
307 }
308
309 impl SignedMessage for MyTest {
310 fn fetch_all_signature_headers(&self) -> Vec<String> {
311 vec![self.signature_header.clone()]
312 }
313 fn fetch_all_signature_inputs(&self) -> Vec<String> {
314 vec![self.signature_input.clone()]
315 }
316 fn lookup_component(&self, name: &CoveredComponent) -> Option<String> {
317 match *name {
318 CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
319 Some("example.com".to_string())
320 }
321 _ => None,
322 }
323 }
324 }
325
326 impl WebBotAuthSignedMessage for MyTest {
327 fn fetch_all_signature_agents(&self) -> Vec<String> {
328 vec![]
329 }
330 }
331
332 let public_key: [u8; ed25519_dalek::PUBLIC_KEY_LENGTH] = [
333 0x26, 0xb4, 0x0b, 0x8f, 0x93, 0xff, 0xf3, 0xd8, 0x97, 0x11, 0x2f, 0x7e, 0xbc, 0x58,
334 0x2b, 0x23, 0x2d, 0xbd, 0x72, 0x51, 0x7d, 0x08, 0x2f, 0xe8, 0x3c, 0xfb, 0x30, 0xdd,
335 0xce, 0x43, 0xd1, 0xbb,
336 ];
337
338 let private_key: [u8; ed25519_dalek::SECRET_KEY_LENGTH] = [
339 0x9f, 0x83, 0x62, 0xf8, 0x7a, 0x48, 0x4a, 0x95, 0x4e, 0x6e, 0x74, 0x0c, 0x5b, 0x4c,
340 0x0e, 0x84, 0x22, 0x91, 0x39, 0xa2, 0x0a, 0xa8, 0xab, 0x56, 0xff, 0x66, 0x58, 0x6f,
341 0x6a, 0x7d, 0x29, 0xc5,
342 ];
343
344 let mut keyring = KeyRing::default();
345 keyring.import_raw(
346 "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U".to_string(),
347 Algorithm::Ed25519,
348 public_key.to_vec(),
349 );
350
351 let signer = message_signatures::MessageSigner {
352 keyid: "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U".into(),
353 nonce: "end-to-end-test".into(),
354 tag: "web-bot-auth".into(),
355 };
356
357 let mut mytest = MyTest {
358 signature_input: String::new(),
359 signature_header: String::new(),
360 };
361
362 signer
363 .generate_signature_headers_content(
364 &mut mytest,
365 std::time::Duration::from_secs(10),
366 Algorithm::Ed25519,
367 &private_key.to_vec(),
368 )
369 .unwrap();
370
371 let verifier = WebBotAuthVerifier::parse(&mytest).unwrap();
372 let advisory = verifier
373 .get_parsed_label()
374 .base
375 .parameters
376 .details
377 .possibly_insecure(|_| false);
378 assert!(!advisory.is_expired.unwrap_or(true));
379 assert!(!advisory.nonce_is_invalid.unwrap_or(true));
380
381 let timing = verifier.verify(&keyring, None).unwrap();
382 assert!(timing.generation.as_nanos() > 0);
383 assert!(timing.verification.as_nanos() > 0);
384 }
385
386 #[test]
387 fn test_missing_tags_break_web_bot_auth() {
388 struct MissingParametersTestVector {}
389
390 impl SignedMessage for MissingParametersTestVector {
391 fn fetch_all_signature_headers(&self) -> Vec<String> {
392 vec![
393 "sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned()
394 ]
395 }
396 fn fetch_all_signature_inputs(&self) -> Vec<String> {
397 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()]
398 }
399 fn lookup_component(&self, name: &CoveredComponent) -> Option<String> {
400 match *name {
401 CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
402 Some("example.com".to_string())
403 }
404 _ => None,
405 }
406 }
407 }
408
409 impl WebBotAuthSignedMessage for MissingParametersTestVector {
410 fn fetch_all_signature_agents(&self) -> Vec<String> {
411 vec![]
412 }
413 }
414
415 let test = MissingParametersTestVector {};
416 WebBotAuthVerifier::parse(&test).expect_err("This should not have parsed");
417 }
418
419 #[test]
420 fn test_signature_agents_are_required_in_signature_input() {
421 struct MissingParametersTestVector {}
422
423 impl SignedMessage for MissingParametersTestVector {
424 fn fetch_all_signature_headers(&self) -> Vec<String> {
425 vec!["sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned()]
426 }
427 fn fetch_all_signature_inputs(&self) -> Vec<String> {
428 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()]
429 }
430 fn lookup_component(&self, name: &CoveredComponent) -> Option<String> {
431 match *name {
432 CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
433 Some("example.com".to_string())
434 }
435 _ => None,
436 }
437 }
438 }
439
440 impl WebBotAuthSignedMessage for MissingParametersTestVector {
441 fn fetch_all_signature_agents(&self) -> Vec<String> {
442 vec![String::from("\"https://myexample.com\"")]
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_parsed_correctly() {
452 struct StandardTestVector {}
453
454 impl SignedMessage for StandardTestVector {
455 fn fetch_all_signature_headers(&self) -> Vec<String> {
456 vec!["sig1=:3q7S1TtbrFhQhpcZ1gZwHPCFHTvdKXNY1xngkp6lyaqqqv3QZupwpu/wQG5a7qybnrj2vZYMeVKuWepm+rNkDw==:".to_owned()]
457 }
458 fn fetch_all_signature_inputs(&self) -> Vec<String> {
459 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()]
460 }
461 fn lookup_component(&self, name: &CoveredComponent) -> Option<String> {
462 match name {
463 CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
464 Some("example.com".to_string())
465 }
466 CoveredComponent::HTTP(components::HTTPField { name, .. }) => {
467 if name == "signature-agent" {
468 return Some(String::from("\"https://myexample.com\""));
469 }
470 None
471 }
472 _ => None,
473 }
474 }
475 }
476
477 impl WebBotAuthSignedMessage for StandardTestVector {
478 fn fetch_all_signature_agents(&self) -> Vec<String> {
479 vec![String::from("\"https://myexample.com\"")]
480 }
481 }
482
483 let public_key: [u8; ed25519_dalek::PUBLIC_KEY_LENGTH] = [
484 0x26, 0xb4, 0x0b, 0x8f, 0x93, 0xff, 0xf3, 0xd8, 0x97, 0x11, 0x2f, 0x7e, 0xbc, 0x58,
485 0x2b, 0x23, 0x2d, 0xbd, 0x72, 0x51, 0x7d, 0x08, 0x2f, 0xe8, 0x3c, 0xfb, 0x30, 0xdd,
486 0xce, 0x43, 0xd1, 0xbb,
487 ];
488 let mut keyring = KeyRing::default();
489 keyring.import_raw(
490 "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U".to_string(),
491 Algorithm::Ed25519,
492 public_key.to_vec(),
493 );
494
495 let test = StandardTestVector {};
496 let verifier = WebBotAuthVerifier::parse(&test).unwrap();
497 let timing = verifier.verify(&keyring, None).unwrap();
498 assert!(timing.generation.as_nanos() > 0);
499 assert!(timing.verification.as_nanos() > 0);
500 }
501}