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
21use components::CoveredComponent;
22use indexmap::IndexMap;
23use sfv::SerializeValue;
24use std::collections::HashMap;
25use std::fmt;
26use std::fmt::Write as _;
27use std::time::{Duration, Instant, SystemTime, SystemTimeError, UNIX_EPOCH};
28
29/// Errors that may be thrown by this module.
30#[derive(Debug)]
31pub enum ImplementationError {
32    /// Errors that arise from invalid conversions from
33    /// parsed structs back into structured field values,
34    /// nominally "impossible" because the structs are already
35    /// in a valid state.
36    ImpossibleSfvError(sfv::Error),
37    /// Errors that arise from conversions of structured field
38    /// values into parsed structs, with an explanation of what
39    /// wrong.
40    ParsingError(String),
41    /// Errors raised when trying to get the value of a covered
42    /// component fails from a `SignedMessage` or `UnsignedMessage`,
43    /// likely because the message did not contain the value.
44    LookupError(CoveredComponent),
45    /// Errors raised when an incoming message references an algorithm
46    /// that isn't currently supported by this implementation. The subset
47    /// of [registered IANA signature algorithms](https://www.iana.org/assignments/http-message-signature/http-message-signature.xhtml)
48    /// implemented here is provided by `Algorithms` struct.
49    UnsupportedAlgorithm,
50    /// An attempt to resolve key identifier to a valid public key failed.
51    /// This prevents message verification.
52    NoSuchKey,
53    /// The resolved key ID did not have the sufficient length to be parsed as
54    /// a valid key for the algorithm chosen.
55    InvalidKeyLength,
56    /// The signature provided in `Signature` header was not long enough to be
57    /// a valid signature for the algorithm chosen.
58    InvalidSignatureLength,
59    /// Verification of a parsed signature against a resolved key failed, indicating
60    /// the signature was invalid.
61    FailedToVerify,
62    /// A valid signature base must contain only ASCII characters; this error is thrown
63    /// if that's not the case. This may be thrown if some of the headers included in
64    /// covered components contained non-ASCII characters, for example. This will be thrown
65    /// during both signing and verification, as both steps require constructing the signature
66    /// base.
67    NonAsciiContentFound,
68    /// Signature bases are terminated with a line beginning with `@signature-params`. This error
69    /// is thrown if the value of that line could not be converted into a structured field value.
70    /// This is considered "impossible" as invalid values should not be present in the structure
71    /// containing those values.
72    SignatureParamsSerialization,
73    /// Verification of `created` or `expires` component parameter requires use of a system clock.
74    /// This error is thrown if the system clock is configured in ways that prevent adequate time
75    /// resolution, such as the clock believes the start of Unix time is in the future.
76    TimeError(SystemTimeError),
77    /// A wrapper around `WebBotAuthError`
78    WebBotAuth(WebBotAuthError),
79}
80
81/// Errors thrown when verifying a Web Bot Auth-signed message specifically.
82#[derive(Debug)]
83pub enum WebBotAuthError {
84    /// Thrown when the signature is detected to be expired, using the `expires`
85    /// and `creates` method.
86    SignatureIsExpired,
87    /// Thrown today only if a Signature-Agent header is provided and following
88    /// the link in that is enabled. In a future release, we may support fetching
89    /// and ingesting the key.
90    NotImplemented,
91}
92
93#[derive(Clone, Debug)]
94struct SignatureParams {
95    raw: sfv::Parameters,
96    details: ParameterDetails,
97}
98
99/// Parsed values from `Signature-Input` header.
100#[derive(Clone, Debug)]
101pub struct ParameterDetails {
102    /// The value of the `alg` parameter, if present and resolves to a known algorithm.
103    pub algorithm: Option<Algorithm>,
104    /// The value of the `created` parameter, if present.
105    pub created: Option<i64>,
106    /// The value of the `expires` parameter, if present.
107    pub expires: Option<i64>,
108    /// The value of the `keyid` parameter, if present.
109    pub keyid: Option<String>,
110    /// The value of the `nonce` parameter, if present.
111    pub nonce: Option<String>,
112    /// The value of the `tag` parameter,if present.
113    pub tag: Option<String>,
114}
115
116impl From<sfv::Parameters> for SignatureParams {
117    fn from(value: sfv::Parameters) -> Self {
118        let mut parameter_details = ParameterDetails {
119            algorithm: None,
120            created: None,
121            expires: None,
122            keyid: None,
123            nonce: None,
124            tag: None,
125        };
126
127        for (key, val) in &value {
128            match key.as_str() {
129                "alg" => {
130                    parameter_details.algorithm = val.as_string().and_then(|algorithm_string| {
131                        match algorithm_string.as_str() {
132                            "ed25519" => Some(Algorithm::Ed25519),
133                            _ => None,
134                        }
135                    });
136                }
137                "keyid" => {
138                    parameter_details.keyid = val.as_string().map(|s| s.as_str().to_string());
139                }
140                "tag" => parameter_details.tag = val.as_string().map(|s| s.as_str().to_string()),
141                "nonce" => {
142                    parameter_details.nonce = val.as_string().map(|s| s.as_str().to_string());
143                }
144                "created" => {
145                    parameter_details.created = val.as_integer().map(std::convert::Into::into);
146                }
147                "expires" => {
148                    parameter_details.expires = val.as_integer().map(std::convert::Into::into);
149                }
150                _ => {}
151            }
152        }
153
154        Self {
155            raw: value,
156            details: parameter_details,
157        }
158    }
159}
160
161struct SignatureBaseBuilder {
162    components: Vec<CoveredComponent>,
163    parameters: SignatureParams,
164}
165
166impl TryFrom<sfv::InnerList> for SignatureBaseBuilder {
167    type Error = ImplementationError;
168
169    fn try_from(value: sfv::InnerList) -> Result<Self, Self::Error> {
170        Ok(SignatureBaseBuilder {
171            components: value
172                .items
173                .iter()
174                .map(|item| (*item).clone().try_into())
175                .collect::<Result<Vec<CoveredComponent>, ImplementationError>>()?,
176            // Note: it is the responsibility of higher layers to check whether the message is
177            // expired, down here we just parse.
178            parameters: value.params.into(),
179        })
180    }
181}
182
183impl SignatureBaseBuilder {
184    fn into_signature_base(
185        self,
186        message: &impl SignedMessage,
187    ) -> Result<SignatureBase, ImplementationError> {
188        Ok(SignatureBase {
189            components: IndexMap::from_iter(
190                self.components
191                    .into_iter()
192                    .map(|component| match message.lookup_component(&component) {
193                        Some(serialized_value) => Ok((component, serialized_value)),
194                        None => Err(ImplementationError::LookupError(component)),
195                    })
196                    .collect::<Result<Vec<(CoveredComponent, String)>, ImplementationError>>()?,
197            ),
198            parameters: self.parameters,
199        })
200    }
201}
202
203/// A representation of the signature base to be generated during verification and signing.
204#[derive(Clone, Debug)]
205struct SignatureBase {
206    components: IndexMap<CoveredComponent, String>,
207    parameters: SignatureParams,
208}
209
210impl SignatureBase {
211    // Convert `SignatureBase` into its ASCII representation as well as the portion of
212    // itself that corresponds to `@signature-params` line.
213    fn into_ascii(self) -> Result<(String, String), ImplementationError> {
214        let mut output = String::new();
215
216        let mut signature_params_line_items: Vec<sfv::Item> = vec![];
217
218        for (component, serialized_value) in self.components {
219            let sfv_item = match component {
220                CoveredComponent::HTTP(http) => sfv::Item::try_from(http)?,
221                CoveredComponent::Derived(derived) => sfv::Item::try_from(derived)?,
222            };
223
224            let _ = writeln!(
225                output,
226                "{}: {}",
227                sfv_item.serialize_value(),
228                serialized_value
229            );
230            signature_params_line_items.push(sfv_item);
231        }
232
233        let signature_params_line = vec![sfv::ListEntry::InnerList(sfv::InnerList::with_params(
234            signature_params_line_items,
235            self.parameters.raw,
236        ))]
237        .serialize_value()
238        .ok_or(ImplementationError::SignatureParamsSerialization)?;
239
240        let _ = write!(output, "\"@signature-params\": {signature_params_line}");
241
242        if output.is_ascii() {
243            Ok((output, signature_params_line))
244        } else {
245            Err(ImplementationError::NonAsciiContentFound)
246        }
247    }
248
249    fn get_details(&self) -> ParameterDetails {
250        self.parameters.details.clone()
251    }
252
253    fn is_expired(&self) -> Option<bool> {
254        self.parameters.details.expires.map(|expires| {
255            if expires <= 0 {
256                return true;
257            }
258
259            match SystemTime::now().duration_since(UNIX_EPOCH) {
260                Ok(duration) => i64::try_from(duration.as_secs())
261                    .map(|dur| dur >= expires)
262                    .unwrap_or(true),
263                Err(_) => true,
264            }
265        })
266    }
267}
268
269/// Subset of [HTTP signature algorithm](https://www.iana.org/assignments/http-message-signature/http-message-signature.xhtml)
270/// implemented in this module. In the future, we may support more.
271#[derive(Clone, Debug)]
272pub enum Algorithm {
273    /// [The `ed25519` algorithm](https://www.rfc-editor.org/rfc/rfc9421#name-eddsa-using-curve-edwards25)
274    Ed25519,
275}
276
277impl fmt::Display for Algorithm {
278    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
279        match self {
280            Algorithm::Ed25519 => write!(f, "ed25519"),
281        }
282    }
283}
284
285/// Represents a public key to be consumed during the verification.
286pub type PublicKey = Vec<u8>;
287/// Represents a JSON Web Key base64-encoded thumpprint as implemented
288/// per [RFC 7638](https://www.rfc-editor.org/rfc/rfc7638.html)
289pub type Thumbprint = String;
290/// A map from a thumbprint to the public key, to be used to map `keyid`s
291/// to public keys.
292pub type KeyRing = HashMap<Thumbprint, PublicKey>;
293
294/// Trait that messages seeking verification should implement to facilitate looking up
295/// raw values from the underlying message.
296pub trait SignedMessage {
297    /// Obtain the parsed version of `Signature` HTTP header
298    fn fetch_signature_header(&self) -> Option<String>;
299    /// Obtain the parsed version of `Signature-Input` HTTP header
300    fn fetch_signature_input(&self) -> Option<String>;
301    /// Obtain the serialized value of a covered component. Implementations should
302    /// respect any parameter values set on the covered component per the message
303    /// signature spec. Component values that cannot be found must return None.
304    /// `CoveredComponent::HTTP` fields are guaranteed to have lowercase ASCII names, so
305    /// care should be taken to ensure HTTP field names in the message are checked in a
306    /// case-insensitive way.
307    fn lookup_component(&self, name: &CoveredComponent) -> Option<String>;
308}
309
310/// Trait that messages seeking signing should implement to generate `Signature-Input`
311/// and `Signature` header contents.
312pub trait UnsignedMessage {
313    /// Obtain a list of covered components to be included
314    fn fetch_components_to_cover(&self) -> IndexMap<CoveredComponent, String>;
315    /// Store the contents of a generated `Signature-Input` and `Signature` header value.
316    /// It is the responsibility of the application to generate a consistent label for both.
317    /// `signature_header` is guaranteed to be a `sfv` byte sequence element. `signature_input`
318    /// is guaranteed to be `sfv` inner list of strings.
319    fn register_header_contents(&mut self, signature_input: String, signature_header: String);
320}
321
322/// A struct that implements signing. The struct fields here are serialized into the `Signature-Input`
323/// header.
324pub struct MessageSigner {
325    /// Algorith mto use for signing
326    pub algorithm: Algorithm,
327    /// Name to use for `keyid` parameter
328    pub keyid: String,
329    /// A random nonce to be provided for additional security
330    pub nonce: String,
331    /// Value to be used for `tag` parameter
332    pub tag: String,
333}
334
335impl MessageSigner {
336    /// Sign the provided method with `signing_key`, setting an expiration value of
337    /// length `expires` from now (the time of signing).
338    ///
339    /// # Errors
340    ///
341    /// Returns `ImplementationErrors` relevant to signing and parsing.
342    pub fn generate_signature_headers_content(
343        &self,
344        message: &mut impl UnsignedMessage,
345        expires: Duration,
346        signing_key: &PublicKey,
347    ) -> Result<(), ImplementationError> {
348        let components_to_cover = message.fetch_components_to_cover();
349        let mut sfv_parameters = sfv::Parameters::new();
350
351        sfv_parameters.insert(
352            sfv::KeyRef::constant("alg").to_owned(),
353            sfv::BareItem::String(sfv::StringRef::constant(&self.algorithm.to_string()).to_owned()),
354        );
355
356        sfv_parameters.insert(
357            sfv::KeyRef::constant("keyid").to_owned(),
358            sfv::BareItem::String(
359                sfv::StringRef::from_str(&self.keyid)
360                    .map_err(|_| {
361                        ImplementationError::ParsingError(
362                            "keyid contains non-printable ASCII characters".into(),
363                        )
364                    })?
365                    .to_owned(),
366            ),
367        );
368
369        sfv_parameters.insert(
370            sfv::KeyRef::constant("nonce").to_owned(),
371            sfv::BareItem::String(
372                sfv::StringRef::from_str(&self.nonce)
373                    .map_err(|_| {
374                        ImplementationError::ParsingError(
375                            "nonce contains non-printable ASCII characters".into(),
376                        )
377                    })?
378                    .to_owned(),
379            ),
380        );
381
382        sfv_parameters.insert(
383            sfv::KeyRef::constant("tag").to_owned(),
384            sfv::BareItem::String(
385                sfv::StringRef::from_str(&self.tag)
386                    .map_err(|_| {
387                        ImplementationError::ParsingError(
388                            "tag contains non-printable ASCII characters".into(),
389                        )
390                    })?
391                    .to_owned(),
392            ),
393        );
394
395        let created = SystemTime::now()
396            .duration_since(UNIX_EPOCH)
397            .map_err(ImplementationError::TimeError)?;
398        let expiry = created + expires;
399
400        let created_as_i64 = i64::try_from(created.as_secs()).map_err(|_| {
401            ImplementationError::ParsingError(
402                "Clock time does not fit in i64, verfy your clock is set correctly".into(),
403            )
404        })?;
405        let expires_as_i64 = i64::try_from(expiry.as_secs()).map_err(|_| {
406            ImplementationError::ParsingError(
407                "Clcok time + `expires` value does not fit in i64, verfy your duration is valid"
408                    .into(),
409            )
410        })?;
411
412        sfv_parameters.insert(
413            sfv::KeyRef::constant("created").to_owned(),
414            sfv::BareItem::Integer(sfv::Integer::constant(created_as_i64)),
415        );
416
417        sfv_parameters.insert(
418            sfv::KeyRef::constant("expires").to_owned(),
419            sfv::BareItem::Integer(sfv::Integer::constant(expires_as_i64)),
420        );
421
422        let (signature_base, signature_params_content) = SignatureBase {
423            components: components_to_cover,
424            parameters: sfv_parameters.into(),
425        }
426        .into_ascii()?;
427
428        let signature = match self.algorithm {
429            Algorithm::Ed25519 => {
430                use ed25519_dalek::{Signer, SigningKey};
431                let signing_key_dalek = SigningKey::try_from(signing_key.as_slice())
432                    .map_err(|_| ImplementationError::InvalidKeyLength)?;
433
434                sfv::Item {
435                    bare_item: sfv::BareItem::ByteSequence(
436                        signing_key_dalek.sign(signature_base.as_bytes()).to_vec(),
437                    ),
438                    params: sfv::Parameters::new(),
439                }
440                .serialize_value()
441            }
442        };
443
444        message.register_header_contents(signature_params_content, signature);
445
446        Ok(())
447    }
448}
449
450#[derive(Clone, Debug)]
451struct ParsedLabel {
452    signature: Vec<u8>,
453    base: SignatureBase,
454}
455
456/// A `MessageVerifier` performs the verifications needed for a signed message.
457#[derive(Clone, Debug)]
458pub struct MessageVerifier {
459    parsed: ParsedLabel,
460    algorithm: Algorithm,
461}
462
463/// Micro-measurements of different parts of the process in a call to `verify()`.
464/// Useful for measuring overhead.
465#[derive(Clone, Debug)]
466pub struct SignatureTiming {
467    /// Time taken to generate a signature base,
468    pub generation: Duration,
469    /// Time taken to execute cryptographic verification.
470    pub verification: Duration,
471}
472
473impl MessageVerifier {
474    /// Parse a message into a structure that is ready for verification against an
475    /// external key with a suitable algorithm. If `alg` is not set, a default will
476    /// be chosen from the `alg` parameter. `pick` is a predicate
477    /// enabling you to choose which message label should be considered as the message to
478    /// verify - if it is known only one signature is in the message, simply return true.
479    ///
480    /// # Errors
481    ///
482    /// Returns `ImplementationErrors` relevant to verifying and parsing.
483    pub fn parse<P>(
484        message: &impl SignedMessage,
485        alg: Option<Algorithm>,
486        pick: P,
487    ) -> Result<Self, ImplementationError>
488    where
489        P: Fn(&(sfv::Key, sfv::InnerList)) -> bool,
490    {
491        let unparsed_signature_header =
492            message
493                .fetch_signature_header()
494                .ok_or(ImplementationError::ParsingError(
495                    "No `Signature` header value ".into(),
496                ))?;
497
498        let unparsed_signature_input =
499            message
500                .fetch_signature_input()
501                .ok_or(ImplementationError::ParsingError(
502                    "No `Signature-Input` value ".into(),
503                ))?;
504
505        let signature_input = sfv::Parser::new(&unparsed_signature_input)
506            .parse_dictionary()
507            .map_err(|e| {
508                ImplementationError::ParsingError(format!(
509                    "Failed to parse `Signature-Input` header into sfv::Dictionary: {e}"
510                ))
511            })?;
512
513        let mut signature_header = sfv::Parser::new(&unparsed_signature_header)
514            .parse_dictionary()
515            .map_err(|e| {
516                ImplementationError::ParsingError(format!(
517                    "Failed to parse `Signature` header into sfv::Dictionary: {e}"
518                ))
519            })?;
520
521        let (label, innerlist) = signature_input
522            .into_iter()
523            .filter_map(|(label, listentry)| match listentry {
524                sfv::ListEntry::InnerList(inner_list) => Some((label, inner_list)),
525                sfv::ListEntry::Item(_) => None,
526            })
527            .find(pick)
528            .ok_or(ImplementationError::ParsingError(
529                "No matching label and signature base found".into(),
530            ))?;
531
532        let signature = match signature_header.shift_remove(&label).ok_or(
533            ImplementationError::ParsingError("No matching signature found from label".into()),
534        )? {
535            sfv::ListEntry::Item(sfv::Item {
536                bare_item,
537                params: _,
538            }) => match bare_item {
539                sfv::GenericBareItem::ByteSequence(sequence) => sequence,
540                other_type => {
541                    return Err(ImplementationError::ParsingError(format!(
542                        "Invalid type for signature found, expected byte sequence: {other_type:?}"
543                    )));
544                }
545            },
546            other_type @ sfv::ListEntry::InnerList(_) => {
547                return Err(ImplementationError::ParsingError(format!(
548                    "Invalid type for signature found, expected byte sequence: {other_type:?}"
549                )));
550            }
551        };
552
553        let builder = SignatureBaseBuilder::try_from(innerlist)?;
554        let base = builder.into_signature_base(message)?;
555
556        let algorithm = match alg {
557            Some(algorithm) => algorithm,
558            None => base
559                .get_details()
560                .algorithm
561                .clone()
562                .ok_or(ImplementationError::UnsupportedAlgorithm)?,
563        };
564
565        Ok(MessageVerifier {
566            parsed: ParsedLabel { signature, base },
567            algorithm,
568        })
569    }
570
571    /// Retrieve the parsed `ParameterDetails` from the message. Useful for logging
572    /// information about the message.
573    pub fn get_details(&self) -> ParameterDetails {
574        self.parsed.base.parameters.details.clone()
575    }
576
577    /// Verify the messsage, consuming the verifier in the process.
578    /// If `key_id` is not supplied, a key ID to fetch the public key
579    /// from `keyring` will be sourced from the `keyid` parameter
580    /// within the message. Returns information about how long verification
581    /// took if successful.
582    ///
583    /// # Errors
584    ///
585    /// Returns `ImplementationErrors` relevant to verifying and parsing.
586    pub fn verify(
587        self,
588        keyring: &KeyRing,
589        key_id: Option<Thumbprint>,
590    ) -> Result<SignatureTiming, ImplementationError> {
591        let keying_material = (match key_id {
592            Some(key) => keyring.get(&key),
593            None => self
594                .parsed
595                .base
596                .parameters
597                .details
598                .keyid
599                .as_ref()
600                .and_then(|key| keyring.get(key)),
601        })
602        .ok_or(ImplementationError::NoSuchKey)?;
603        let generation = Instant::now();
604        let (base_representation, _) = self.parsed.base.into_ascii()?;
605        let generation = generation.elapsed();
606        match self.algorithm {
607            Algorithm::Ed25519 => {
608                use ed25519_dalek::{Signature, Verifier, VerifyingKey};
609                let verifying_key = VerifyingKey::try_from(keying_material.as_slice())
610                    .map_err(|_| ImplementationError::InvalidKeyLength)?;
611
612                let sig = Signature::try_from(self.parsed.signature.as_slice())
613                    .map_err(|_| ImplementationError::InvalidSignatureLength)?;
614
615                let verification = Instant::now();
616                verifying_key
617                    .verify(base_representation.as_bytes(), &sig)
618                    .map_err(|_| ImplementationError::FailedToVerify)
619                    .map(|()| SignatureTiming {
620                        generation,
621                        verification: verification.elapsed(),
622                    })
623            }
624        }
625    }
626
627    /// Whether or not this message is expired, based on its `expires` value.
628    pub fn is_expired(&self) -> Option<bool> {
629        self.parsed.base.is_expired()
630    }
631}
632
633/// A trait that messages wishing to be verified as a `web-bot-auth` method specifically
634/// must implement.
635pub trait WebBotAuthSignedMessage: SignedMessage {
636    /// Obtain the parsed version of `Signature-Agent` HTTP header
637    fn fetch_signature_agent(&self) -> Option<String>;
638}
639
640/// A verifier for Web Bot Auth messages specifically.
641#[derive(Clone, Debug)]
642pub struct WebBotAuthVerifier {
643    message_verifier: MessageVerifier,
644    /// The value of `Signature-Agent` header, if resolved to a link
645    key_directory: Option<String>,
646}
647
648impl WebBotAuthVerifier {
649    /// Parse a message into a structure that is ready for verification against an
650    /// external key with a suitable algorithm. If `alg` is not set, a default will
651    /// be chosen from the `alg` parameter.
652    ///
653    /// # Errors
654    ///
655    /// Returns `ImplementationErrors` relevant to verifying and parsing.
656    pub fn parse(
657        message: &impl WebBotAuthSignedMessage,
658        algorithm: Option<Algorithm>,
659    ) -> Result<Self, ImplementationError> {
660        let signature_agent = match message.fetch_signature_agent() {
661            Some(agent) => Some(sfv::Parser::new(&agent).parse_item().map_err(|e| {
662                ImplementationError::ParsingError(format!(
663                    "Failed to parse `Signature-Agent` into valid sfv::Item: {e}"
664                ))
665            })?),
666            None => None,
667        };
668
669        let key_directory = signature_agent.and_then(|item| {
670            item.bare_item
671                .as_string()
672                .filter(|link| {
673                    link.as_str().starts_with("https") || link.as_str().starts_with("data")
674                })
675                .map(std::string::ToString::to_string)
676        });
677
678        let web_bot_auth_verifier = Self {
679            message_verifier: MessageVerifier::parse(message, algorithm, |(_, innerlist)| {
680                innerlist.params.contains_key("keyid")
681                    && innerlist.params.contains_key("tag")
682                    && innerlist.params.contains_key("expires")
683                    && innerlist.params.contains_key("created")
684                    && innerlist
685                        .params
686                        .get("tag")
687                        .and_then(|tag| tag.as_string())
688                        .is_some_and(|tag| tag.as_str() == "web-bot-auth")
689                    && innerlist.items.iter().any(|item| {
690                        *item == sfv::Item::new(sfv::StringRef::constant("@authority"))
691                            || (key_directory.is_some()
692                                && *item
693                                    == sfv::Item::new(sfv::StringRef::constant("signature-agent")))
694                    })
695            })?,
696            key_directory,
697        };
698
699        Ok(web_bot_auth_verifier)
700    }
701
702    /// Verify the messsage, consuming the verifier in the process.
703    /// If `key_id` is not supplied, a key ID to fetch the public key
704    /// from `keyring` will be sourced from the `keyid` parameter
705    /// within the message. If `enforce_key_directory_lookup` is set,
706    /// verification will attempt to follow the `Signature-Agent` header
707    /// to ingest the JWK from an external directory. Note: we currently
708    /// do not implement ingesting JWKs from an external directory.
709    ///
710    /// # Errors
711    ///
712    /// Returns `ImplementationErrors` relevant to verifying and parsing.
713    pub fn verify(
714        self,
715        keyring: &KeyRing,
716        key_id: Option<Thumbprint>,
717        enforce_key_directory_lookup: bool,
718    ) -> Result<SignatureTiming, ImplementationError> {
719        if (!enforce_key_directory_lookup && self.key_directory.is_some())
720            || self.key_directory.is_none()
721        {
722            return self.message_verifier.verify(keyring, key_id);
723        }
724
725        Err(ImplementationError::WebBotAuth(
726            WebBotAuthError::NotImplemented,
727        ))
728    }
729
730    /// Retrieve the parsed `ParameterDetails` from the message. Useful for logging
731    /// information about the message.
732    pub fn get_details(&self) -> ParameterDetails {
733        self.message_verifier.get_details()
734    }
735
736    /// Indicates whether or not the message has semantic errors
737    /// that pose a security risk, such as whether or not the message
738    /// is expired, the nonce is invalid, etc.
739    pub fn possibly_insecure(&self) -> bool {
740        self.message_verifier.is_expired().unwrap_or(false)
741
742        // TODO: Validate nonce somehow
743    }
744}
745
746#[cfg(test)]
747mod tests {
748
749    use components::{DerivedComponent, HTTPField, HTTPFieldParametersSet};
750    use indexmap::IndexMap;
751
752    use super::*;
753
754    struct StandardTestVector {}
755
756    impl SignedMessage for StandardTestVector {
757        fn fetch_signature_header(&self) -> Option<String> {
758            Some("sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned())
759        }
760        fn fetch_signature_input(&self) -> Option<String> {
761            Some(r#"sig1=("@authority");created=1735689600;keyid="poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U";alg="ed25519";expires=1735693200;nonce="gubxywVx7hzbYKatLgzuKDllDAIXAkz41PydU7aOY7vT+Mb3GJNxW0qD4zJ+IOQ1NVtg+BNbTCRUMt1Ojr5BgA==";tag="web-bot-auth""#.to_owned())
762        }
763        fn lookup_component(&self, name: &CoveredComponent) -> Option<String> {
764            match *name {
765                CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
766                    Some("example.com".to_string())
767                }
768                _ => None,
769            }
770        }
771    }
772
773    impl WebBotAuthSignedMessage for StandardTestVector {
774        fn fetch_signature_agent(&self) -> Option<String> {
775            None
776        }
777    }
778
779    #[test]
780    fn test_parsing_as_http_signature() {
781        let test = StandardTestVector {};
782        let verifier = MessageVerifier::parse(&test, None, |(_, _)| true).unwrap();
783        let expected_signature_params = "(\"@authority\");created=1735689600;keyid=\"poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U\";alg=\"ed25519\";expires=1735693200;nonce=\"gubxywVx7hzbYKatLgzuKDllDAIXAkz41PydU7aOY7vT+Mb3GJNxW0qD4zJ+IOQ1NVtg+BNbTCRUMt1Ojr5BgA==\";tag=\"web-bot-auth\"";
784        let expected_base = format!(
785            "\"@authority\": example.com\n\"@signature-params\": {expected_signature_params}"
786        );
787        let (base, signature_params) = verifier.parsed.base.into_ascii().unwrap();
788        assert_eq!(base, expected_base.as_str());
789        assert_eq!(signature_params, expected_signature_params);
790    }
791
792    #[test]
793    fn test_verifying_as_http_signature() {
794        let test = StandardTestVector {};
795        let public_key: [u8; ed25519_dalek::PUBLIC_KEY_LENGTH] = [
796            0x26, 0xb4, 0x0b, 0x8f, 0x93, 0xff, 0xf3, 0xd8, 0x97, 0x11, 0x2f, 0x7e, 0xbc, 0x58,
797            0x2b, 0x23, 0x2d, 0xbd, 0x72, 0x51, 0x7d, 0x08, 0x2f, 0xe8, 0x3c, 0xfb, 0x30, 0xdd,
798            0xce, 0x43, 0xd1, 0xbb,
799        ];
800        let keyring: KeyRing = HashMap::from_iter([(
801            "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U".to_string(),
802            public_key.to_vec(),
803        )]);
804        let verifier = MessageVerifier::parse(&test, None, |(_, _)| true).unwrap();
805        let timing = verifier.verify(&keyring, None).unwrap();
806        assert!(timing.generation.as_nanos() > 0);
807        assert!(timing.verification.as_nanos() > 0);
808    }
809
810    #[test]
811    fn test_verifying_as_web_bot_auth() {
812        let test = StandardTestVector {};
813        let public_key: [u8; ed25519_dalek::PUBLIC_KEY_LENGTH] = [
814            0x26, 0xb4, 0x0b, 0x8f, 0x93, 0xff, 0xf3, 0xd8, 0x97, 0x11, 0x2f, 0x7e, 0xbc, 0x58,
815            0x2b, 0x23, 0x2d, 0xbd, 0x72, 0x51, 0x7d, 0x08, 0x2f, 0xe8, 0x3c, 0xfb, 0x30, 0xdd,
816            0xce, 0x43, 0xd1, 0xbb,
817        ];
818        let keyring: KeyRing = HashMap::from_iter([(
819            "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U".to_string(),
820            public_key.to_vec(),
821        )]);
822        let verifier = WebBotAuthVerifier::parse(&test, None).unwrap();
823        // Since the expiry date is in the past.
824        assert!(verifier.possibly_insecure());
825        let timing = verifier.verify(&keyring, None, false).unwrap();
826        assert!(timing.generation.as_nanos() > 0);
827        assert!(timing.verification.as_nanos() > 0);
828    }
829
830    #[test]
831    fn test_signing_then_verifying() {
832        struct MyTest {
833            signature_input: String,
834            signature_header: String,
835        }
836
837        impl UnsignedMessage for MyTest {
838            fn fetch_components_to_cover(&self) -> IndexMap<CoveredComponent, String> {
839                IndexMap::from_iter([(
840                    CoveredComponent::Derived(DerivedComponent::Authority { req: false }),
841                    "example.com".to_string(),
842                )])
843            }
844
845            fn register_header_contents(
846                &mut self,
847                signature_input: String,
848                signature_header: String,
849            ) {
850                self.signature_input = format!("sig1={signature_input}");
851                self.signature_header = format!("sig1={signature_header}");
852            }
853        }
854
855        impl SignedMessage for MyTest {
856            fn fetch_signature_header(&self) -> Option<String> {
857                Some(self.signature_header.clone())
858            }
859            fn fetch_signature_input(&self) -> Option<String> {
860                Some(self.signature_input.clone())
861            }
862            fn lookup_component(&self, name: &CoveredComponent) -> Option<String> {
863                match *name {
864                    CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
865                        Some("example.com".to_string())
866                    }
867                    _ => None,
868                }
869            }
870        }
871
872        impl WebBotAuthSignedMessage for MyTest {
873            fn fetch_signature_agent(&self) -> Option<String> {
874                None
875            }
876        }
877
878        let public_key: [u8; ed25519_dalek::PUBLIC_KEY_LENGTH] = [
879            0x26, 0xb4, 0x0b, 0x8f, 0x93, 0xff, 0xf3, 0xd8, 0x97, 0x11, 0x2f, 0x7e, 0xbc, 0x58,
880            0x2b, 0x23, 0x2d, 0xbd, 0x72, 0x51, 0x7d, 0x08, 0x2f, 0xe8, 0x3c, 0xfb, 0x30, 0xdd,
881            0xce, 0x43, 0xd1, 0xbb,
882        ];
883
884        let private_key: [u8; ed25519_dalek::SECRET_KEY_LENGTH] = [
885            0x9f, 0x83, 0x62, 0xf8, 0x7a, 0x48, 0x4a, 0x95, 0x4e, 0x6e, 0x74, 0x0c, 0x5b, 0x4c,
886            0x0e, 0x84, 0x22, 0x91, 0x39, 0xa2, 0x0a, 0xa8, 0xab, 0x56, 0xff, 0x66, 0x58, 0x6f,
887            0x6a, 0x7d, 0x29, 0xc5,
888        ];
889
890        let keyring: KeyRing = HashMap::from_iter([(
891            "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U".to_string(),
892            public_key.to_vec(),
893        )]);
894
895        let signer = MessageSigner {
896            algorithm: Algorithm::Ed25519,
897            keyid: "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U".into(),
898            nonce: "end-to-end-test".into(),
899            tag: "web-bot-auth".into(),
900        };
901
902        let mut mytest = MyTest {
903            signature_input: String::new(),
904            signature_header: String::new(),
905        };
906
907        signer
908            .generate_signature_headers_content(
909                &mut mytest,
910                Duration::from_secs(10),
911                &private_key.to_vec(),
912            )
913            .unwrap();
914
915        let verifier = WebBotAuthVerifier::parse(&mytest, None).unwrap();
916        assert!(!verifier.possibly_insecure());
917
918        let timing = verifier.verify(&keyring, None, false).unwrap();
919        assert!(timing.generation.as_nanos() > 0);
920        assert!(timing.verification.as_nanos() > 0);
921    }
922
923    #[test]
924    fn test_missing_tags_break_web_bot_auth() {
925        struct MissingParametersTestVector {}
926
927        impl SignedMessage for MissingParametersTestVector {
928            fn fetch_signature_header(&self) -> Option<String> {
929                Some("sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned())
930            }
931            fn fetch_signature_input(&self) -> Option<String> {
932                Some(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())
933            }
934            fn lookup_component(&self, name: &CoveredComponent) -> Option<String> {
935                match *name {
936                    CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
937                        Some("example.com".to_string())
938                    }
939                    _ => None,
940                }
941            }
942        }
943
944        impl WebBotAuthSignedMessage for MissingParametersTestVector {
945            fn fetch_signature_agent(&self) -> Option<String> {
946                None
947            }
948        }
949
950        let test = MissingParametersTestVector {};
951        WebBotAuthVerifier::parse(&test, None).expect_err("This should not have parsed");
952    }
953
954    #[test]
955    fn test_signing() {
956        struct SigningTest {}
957        impl UnsignedMessage for SigningTest {
958            fn fetch_components_to_cover(&self) -> IndexMap<CoveredComponent, String> {
959                IndexMap::from_iter([
960                    (
961                        CoveredComponent::Derived(DerivedComponent::Method { req: false }),
962                        "POST".to_string(),
963                    ),
964                    (
965                        CoveredComponent::Derived(DerivedComponent::Authority { req: false }),
966                        "example.com".to_string(),
967                    ),
968                    (
969                        CoveredComponent::HTTP(HTTPField {
970                            name: "content-length".to_string(),
971                            parameters: HTTPFieldParametersSet(vec![]),
972                        }),
973                        "18".to_string(),
974                    ),
975                ])
976            }
977
978            fn register_header_contents(
979                &mut self,
980                _signature_input: String,
981                _signature_header: String,
982            ) {
983            }
984        }
985
986        let signer = MessageSigner {
987            algorithm: Algorithm::Ed25519,
988            keyid: "test".into(),
989            nonce: "another-test".into(),
990            tag: "web-bot-auth".into(),
991        };
992
993        let private_key: [u8; ed25519_dalek::SECRET_KEY_LENGTH] = [
994            0x9f, 0x83, 0x62, 0xf8, 0x7a, 0x48, 0x4a, 0x95, 0x4e, 0x6e, 0x74, 0x0c, 0x5b, 0x4c,
995            0x0e, 0x84, 0x22, 0x91, 0x39, 0xa2, 0x0a, 0xa8, 0xab, 0x56, 0xff, 0x66, 0x58, 0x6f,
996            0x6a, 0x7d, 0x29, 0xc5,
997        ];
998
999        let mut test = SigningTest {};
1000
1001        assert!(
1002            signer
1003                .generate_signature_headers_content(
1004                    &mut test,
1005                    Duration::from_secs(10),
1006                    &private_key.to_vec()
1007                )
1008                .is_ok()
1009        );
1010    }
1011
1012    #[test]
1013    fn signature_base_generates_the_expected_representation() {
1014        let sigbase = SignatureBase {
1015            components: IndexMap::from_iter([
1016                (
1017                    CoveredComponent::Derived(DerivedComponent::Method { req: false }),
1018                    "POST".to_string(),
1019                ),
1020                (
1021                    CoveredComponent::Derived(DerivedComponent::Authority { req: false }),
1022                    "example.com".to_string(),
1023                ),
1024                (
1025                    CoveredComponent::HTTP(HTTPField {
1026                        name: "content-length".to_string(),
1027                        parameters: HTTPFieldParametersSet(vec![]),
1028                    }),
1029                    "18".to_string(),
1030                ),
1031            ]),
1032            parameters: IndexMap::from_iter([
1033                (
1034                    sfv::Key::from_string("keyid".into()).unwrap(),
1035                    sfv::BareItem::String(sfv::String::from_string("test".to_string()).unwrap()),
1036                ),
1037                (
1038                    sfv::Key::from_string("created".into()).unwrap(),
1039                    sfv::BareItem::Integer(sfv::Integer::constant(1_618_884_473_i64)),
1040                ),
1041            ])
1042            .into(),
1043        };
1044
1045        let expected_base = "\"@method\": POST\n\"@authority\": example.com\n\"content-length\": 18\n\"@signature-params\": (\"@method\" \"@authority\" \"content-length\");keyid=\"test\";created=1618884473";
1046        let (base, _) = sigbase.into_ascii().unwrap();
1047        assert_eq!(base, expected_base);
1048    }
1049}