Skip to main content

web_bot_auth/
message_signatures.rs

1use super::ImplementationError;
2use crate::components::{self, CoveredComponent, HTTPField};
3use crate::keyring::{Algorithm, KeyRing};
4use indexmap::IndexMap;
5use regex::bytes::Regex;
6use sfv::SerializeValue;
7use std::fmt::Write as _;
8use std::sync::LazyLock;
9use time::{Duration, UtcDateTime};
10static OBSOLETE_LINE_FOLDING: LazyLock<Regex> =
11    LazyLock::new(|| Regex::new(r"\s*\r\n\s+").unwrap());
12
13/// The component parameters associated with the signature in `Signature-Input`
14#[derive(Clone, Debug)]
15pub struct SignatureParams {
16    /// The raw signature parameters associated with this request.
17    pub raw: sfv::Parameters,
18    /// Standard values obtained from the component parameters, such as created, etc.
19    pub details: ParameterDetails,
20}
21
22/// Parsed values from `Signature-Input` header.
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct ParameterDetails {
25    /// The value of the `alg` parameter, if present and resolves to a known algorithm.
26    pub algorithm: Option<Algorithm>,
27    /// The value of the `created` parameter, if present.
28    pub created: Option<i64>,
29    /// The value of the `expires` parameter, if present.
30    pub expires: Option<i64>,
31    /// The value of the `keyid` parameter, if present.
32    pub keyid: Option<String>,
33    /// The value of the `nonce` parameter, if present.
34    pub nonce: Option<String>,
35    /// The value of the `tag` parameter,if present.
36    pub tag: Option<String>,
37}
38
39impl From<sfv::Parameters> for SignatureParams {
40    fn from(value: sfv::Parameters) -> Self {
41        let mut parameter_details = ParameterDetails {
42            algorithm: None,
43            created: None,
44            expires: None,
45            keyid: None,
46            nonce: None,
47            tag: None,
48        };
49
50        for (key, val) in &value {
51            match key.as_str() {
52                "alg" => {
53                    parameter_details.algorithm = val.as_string().and_then(|algorithm_string| {
54                        match algorithm_string.as_str() {
55                            "ed25519" => Some(Algorithm::Ed25519),
56                            "rsa-pss-sha512" => Some(Algorithm::RsaPssSha512),
57                            "rsa-v1_5-sha256" => Some(Algorithm::RsaV1_5Sha256),
58                            "hmac-sha256" => Some(Algorithm::HmacSha256),
59                            "ecdsa-p256-sha256" => Some(Algorithm::EcdsaP256Sha256),
60                            "ecdsa-p384-sha384" => Some(Algorithm::EcdsaP384Sha384),
61                            _ => None,
62                        }
63                    });
64                }
65                "keyid" => {
66                    parameter_details.keyid = val.as_string().map(|s| s.as_str().to_string());
67                }
68                "tag" => parameter_details.tag = val.as_string().map(|s| s.as_str().to_string()),
69                "nonce" => {
70                    parameter_details.nonce = val.as_string().map(|s| s.as_str().to_string());
71                }
72                "created" => {
73                    parameter_details.created = val.as_integer().map(std::convert::Into::into);
74                }
75                "expires" => {
76                    parameter_details.expires = val.as_integer().map(std::convert::Into::into);
77                }
78                _ => {}
79            }
80        }
81
82        Self {
83            raw: value,
84            details: parameter_details,
85        }
86    }
87}
88
89/// Advises whether or not to accept the message as valid prior to
90/// verification, based on a cursory examination of the message parameters.
91pub struct SecurityAdvisory {
92    /// If the `expires` tag was present on the message, whether or not
93    /// the message expired in the past.
94    pub is_expired: Option<bool>,
95    /// If the `nonce` tag was present on the message, whether or not
96    /// the nonce was valid, as judged py a suitable nonce validator.
97    pub nonce_is_invalid: Option<bool>,
98}
99
100impl ParameterDetails {
101    /// Indicates whether or not the message has semantic errors
102    /// that suggest the message should not be verified on account of posing
103    /// a security risk. `nonce_validator` should return `true` if the nonce is
104    /// invalid, and `false` otherwise.
105    pub fn possibly_insecure<F>(&self, nonce_validator: F) -> SecurityAdvisory
106    where
107        F: FnOnce(&String) -> bool,
108    {
109        SecurityAdvisory {
110            is_expired: self.expires.map(|expires| {
111                if let Ok(expiry) = UtcDateTime::from_unix_timestamp(expires) {
112                    let now = UtcDateTime::now();
113                    return now >= expiry;
114                }
115
116                true
117            }),
118            nonce_is_invalid: self.nonce.as_ref().map(nonce_validator),
119        }
120    }
121}
122
123struct SignatureBaseBuilder {
124    components: Vec<CoveredComponent>,
125    parameters: SignatureParams,
126}
127
128impl TryFrom<sfv::InnerList> for SignatureBaseBuilder {
129    type Error = ImplementationError;
130
131    fn try_from(value: sfv::InnerList) -> Result<Self, Self::Error> {
132        Ok(SignatureBaseBuilder {
133            components: value
134                .items
135                .iter()
136                .map(|item| (*item).clone().try_into())
137                .collect::<Result<Vec<CoveredComponent>, ImplementationError>>()?,
138            // Note: it is the responsibility of higher layers to check whether the message is
139            // expired, down here we just parse.
140            parameters: value.params.into(),
141        })
142    }
143}
144
145impl SignatureBaseBuilder {
146    fn into_signature_base(
147        self,
148        message: &impl SignedMessage,
149    ) -> Result<SignatureBase, ImplementationError> {
150        Ok(SignatureBase {
151            components: IndexMap::from_iter(
152                self.components
153                    .into_iter()
154                    .map(|component| match message.lookup_component(&component) {
155                        v if v.len() == 1 => Ok((component, v[0].to_owned())),
156                        v if v.len() > 1 && matches!(component, CoveredComponent::HTTP(_)) => {
157                            let mut register: Vec<String> = vec![];
158
159                            for header_value in v.into_iter() {
160                                register.push(
161                                    // replace leading / trailing whitespace and obsolete line folding,
162                                    // per HTTP message signature spec
163                                    String::from_utf8(
164                                        OBSOLETE_LINE_FOLDING
165                                            .replace_all(header_value.as_bytes().trim_ascii(), b" ")
166                                            .into_owned(),
167                                    )
168                                    .map_err(|_| ImplementationError::NonAsciiContentFound)?,
169                                );
170                            }
171
172                            Ok((component, register.join(", ")))
173                        }
174                        _ => Err(ImplementationError::LookupError(component)),
175                    })
176                    .collect::<Result<Vec<(CoveredComponent, String)>, ImplementationError>>()?,
177            ),
178            parameters: self.parameters,
179        })
180    }
181}
182
183/// A representation of the signature base to be generated during verification and signing.
184#[derive(Clone, Debug)]
185pub struct SignatureBase {
186    /// The components that have been covered and their found values
187    pub components: IndexMap<CoveredComponent, String>,
188    /// The component parameters associated with this message.
189    pub parameters: SignatureParams,
190}
191
192impl SignatureBase {
193    // Convert `SignatureBase` into its ASCII representation as well as the portion of
194    // itself that corresponds to `@signature-params` line.
195    fn into_ascii(self) -> Result<(String, String), ImplementationError> {
196        let mut output = String::new();
197
198        let mut signature_params_line_items: Vec<sfv::Item> = vec![];
199
200        for (component, serialized_value) in self.components {
201            let sfv_item = match component {
202                CoveredComponent::HTTP(http) => sfv::Item::try_from(http)?,
203                CoveredComponent::Derived(derived) => sfv::Item::try_from(derived)?,
204            };
205
206            let _ = writeln!(
207                output,
208                "{}: {}",
209                sfv_item.serialize_value(),
210                serialized_value
211            );
212            signature_params_line_items.push(sfv_item);
213        }
214
215        let signature_params_line = vec![sfv::ListEntry::InnerList(sfv::InnerList::with_params(
216            signature_params_line_items,
217            self.parameters.raw,
218        ))]
219        .serialize_value()
220        .ok_or(ImplementationError::SignatureParamsSerialization)?;
221
222        let _ = write!(output, "\"@signature-params\": {signature_params_line}");
223
224        if output.is_ascii() {
225            Ok((output, signature_params_line))
226        } else {
227            Err(ImplementationError::NonAsciiContentFound)
228        }
229    }
230}
231
232/// Trait that messages seeking verification should implement to facilitate looking up
233/// raw values from the underlying message.
234pub trait SignedMessage {
235    /// Retrieve the raw value(s) of a covered component. Implementations should
236    /// respect any parameter values set on the covered component per the message
237    /// signature spec. Component values that cannot be found must return an empty vector.
238    /// `CoveredComponent::HTTP` fields are guaranteed to have lowercase ASCII names, so
239    /// care should be taken to ensure HTTP field names in the message are checked in a
240    /// case-insensitive way. Only `CoveredComponent::Http` should return a vector with
241    /// more than one element.
242    ///
243    /// This function is also used to look up the values of `Signature-Input`, `Signature`
244    /// and (if used for web bot auth) `Signature-Agent` as standard HTTP headers.
245    /// Implementations should return those headers as well.
246    fn lookup_component(&self, name: &CoveredComponent) -> Vec<String>;
247}
248
249/// Trait that messages seeking signing should implement to generate `Signature-Input`
250/// and `Signature` header contents.
251pub trait UnsignedMessage {
252    /// Obtain a list of covered components to be included. HTTP fields must be lowercased before
253    /// emitting. It is NOT RECOMMENDED to include `signature` and `signature-input` fields here.
254    /// If signing a Web Bot Auth message, and `Signature-Agent` header is intended present, you MUST
255    /// include it as a component here for successful verification.
256    fn fetch_components_to_cover(&self) -> IndexMap<CoveredComponent, String>;
257    /// Store the contents of a generated `Signature-Input` and `Signature` header value.
258    /// It is the responsibility of the application to generate a consistent label for both.
259    /// `signature_header` is guaranteed to be a `sfv` byte sequence element. `signature_input`
260    /// is guaranteed to be `sfv` inner list of strings.
261    fn register_header_contents(&mut self, signature_input: String, signature_header: String);
262}
263
264/// Trait that provides interface to generate signatures given a message and an algorithm.
265/// This is implemented for `Vec<u8>` and friends as a batteries-included way to generate
266/// signatures from raw key material, but can be implemented for any type of client-controlled
267/// signer as well (yubikey, cloud kms, web3 wallet, etc).
268pub trait GenerateSignature {
269    /// Generate signature given the algorithm and the message to sign.
270    fn generate_signature(
271        &self,
272        algorithm: Algorithm,
273        msg: &[u8],
274    ) -> Result<Vec<u8>, ImplementationError>;
275}
276
277impl GenerateSignature for [u8] {
278    fn generate_signature(
279        &self,
280        algorithm: Algorithm,
281        msg: &[u8],
282    ) -> Result<Vec<u8>, ImplementationError> {
283        let signature = match algorithm {
284            Algorithm::Ed25519 => {
285                use ed25519_dalek::{Signer, SigningKey};
286                let signing_key_dalek = SigningKey::try_from(self)
287                    .map_err(|_| ImplementationError::InvalidKeyLength)?;
288
289                signing_key_dalek.sign(msg).to_vec()
290            }
291            other => return Err(ImplementationError::UnsupportedAlgorithm(other)),
292        };
293
294        Ok(signature)
295    }
296}
297
298impl GenerateSignature for Vec<u8> {
299    fn generate_signature(
300        &self,
301        algorithm: Algorithm,
302        msg: &[u8],
303    ) -> Result<Vec<u8>, ImplementationError> {
304        self.as_slice().generate_signature(algorithm, msg)
305    }
306}
307
308impl GenerateSignature for [u8; 32] {
309    fn generate_signature(
310        &self,
311        algorithm: Algorithm,
312        msg: &[u8],
313    ) -> Result<Vec<u8>, ImplementationError> {
314        self.as_slice().generate_signature(algorithm, msg)
315    }
316}
317
318/// A struct that implements signing. The struct fields here are serialized into the `Signature-Input`
319/// header.
320pub struct MessageSigner {
321    /// Name to use for `keyid` parameter
322    pub keyid: String,
323    /// A random nonce to be provided for additional security
324    pub nonce: String,
325    /// Value to be used for `tag` parameter
326    pub tag: String,
327}
328
329impl MessageSigner {
330    /// Sign the provided method with `signer`, setting an expiration value of
331    /// length `expires` from now (the time of signing).
332    ///
333    /// # Errors
334    ///
335    /// Returns `ImplementationErrors` relevant to signing and parsing.
336    /// Returns an error if the algorithm chosen is not supported by this library.
337    pub fn generate_signature_headers_content(
338        &self,
339        message: &mut impl UnsignedMessage,
340        expires: Duration,
341        algorithm: Algorithm,
342        signer: &(impl GenerateSignature + ?Sized),
343    ) -> Result<(), ImplementationError> {
344        let components_to_cover = message.fetch_components_to_cover();
345        let mut sfv_parameters = sfv::Parameters::new();
346
347        sfv_parameters.insert(
348            sfv::KeyRef::constant("keyid").to_owned(),
349            sfv::BareItem::String(
350                sfv::StringRef::from_str(&self.keyid)
351                    .map_err(|_| {
352                        ImplementationError::ParsingError(
353                            "keyid contains non-printable ASCII characters".into(),
354                        )
355                    })?
356                    .to_owned(),
357            ),
358        );
359
360        sfv_parameters.insert(
361            sfv::KeyRef::constant("nonce").to_owned(),
362            sfv::BareItem::String(
363                sfv::StringRef::from_str(&self.nonce)
364                    .map_err(|_| {
365                        ImplementationError::ParsingError(
366                            "nonce contains non-printable ASCII characters".into(),
367                        )
368                    })?
369                    .to_owned(),
370            ),
371        );
372
373        sfv_parameters.insert(
374            sfv::KeyRef::constant("tag").to_owned(),
375            sfv::BareItem::String(
376                sfv::StringRef::from_str(&self.tag)
377                    .map_err(|_| {
378                        ImplementationError::ParsingError(
379                            "tag contains non-printable ASCII characters".into(),
380                        )
381                    })?
382                    .to_owned(),
383            ),
384        );
385
386        sfv_parameters.insert(
387            sfv::KeyRef::constant("alg").to_owned(),
388            sfv::BareItem::String(
389                sfv::StringRef::from_str(&format!("{}", algorithm))
390                    .map_err(|_| {
391                        ImplementationError::ParsingError(
392                            "tag contains non-printable ASCII characters".into(),
393                        )
394                    })?
395                    .to_owned(),
396            ),
397        );
398
399        let created = UtcDateTime::now();
400        let expiry = created + expires;
401
402        sfv_parameters.insert(
403            sfv::KeyRef::constant("created").to_owned(),
404            sfv::BareItem::Integer(sfv::Integer::constant(created.unix_timestamp())),
405        );
406
407        sfv_parameters.insert(
408            sfv::KeyRef::constant("expires").to_owned(),
409            sfv::BareItem::Integer(sfv::Integer::constant(expiry.unix_timestamp())),
410        );
411
412        let (signature_base, signature_params_content) = SignatureBase {
413            components: components_to_cover,
414            parameters: sfv_parameters.into(),
415        }
416        .into_ascii()?;
417
418        let signature = sfv::Item {
419            bare_item: sfv::BareItem::ByteSequence(
420                signer.generate_signature(algorithm, signature_base.as_bytes())?,
421            ),
422            params: sfv::Parameters::new(),
423        }
424        .serialize_value();
425
426        message.register_header_contents(signature_params_content, signature);
427
428        Ok(())
429    }
430}
431
432/// A parsed representation of the signature and the components chosen to cover that
433/// signature, once `MessageVerifier` has parsed the message. This allows inspection
434/// of the chosen labl and its components.
435#[derive(Clone, Debug)]
436pub struct ParsedLabel {
437    /// The label that was chosen.
438    pub label: sfv::Key,
439    /// The signature obtained from the message that verifiers will verify
440    pub signature: Vec<u8>,
441    /// The signature base obtained from the message, containining both the chosen
442    /// components to cover as well as any interesting parameters of the same.
443    pub base: SignatureBase,
444}
445
446/// A `MessageVerifier` performs the verifications needed for a signed message.
447#[derive(Clone, Debug)]
448pub struct MessageVerifier {
449    /// Parsed version of the signature label chosen for this message.
450    pub parsed: ParsedLabel,
451}
452
453/// Micro-measurements of different parts of the process in a call to `verify()`.
454/// Useful for measuring overhead.
455#[derive(Clone, Debug)]
456pub struct SignatureTiming {
457    /// Time taken to generate a signature base,
458    pub generation: Duration,
459    /// Time taken to execute cryptographic verification.
460    pub verification: Duration,
461}
462
463impl MessageVerifier {
464    /// Parse a message into a structure that is ready for verification against an
465    /// external key with a suitable algorithm. `pick` is a predicate
466    /// enabling you to choose which message label should be considered as the message to
467    /// verify - if it is known only one signature is in the message, simply return true.
468    ///
469    /// # Errors
470    ///
471    /// Returns `ImplementationErrors` relevant to verifying and parsing.
472    pub fn parse<P>(message: &impl SignedMessage, pick: P) -> Result<Self, ImplementationError>
473    where
474        P: Fn(&(sfv::Key, sfv::InnerList)) -> bool,
475    {
476        let signature_input = message
477            .lookup_component(&CoveredComponent::HTTP(HTTPField {
478                name: "signature-input".to_string(),
479                parameters: components::HTTPFieldParametersSet(vec![]),
480            }))
481            .into_iter()
482            .filter_map(|sig_input| sfv::Parser::new(&sig_input).parse_dictionary().ok())
483            .reduce(|mut acc, sig_input| {
484                acc.extend(sig_input);
485                acc
486            })
487            .ok_or(ImplementationError::ParsingError(
488                "No validly-formatted `Signature-Input` headers found".to_string(),
489            ))?;
490
491        let mut signature_header = message
492            .lookup_component(&CoveredComponent::HTTP(HTTPField {
493                name: "signature".to_string(),
494                parameters: components::HTTPFieldParametersSet(vec![]),
495            }))
496            .into_iter()
497            .filter_map(|sig_input| sfv::Parser::new(&sig_input).parse_dictionary().ok())
498            .reduce(|mut acc, sig_input| {
499                acc.extend(sig_input);
500                acc
501            })
502            .ok_or(ImplementationError::ParsingError(
503                "No validly-formatted `Signature` headers found".to_string(),
504            ))?;
505
506        let (label, innerlist) = signature_input
507            .into_iter()
508            .filter_map(|(label, listentry)| match listentry {
509                sfv::ListEntry::InnerList(inner_list) => Some((label, inner_list)),
510                sfv::ListEntry::Item(_) => None,
511            })
512            .find(pick)
513            .ok_or(ImplementationError::ParsingError(
514                "No matching label and signature base found".into(),
515            ))?;
516
517        let signature = match signature_header.shift_remove(&label).ok_or(
518            ImplementationError::ParsingError("No matching signature found from label".into()),
519        )? {
520            sfv::ListEntry::Item(sfv::Item {
521                bare_item,
522                params: _,
523            }) => match bare_item {
524                sfv::GenericBareItem::ByteSequence(sequence) => sequence,
525                other_type => {
526                    return Err(ImplementationError::ParsingError(format!(
527                        "Invalid type for signature found, expected byte sequence: {other_type:?}"
528                    )));
529                }
530            },
531            other_type @ sfv::ListEntry::InnerList(_) => {
532                return Err(ImplementationError::ParsingError(format!(
533                    "Invalid type for signature found, expected byte sequence: {other_type:?}"
534                )));
535            }
536        };
537
538        let builder = SignatureBaseBuilder::try_from(innerlist)?;
539        let base = builder.into_signature_base(message)?;
540
541        Ok(MessageVerifier {
542            parsed: ParsedLabel {
543                label,
544                signature,
545                base,
546            },
547        })
548    }
549
550    /// Verify the messsage, consuming the verifier in the process.
551    /// If `key_id` is not supplied, a key ID to fetch the public key
552    /// from `keyring` will be sourced from the `keyid` parameter
553    /// within the message. Returns information about how long verification
554    /// took if successful.
555    ///
556    /// # Errors
557    ///
558    /// Returns `ImplementationErrors` relevant to verifying and parsing.
559    pub fn verify(
560        self,
561        keyring: &KeyRing,
562        key_id: Option<String>,
563    ) -> Result<SignatureTiming, ImplementationError> {
564        let keying_material = (match key_id {
565            Some(key) => keyring.get(&key),
566            None => self
567                .parsed
568                .base
569                .parameters
570                .details
571                .keyid
572                .as_ref()
573                .and_then(|key| keyring.get(key)),
574        })
575        .ok_or(ImplementationError::NoSuchKey)?;
576        let generation = UtcDateTime::now();
577        let (base_representation, _) = self.parsed.base.into_ascii()?;
578        let generation = UtcDateTime::now() - generation;
579        match &keying_material.0 {
580            Algorithm::Ed25519 => {
581                use ed25519_dalek::{Signature, Verifier, VerifyingKey};
582                let verifying_key = VerifyingKey::try_from(keying_material.1.as_slice())
583                    .map_err(|_| ImplementationError::InvalidKeyLength)?;
584
585                let sig = Signature::try_from(self.parsed.signature.as_slice())
586                    .map_err(|_| ImplementationError::InvalidSignatureLength)?;
587
588                let verification = UtcDateTime::now();
589                verifying_key
590                    .verify(base_representation.as_bytes(), &sig)
591                    .map_err(ImplementationError::FailedToVerify)
592                    .map(|()| SignatureTiming {
593                        generation,
594                        verification: UtcDateTime::now() - verification,
595                    })
596            }
597            other => Err(ImplementationError::UnsupportedAlgorithm(other.clone())),
598        }
599    }
600}
601
602#[cfg(test)]
603mod tests {
604
605    use crate::components::{DerivedComponent, HTTPField, HTTPFieldParametersSet};
606    use indexmap::IndexMap;
607
608    use super::*;
609
610    struct StandardTestVector {}
611
612    impl SignedMessage for StandardTestVector {
613        fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
614            match name {
615                CoveredComponent::HTTP(HTTPField { name, .. }) => {
616                    if name == "signature" {
617                        return vec!["sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned()];
618                    }
619
620                    if name == "signature-input" {
621                        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()];
622                    }
623                    vec![]
624                }
625                CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
626                    vec!["example.com".to_string()]
627                }
628                _ => vec![],
629            }
630        }
631    }
632
633    #[test]
634    fn test_parsing_as_http_signature() {
635        let test = StandardTestVector {};
636        let verifier = MessageVerifier::parse(&test, |(_, _)| true).unwrap();
637        let expected_signature_params = "(\"@authority\");created=1735689600;keyid=\"poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U\";alg=\"ed25519\";expires=1735693200;nonce=\"gubxywVx7hzbYKatLgzuKDllDAIXAkz41PydU7aOY7vT+Mb3GJNxW0qD4zJ+IOQ1NVtg+BNbTCRUMt1Ojr5BgA==\";tag=\"web-bot-auth\"";
638        let expected_base = format!(
639            "\"@authority\": example.com\n\"@signature-params\": {expected_signature_params}"
640        );
641        let (base, signature_params) = verifier.parsed.base.into_ascii().unwrap();
642        assert_eq!(base, expected_base.as_str());
643        assert_eq!(signature_params, expected_signature_params);
644    }
645
646    #[test]
647    fn test_verifying_as_http_signature() {
648        let test = StandardTestVector {};
649        let public_key: [u8; ed25519_dalek::PUBLIC_KEY_LENGTH] = [
650            0x26, 0xb4, 0x0b, 0x8f, 0x93, 0xff, 0xf3, 0xd8, 0x97, 0x11, 0x2f, 0x7e, 0xbc, 0x58,
651            0x2b, 0x23, 0x2d, 0xbd, 0x72, 0x51, 0x7d, 0x08, 0x2f, 0xe8, 0x3c, 0xfb, 0x30, 0xdd,
652            0xce, 0x43, 0xd1, 0xbb,
653        ];
654        let mut keyring = KeyRing::default();
655        keyring.import_raw(
656            "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U".to_string(),
657            Algorithm::Ed25519,
658            public_key.to_vec(),
659        );
660        let verifier = MessageVerifier::parse(&test, |(_, _)| true).unwrap();
661        let timing = verifier.verify(&keyring, None).unwrap();
662        assert!(timing.generation.whole_nanoseconds() > 0);
663        assert!(timing.verification.whole_nanoseconds() > 0);
664    }
665
666    #[test]
667    fn test_signing() {
668        struct SigningTest {}
669        impl UnsignedMessage for SigningTest {
670            fn fetch_components_to_cover(&self) -> IndexMap<CoveredComponent, String> {
671                IndexMap::from_iter([
672                    (
673                        CoveredComponent::Derived(DerivedComponent::Method { req: false }),
674                        "POST".to_string(),
675                    ),
676                    (
677                        CoveredComponent::Derived(DerivedComponent::Authority { req: false }),
678                        "example.com".to_string(),
679                    ),
680                    (
681                        CoveredComponent::HTTP(HTTPField {
682                            name: "content-length".to_string(),
683                            parameters: HTTPFieldParametersSet(vec![]),
684                        }),
685                        "18".to_string(),
686                    ),
687                ])
688            }
689
690            fn register_header_contents(
691                &mut self,
692                _signature_input: String,
693                _signature_header: String,
694            ) {
695            }
696        }
697
698        let signer = MessageSigner {
699            keyid: "test".into(),
700            nonce: "another-test".into(),
701            tag: "web-bot-auth".into(),
702        };
703
704        let private_key: [u8; ed25519_dalek::SECRET_KEY_LENGTH] = [
705            0x9f, 0x83, 0x62, 0xf8, 0x7a, 0x48, 0x4a, 0x95, 0x4e, 0x6e, 0x74, 0x0c, 0x5b, 0x4c,
706            0x0e, 0x84, 0x22, 0x91, 0x39, 0xa2, 0x0a, 0xa8, 0xab, 0x56, 0xff, 0x66, 0x58, 0x6f,
707            0x6a, 0x7d, 0x29, 0xc5,
708        ];
709
710        let mut test = SigningTest {};
711
712        assert!(
713            signer
714                .generate_signature_headers_content(
715                    &mut test,
716                    Duration::seconds(10),
717                    Algorithm::Ed25519,
718                    &private_key
719                )
720                .is_ok()
721        );
722    }
723
724    #[test]
725    fn signature_base_generates_the_expected_representation() {
726        let sigbase = SignatureBase {
727            components: IndexMap::from_iter([
728                (
729                    CoveredComponent::Derived(DerivedComponent::Method { req: false }),
730                    "POST".to_string(),
731                ),
732                (
733                    CoveredComponent::Derived(DerivedComponent::Authority { req: false }),
734                    "example.com".to_string(),
735                ),
736                (
737                    CoveredComponent::HTTP(HTTPField {
738                        name: "content-length".to_string(),
739                        parameters: HTTPFieldParametersSet(vec![]),
740                    }),
741                    "18".to_string(),
742                ),
743            ]),
744            parameters: IndexMap::from_iter([
745                (
746                    sfv::Key::from_string("keyid".into()).unwrap(),
747                    sfv::BareItem::String(sfv::String::from_string("test".to_string()).unwrap()),
748                ),
749                (
750                    sfv::Key::from_string("created".into()).unwrap(),
751                    sfv::BareItem::Integer(sfv::Integer::constant(1_618_884_473_i64)),
752                ),
753            ])
754            .into(),
755        };
756
757        let expected_base = "\"@method\": POST\n\"@authority\": example.com\n\"content-length\": 18\n\"@signature-params\": (\"@method\" \"@authority\" \"content-length\");keyid=\"test\";created=1618884473";
758        let (base, _) = sigbase.into_ascii().unwrap();
759        assert_eq!(base, expected_base);
760    }
761}