Skip to main content

web_bot_auth/
lib.rs

1#![deny(missing_docs)]
2// Copyright (c) 2025 Cloudflare, Inc.
3// Licensed under the Apache 2.0 license found in the LICENSE file or at:
4//     https://opensource.org/licenses/Apache-2.0
5
6//! # web-bot-auth library
7//!
8//! `web-bot-auth` is a library provides a Rust implementation of HTTP Message Signatures as defined in
9//! [RFC 9421](https://datatracker.ietf.org/doc/html/rfc9421), with additional support
10//! for verifying a web bot auth signed message.
11//!
12//! ## Features
13//!
14//! - **Message Signing**: Generate HTTP message signatures using Ed25519 cryptography
15//! - **Message Verification**: Verify signed HTTP messages against public keys
16//! - **Web Bot Auth**: Specialized verification for automated agents with additional security requirements
17/// HTTP message components that can be present in a given signed / unsigned message, and all the logic
18/// to parse it from an incoming message.
19pub mod components;
20
21/// Implementation of a JSON Web Key manager suitable for use with `web-bot-auth`. Can be used for arbitrary
22/// HTTP message signatures as well.
23pub mod keyring;
24
25/// Implementation of HTTP Message Signatures
26pub 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/// Errors that may be thrown by this module.
37#[derive(Debug)]
38pub enum ImplementationError {
39    /// Errors that arise from invalid conversions from
40    /// parsed structs back into structured field values,
41    /// nominally "impossible" because the structs are already
42    /// in a valid state.
43    ImpossibleSfvError(sfv::Error),
44    /// Errors that arise from conversions of structured field
45    /// values into parsed structs, with an explanation of what
46    /// wrong.
47    ParsingError(String),
48    /// Errors raised when trying to get the value of a covered
49    /// component fails from a `SignedMessage` or `UnsignedMessage`,
50    /// likely because the message did not contain the value.
51    LookupError(CoveredComponent),
52    /// Errors raised when an incoming message references an algorithm
53    /// that isn't currently supported by this implementation. The subset
54    /// of [registered IANA signature algorithms](https://www.iana.org/assignments/http-message-signature/http-message-signature.xhtml)
55    /// implemented here is provided by `Algorithms` struct.
56    UnsupportedAlgorithm(Algorithm),
57    /// An attempt to resolve key identifier to a valid public key failed.
58    /// This prevents message verification.
59    NoSuchKey,
60    /// The resolved key ID did not have the sufficient length to be parsed as
61    /// a valid key for the algorithm chosen.
62    InvalidKeyLength,
63    /// The signature provided in `Signature` header was not long enough to be
64    /// a valid signature for the algorithm chosen.
65    InvalidSignatureLength,
66    /// Verification of a parsed signature against a resolved key failed, indicating
67    /// the signature was invalid.
68    FailedToVerify(ed25519_dalek::SignatureError),
69    /// A valid signature base must contain only ASCII characters; this error is thrown
70    /// if that's not the case. This may be thrown if some of the headers included in
71    /// covered components contained non-ASCII characters, for example. This will be thrown
72    /// during both signing and verification, as both steps require constructing the signature
73    /// base.
74    NonAsciiContentFound,
75    /// Signature bases are terminated with a line beginning with `@signature-params`. This error
76    /// is thrown if the value of that line could not be converted into a structured field value.
77    /// This is considered "impossible" as invalid values should not be present in the structure
78    /// containing those values.
79    SignatureParamsSerialization,
80    /// A wrapper around `WebBotAuthError`
81    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/// Errors thrown when verifying a Web Bot Auth-signed message specifically.
126#[derive(Debug)]
127pub enum WebBotAuthError {
128    /// Thrown when the signature is detected to be expired, using the `expires`
129    /// and `creates` method.
130    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/// A verifier for Web Bot Auth messages specifically.
144#[derive(Clone, Debug)]
145pub struct WebBotAuthVerifier {
146    message_verifier: MessageVerifier,
147    /// List of valid `Signature-Agent` headers to try, if present.
148    parsed_directories: Vec<SignatureAgentLink>,
149}
150
151/// The different types of URLs a `Signature-Agent` can have.
152#[derive(Eq, PartialEq, Debug, Clone)]
153pub enum SignatureAgentLink {
154    /// A data URL that was parsed into a JSON Web Key Set ahead of time.
155    Inline(JSONWebKeySet),
156    /// An external https:// or http:// URL that requires resolution into a JSON Web Key Set
157    External(String),
158}
159
160impl WebBotAuthVerifier {
161    /// Parse a message into a structure that is ready for verification against an
162    /// external key with a suitable algorithm. If `alg` is not set, a default will
163    /// be chosen from the `alg` parameter.
164    ///
165    /// # Errors
166    ///
167    /// Returns `ImplementationErrors` relevant to verifying and parsing.
168    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    /// Obtain list of Signature-Agents parsed and ready. This method is ideal for populating
286    /// a keyring ahead of time at your discretion.
287    pub fn get_signature_agents(&self) -> &Vec<SignatureAgentLink> {
288        &self.parsed_directories
289    }
290
291    /// Verify the messsage, consuming the verifier in the process.
292    /// If `key_id` is not supplied, a key ID to fetch the public key
293    /// from `keyring` will be sourced from the `keyid` parameter
294    /// within the message.
295    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    /// Retrieve the contents of the chosen signature and signature input label for
304    /// verification.
305    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        // Since the expiry date is in the past.
365        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}