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/// A struct that implements signing. The struct fields here are serialized into the `Signature-Input`
265/// header.
266pub struct MessageSigner {
267    /// Name to use for `keyid` parameter
268    pub keyid: String,
269    /// A random nonce to be provided for additional security
270    pub nonce: String,
271    /// Value to be used for `tag` parameter
272    pub tag: String,
273}
274
275impl MessageSigner {
276    /// Sign the provided method with `signing_key`, setting an expiration value of
277    /// length `expires` from now (the time of signing).
278    ///
279    /// # Errors
280    ///
281    /// Returns `ImplementationErrors` relevant to signing and parsing.
282    /// Returns an error if the algorithm chosen is not supported by this library.
283    pub fn generate_signature_headers_content(
284        &self,
285        message: &mut impl UnsignedMessage,
286        expires: Duration,
287        algorithm: Algorithm,
288        signing_key: &Vec<u8>,
289    ) -> Result<(), ImplementationError> {
290        let components_to_cover = message.fetch_components_to_cover();
291        let mut sfv_parameters = sfv::Parameters::new();
292
293        sfv_parameters.insert(
294            sfv::KeyRef::constant("keyid").to_owned(),
295            sfv::BareItem::String(
296                sfv::StringRef::from_str(&self.keyid)
297                    .map_err(|_| {
298                        ImplementationError::ParsingError(
299                            "keyid contains non-printable ASCII characters".into(),
300                        )
301                    })?
302                    .to_owned(),
303            ),
304        );
305
306        sfv_parameters.insert(
307            sfv::KeyRef::constant("nonce").to_owned(),
308            sfv::BareItem::String(
309                sfv::StringRef::from_str(&self.nonce)
310                    .map_err(|_| {
311                        ImplementationError::ParsingError(
312                            "nonce contains non-printable ASCII characters".into(),
313                        )
314                    })?
315                    .to_owned(),
316            ),
317        );
318
319        sfv_parameters.insert(
320            sfv::KeyRef::constant("tag").to_owned(),
321            sfv::BareItem::String(
322                sfv::StringRef::from_str(&self.tag)
323                    .map_err(|_| {
324                        ImplementationError::ParsingError(
325                            "tag contains non-printable ASCII characters".into(),
326                        )
327                    })?
328                    .to_owned(),
329            ),
330        );
331
332        sfv_parameters.insert(
333            sfv::KeyRef::constant("alg").to_owned(),
334            sfv::BareItem::String(
335                sfv::StringRef::from_str(&format!("{}", algorithm))
336                    .map_err(|_| {
337                        ImplementationError::ParsingError(
338                            "tag contains non-printable ASCII characters".into(),
339                        )
340                    })?
341                    .to_owned(),
342            ),
343        );
344
345        let created = UtcDateTime::now();
346        let expiry = created + expires;
347
348        sfv_parameters.insert(
349            sfv::KeyRef::constant("created").to_owned(),
350            sfv::BareItem::Integer(sfv::Integer::constant(created.unix_timestamp())),
351        );
352
353        sfv_parameters.insert(
354            sfv::KeyRef::constant("expires").to_owned(),
355            sfv::BareItem::Integer(sfv::Integer::constant(expiry.unix_timestamp())),
356        );
357
358        let (signature_base, signature_params_content) = SignatureBase {
359            components: components_to_cover,
360            parameters: sfv_parameters.into(),
361        }
362        .into_ascii()?;
363
364        let signature = match algorithm {
365            Algorithm::Ed25519 => {
366                use ed25519_dalek::{Signer, SigningKey};
367                let signing_key_dalek = SigningKey::try_from(signing_key.as_slice())
368                    .map_err(|_| ImplementationError::InvalidKeyLength)?;
369
370                sfv::Item {
371                    bare_item: sfv::BareItem::ByteSequence(
372                        signing_key_dalek.sign(signature_base.as_bytes()).to_vec(),
373                    ),
374                    params: sfv::Parameters::new(),
375                }
376                .serialize_value()
377            }
378            other => return Err(ImplementationError::UnsupportedAlgorithm(other)),
379        };
380
381        message.register_header_contents(signature_params_content, signature);
382
383        Ok(())
384    }
385}
386
387/// A parsed representation of the signature and the components chosen to cover that
388/// signature, once `MessageVerifier` has parsed the message. This allows inspection
389/// of the chosen labl and its components.
390#[derive(Clone, Debug)]
391pub struct ParsedLabel {
392    /// The label that was chosen.
393    pub label: sfv::Key,
394    /// The signature obtained from the message that verifiers will verify
395    pub signature: Vec<u8>,
396    /// The signature base obtained from the message, containining both the chosen
397    /// components to cover as well as any interesting parameters of the same.
398    pub base: SignatureBase,
399}
400
401/// A `MessageVerifier` performs the verifications needed for a signed message.
402#[derive(Clone, Debug)]
403pub struct MessageVerifier {
404    /// Parsed version of the signature label chosen for this message.
405    pub parsed: ParsedLabel,
406}
407
408/// Micro-measurements of different parts of the process in a call to `verify()`.
409/// Useful for measuring overhead.
410#[derive(Clone, Debug)]
411pub struct SignatureTiming {
412    /// Time taken to generate a signature base,
413    pub generation: Duration,
414    /// Time taken to execute cryptographic verification.
415    pub verification: Duration,
416}
417
418impl MessageVerifier {
419    /// Parse a message into a structure that is ready for verification against an
420    /// external key with a suitable algorithm. `pick` is a predicate
421    /// enabling you to choose which message label should be considered as the message to
422    /// verify - if it is known only one signature is in the message, simply return true.
423    ///
424    /// # Errors
425    ///
426    /// Returns `ImplementationErrors` relevant to verifying and parsing.
427    pub fn parse<P>(message: &impl SignedMessage, pick: P) -> Result<Self, ImplementationError>
428    where
429        P: Fn(&(sfv::Key, sfv::InnerList)) -> bool,
430    {
431        let signature_input = message
432            .lookup_component(&CoveredComponent::HTTP(HTTPField {
433                name: "signature-input".to_string(),
434                parameters: components::HTTPFieldParametersSet(vec![]),
435            }))
436            .into_iter()
437            .filter_map(|sig_input| sfv::Parser::new(&sig_input).parse_dictionary().ok())
438            .reduce(|mut acc, sig_input| {
439                acc.extend(sig_input);
440                acc
441            })
442            .ok_or(ImplementationError::ParsingError(
443                "No validly-formatted `Signature-Input` headers found".to_string(),
444            ))?;
445
446        let mut signature_header = message
447            .lookup_component(&CoveredComponent::HTTP(HTTPField {
448                name: "signature".to_string(),
449                parameters: components::HTTPFieldParametersSet(vec![]),
450            }))
451            .into_iter()
452            .filter_map(|sig_input| sfv::Parser::new(&sig_input).parse_dictionary().ok())
453            .reduce(|mut acc, sig_input| {
454                acc.extend(sig_input);
455                acc
456            })
457            .ok_or(ImplementationError::ParsingError(
458                "No validly-formatted `Signature` headers found".to_string(),
459            ))?;
460
461        let (label, innerlist) = signature_input
462            .into_iter()
463            .filter_map(|(label, listentry)| match listentry {
464                sfv::ListEntry::InnerList(inner_list) => Some((label, inner_list)),
465                sfv::ListEntry::Item(_) => None,
466            })
467            .find(pick)
468            .ok_or(ImplementationError::ParsingError(
469                "No matching label and signature base found".into(),
470            ))?;
471
472        let signature = match signature_header.shift_remove(&label).ok_or(
473            ImplementationError::ParsingError("No matching signature found from label".into()),
474        )? {
475            sfv::ListEntry::Item(sfv::Item {
476                bare_item,
477                params: _,
478            }) => match bare_item {
479                sfv::GenericBareItem::ByteSequence(sequence) => sequence,
480                other_type => {
481                    return Err(ImplementationError::ParsingError(format!(
482                        "Invalid type for signature found, expected byte sequence: {other_type:?}"
483                    )));
484                }
485            },
486            other_type @ sfv::ListEntry::InnerList(_) => {
487                return Err(ImplementationError::ParsingError(format!(
488                    "Invalid type for signature found, expected byte sequence: {other_type:?}"
489                )));
490            }
491        };
492
493        let builder = SignatureBaseBuilder::try_from(innerlist)?;
494        let base = builder.into_signature_base(message)?;
495
496        Ok(MessageVerifier {
497            parsed: ParsedLabel {
498                label,
499                signature,
500                base,
501            },
502        })
503    }
504
505    /// Verify the messsage, consuming the verifier in the process.
506    /// If `key_id` is not supplied, a key ID to fetch the public key
507    /// from `keyring` will be sourced from the `keyid` parameter
508    /// within the message. Returns information about how long verification
509    /// took if successful.
510    ///
511    /// # Errors
512    ///
513    /// Returns `ImplementationErrors` relevant to verifying and parsing.
514    pub fn verify(
515        self,
516        keyring: &KeyRing,
517        key_id: Option<String>,
518    ) -> Result<SignatureTiming, ImplementationError> {
519        let keying_material = (match key_id {
520            Some(key) => keyring.get(&key),
521            None => self
522                .parsed
523                .base
524                .parameters
525                .details
526                .keyid
527                .as_ref()
528                .and_then(|key| keyring.get(key)),
529        })
530        .ok_or(ImplementationError::NoSuchKey)?;
531        let generation = UtcDateTime::now();
532        let (base_representation, _) = self.parsed.base.into_ascii()?;
533        let generation = UtcDateTime::now() - generation;
534        match &keying_material.0 {
535            Algorithm::Ed25519 => {
536                use ed25519_dalek::{Signature, Verifier, VerifyingKey};
537                let verifying_key = VerifyingKey::try_from(keying_material.1.as_slice())
538                    .map_err(|_| ImplementationError::InvalidKeyLength)?;
539
540                let sig = Signature::try_from(self.parsed.signature.as_slice())
541                    .map_err(|_| ImplementationError::InvalidSignatureLength)?;
542
543                let verification = UtcDateTime::now();
544                verifying_key
545                    .verify(base_representation.as_bytes(), &sig)
546                    .map_err(ImplementationError::FailedToVerify)
547                    .map(|()| SignatureTiming {
548                        generation,
549                        verification: UtcDateTime::now() - verification,
550                    })
551            }
552            other => Err(ImplementationError::UnsupportedAlgorithm(other.clone())),
553        }
554    }
555}
556
557#[cfg(test)]
558mod tests {
559
560    use crate::components::{DerivedComponent, HTTPField, HTTPFieldParametersSet};
561    use indexmap::IndexMap;
562
563    use super::*;
564
565    struct StandardTestVector {}
566
567    impl SignedMessage for StandardTestVector {
568        fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
569            match name {
570                CoveredComponent::HTTP(HTTPField { name, .. }) => {
571                    if name == "signature" {
572                        return vec!["sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned()];
573                    }
574
575                    if name == "signature-input" {
576                        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()];
577                    }
578                    vec![]
579                }
580                CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
581                    vec!["example.com".to_string()]
582                }
583                _ => vec![],
584            }
585        }
586    }
587
588    #[test]
589    fn test_parsing_as_http_signature() {
590        let test = StandardTestVector {};
591        let verifier = MessageVerifier::parse(&test, |(_, _)| true).unwrap();
592        let expected_signature_params = "(\"@authority\");created=1735689600;keyid=\"poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U\";alg=\"ed25519\";expires=1735693200;nonce=\"gubxywVx7hzbYKatLgzuKDllDAIXAkz41PydU7aOY7vT+Mb3GJNxW0qD4zJ+IOQ1NVtg+BNbTCRUMt1Ojr5BgA==\";tag=\"web-bot-auth\"";
593        let expected_base = format!(
594            "\"@authority\": example.com\n\"@signature-params\": {expected_signature_params}"
595        );
596        let (base, signature_params) = verifier.parsed.base.into_ascii().unwrap();
597        assert_eq!(base, expected_base.as_str());
598        assert_eq!(signature_params, expected_signature_params);
599    }
600
601    #[test]
602    fn test_verifying_as_http_signature() {
603        let test = StandardTestVector {};
604        let public_key: [u8; ed25519_dalek::PUBLIC_KEY_LENGTH] = [
605            0x26, 0xb4, 0x0b, 0x8f, 0x93, 0xff, 0xf3, 0xd8, 0x97, 0x11, 0x2f, 0x7e, 0xbc, 0x58,
606            0x2b, 0x23, 0x2d, 0xbd, 0x72, 0x51, 0x7d, 0x08, 0x2f, 0xe8, 0x3c, 0xfb, 0x30, 0xdd,
607            0xce, 0x43, 0xd1, 0xbb,
608        ];
609        let mut keyring = KeyRing::default();
610        keyring.import_raw(
611            "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U".to_string(),
612            Algorithm::Ed25519,
613            public_key.to_vec(),
614        );
615        let verifier = MessageVerifier::parse(&test, |(_, _)| true).unwrap();
616        let timing = verifier.verify(&keyring, None).unwrap();
617        assert!(timing.generation.whole_nanoseconds() > 0);
618        assert!(timing.verification.whole_nanoseconds() > 0);
619    }
620
621    #[test]
622    fn test_signing() {
623        struct SigningTest {}
624        impl UnsignedMessage for SigningTest {
625            fn fetch_components_to_cover(&self) -> IndexMap<CoveredComponent, String> {
626                IndexMap::from_iter([
627                    (
628                        CoveredComponent::Derived(DerivedComponent::Method { req: false }),
629                        "POST".to_string(),
630                    ),
631                    (
632                        CoveredComponent::Derived(DerivedComponent::Authority { req: false }),
633                        "example.com".to_string(),
634                    ),
635                    (
636                        CoveredComponent::HTTP(HTTPField {
637                            name: "content-length".to_string(),
638                            parameters: HTTPFieldParametersSet(vec![]),
639                        }),
640                        "18".to_string(),
641                    ),
642                ])
643            }
644
645            fn register_header_contents(
646                &mut self,
647                _signature_input: String,
648                _signature_header: String,
649            ) {
650            }
651        }
652
653        let signer = MessageSigner {
654            keyid: "test".into(),
655            nonce: "another-test".into(),
656            tag: "web-bot-auth".into(),
657        };
658
659        let private_key: [u8; ed25519_dalek::SECRET_KEY_LENGTH] = [
660            0x9f, 0x83, 0x62, 0xf8, 0x7a, 0x48, 0x4a, 0x95, 0x4e, 0x6e, 0x74, 0x0c, 0x5b, 0x4c,
661            0x0e, 0x84, 0x22, 0x91, 0x39, 0xa2, 0x0a, 0xa8, 0xab, 0x56, 0xff, 0x66, 0x58, 0x6f,
662            0x6a, 0x7d, 0x29, 0xc5,
663        ];
664
665        let mut test = SigningTest {};
666
667        assert!(
668            signer
669                .generate_signature_headers_content(
670                    &mut test,
671                    Duration::seconds(10),
672                    Algorithm::Ed25519,
673                    &private_key.to_vec()
674                )
675                .is_ok()
676        );
677    }
678
679    #[test]
680    fn signature_base_generates_the_expected_representation() {
681        let sigbase = SignatureBase {
682            components: IndexMap::from_iter([
683                (
684                    CoveredComponent::Derived(DerivedComponent::Method { req: false }),
685                    "POST".to_string(),
686                ),
687                (
688                    CoveredComponent::Derived(DerivedComponent::Authority { req: false }),
689                    "example.com".to_string(),
690                ),
691                (
692                    CoveredComponent::HTTP(HTTPField {
693                        name: "content-length".to_string(),
694                        parameters: HTTPFieldParametersSet(vec![]),
695                    }),
696                    "18".to_string(),
697                ),
698            ]),
699            parameters: IndexMap::from_iter([
700                (
701                    sfv::Key::from_string("keyid".into()).unwrap(),
702                    sfv::BareItem::String(sfv::String::from_string("test".to_string()).unwrap()),
703                ),
704                (
705                    sfv::Key::from_string("created".into()).unwrap(),
706                    sfv::BareItem::Integer(sfv::Integer::constant(1_618_884_473_i64)),
707                ),
708            ])
709            .into(),
710        };
711
712        let expected_base = "\"@method\": POST\n\"@authority\": example.com\n\"content-length\": 18\n\"@signature-params\": (\"@method\" \"@authority\" \"content-length\");keyid=\"test\";created=1618884473";
713        let (base, _) = sigbase.into_ascii().unwrap();
714        assert_eq!(base, expected_base);
715    }
716}