web_bot_auth/
message_signatures.rs

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