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