1#![deny(missing_docs)]
2pub mod components;
20
21use components::CoveredComponent;
22use indexmap::IndexMap;
23use sfv::SerializeValue;
24use std::collections::HashMap;
25use std::fmt;
26use std::fmt::Write as _;
27use std::time::{Duration, Instant, SystemTime, SystemTimeError, UNIX_EPOCH};
28
29#[derive(Debug)]
31pub enum ImplementationError {
32 ImpossibleSfvError(sfv::Error),
37 ParsingError(String),
41 LookupError(CoveredComponent),
45 UnsupportedAlgorithm,
50 NoSuchKey,
53 InvalidKeyLength,
56 InvalidSignatureLength,
59 FailedToVerify,
62 NonAsciiContentFound,
68 SignatureParamsSerialization,
73 TimeError(SystemTimeError),
77 WebBotAuth(WebBotAuthError),
79}
80
81#[derive(Debug)]
83pub enum WebBotAuthError {
84 SignatureIsExpired,
87 NotImplemented,
91}
92
93#[derive(Clone, Debug)]
94struct SignatureParams {
95 raw: sfv::Parameters,
96 details: ParameterDetails,
97}
98
99#[derive(Clone, Debug)]
101pub struct ParameterDetails {
102 pub algorithm: Option<Algorithm>,
104 pub created: Option<i64>,
106 pub expires: Option<i64>,
108 pub keyid: Option<String>,
110 pub nonce: Option<String>,
112 pub tag: Option<String>,
114}
115
116impl From<sfv::Parameters> for SignatureParams {
117 fn from(value: sfv::Parameters) -> Self {
118 let mut parameter_details = ParameterDetails {
119 algorithm: None,
120 created: None,
121 expires: None,
122 keyid: None,
123 nonce: None,
124 tag: None,
125 };
126
127 for (key, val) in &value {
128 match key.as_str() {
129 "alg" => {
130 parameter_details.algorithm = val.as_string().and_then(|algorithm_string| {
131 match algorithm_string.as_str() {
132 "ed25519" => Some(Algorithm::Ed25519),
133 _ => None,
134 }
135 });
136 }
137 "keyid" => {
138 parameter_details.keyid = val.as_string().map(|s| s.as_str().to_string());
139 }
140 "tag" => parameter_details.tag = val.as_string().map(|s| s.as_str().to_string()),
141 "nonce" => {
142 parameter_details.nonce = val.as_string().map(|s| s.as_str().to_string());
143 }
144 "created" => {
145 parameter_details.created = val.as_integer().map(std::convert::Into::into);
146 }
147 "expires" => {
148 parameter_details.expires = val.as_integer().map(std::convert::Into::into);
149 }
150 _ => {}
151 }
152 }
153
154 Self {
155 raw: value,
156 details: parameter_details,
157 }
158 }
159}
160
161struct SignatureBaseBuilder {
162 components: Vec<CoveredComponent>,
163 parameters: SignatureParams,
164}
165
166impl TryFrom<sfv::InnerList> for SignatureBaseBuilder {
167 type Error = ImplementationError;
168
169 fn try_from(value: sfv::InnerList) -> Result<Self, Self::Error> {
170 Ok(SignatureBaseBuilder {
171 components: value
172 .items
173 .iter()
174 .map(|item| (*item).clone().try_into())
175 .collect::<Result<Vec<CoveredComponent>, ImplementationError>>()?,
176 parameters: value.params.into(),
179 })
180 }
181}
182
183impl SignatureBaseBuilder {
184 fn into_signature_base(
185 self,
186 message: &impl SignedMessage,
187 ) -> Result<SignatureBase, ImplementationError> {
188 Ok(SignatureBase {
189 components: IndexMap::from_iter(
190 self.components
191 .into_iter()
192 .map(|component| match message.lookup_component(&component) {
193 Some(serialized_value) => Ok((component, serialized_value)),
194 None => Err(ImplementationError::LookupError(component)),
195 })
196 .collect::<Result<Vec<(CoveredComponent, String)>, ImplementationError>>()?,
197 ),
198 parameters: self.parameters,
199 })
200 }
201}
202
203#[derive(Clone, Debug)]
205struct SignatureBase {
206 components: IndexMap<CoveredComponent, String>,
207 parameters: SignatureParams,
208}
209
210impl SignatureBase {
211 fn into_ascii(self) -> Result<(String, String), ImplementationError> {
214 let mut output = String::new();
215
216 let mut signature_params_line_items: Vec<sfv::Item> = vec![];
217
218 for (component, serialized_value) in self.components {
219 let sfv_item = match component {
220 CoveredComponent::HTTP(http) => sfv::Item::try_from(http)?,
221 CoveredComponent::Derived(derived) => sfv::Item::try_from(derived)?,
222 };
223
224 let _ = writeln!(
225 output,
226 "{}: {}",
227 sfv_item.serialize_value(),
228 serialized_value
229 );
230 signature_params_line_items.push(sfv_item);
231 }
232
233 let signature_params_line = vec![sfv::ListEntry::InnerList(sfv::InnerList::with_params(
234 signature_params_line_items,
235 self.parameters.raw,
236 ))]
237 .serialize_value()
238 .ok_or(ImplementationError::SignatureParamsSerialization)?;
239
240 let _ = write!(output, "\"@signature-params\": {signature_params_line}");
241
242 if output.is_ascii() {
243 Ok((output, signature_params_line))
244 } else {
245 Err(ImplementationError::NonAsciiContentFound)
246 }
247 }
248
249 fn get_details(&self) -> ParameterDetails {
250 self.parameters.details.clone()
251 }
252
253 fn is_expired(&self) -> Option<bool> {
254 self.parameters.details.expires.map(|expires| {
255 if expires <= 0 {
256 return true;
257 }
258
259 match SystemTime::now().duration_since(UNIX_EPOCH) {
260 Ok(duration) => i64::try_from(duration.as_secs())
261 .map(|dur| dur >= expires)
262 .unwrap_or(true),
263 Err(_) => true,
264 }
265 })
266 }
267}
268
269#[derive(Clone, Debug)]
272pub enum Algorithm {
273 Ed25519,
275}
276
277impl fmt::Display for Algorithm {
278 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
279 match self {
280 Algorithm::Ed25519 => write!(f, "ed25519"),
281 }
282 }
283}
284
285pub type PublicKey = Vec<u8>;
287pub type Thumbprint = String;
290pub type KeyRing = HashMap<Thumbprint, PublicKey>;
293
294pub trait SignedMessage {
297 fn fetch_signature_header(&self) -> Option<String>;
299 fn fetch_signature_input(&self) -> Option<String>;
301 fn lookup_component(&self, name: &CoveredComponent) -> Option<String>;
308}
309
310pub trait UnsignedMessage {
313 fn fetch_components_to_cover(&self) -> IndexMap<CoveredComponent, String>;
315 fn register_header_contents(&mut self, signature_input: String, signature_header: String);
320}
321
322pub struct MessageSigner {
325 pub algorithm: Algorithm,
327 pub keyid: String,
329 pub nonce: String,
331 pub tag: String,
333}
334
335impl MessageSigner {
336 pub fn generate_signature_headers_content(
343 &self,
344 message: &mut impl UnsignedMessage,
345 expires: Duration,
346 signing_key: &PublicKey,
347 ) -> Result<(), ImplementationError> {
348 let components_to_cover = message.fetch_components_to_cover();
349 let mut sfv_parameters = sfv::Parameters::new();
350
351 sfv_parameters.insert(
352 sfv::KeyRef::constant("alg").to_owned(),
353 sfv::BareItem::String(sfv::StringRef::constant(&self.algorithm.to_string()).to_owned()),
354 );
355
356 sfv_parameters.insert(
357 sfv::KeyRef::constant("keyid").to_owned(),
358 sfv::BareItem::String(
359 sfv::StringRef::from_str(&self.keyid)
360 .map_err(|_| {
361 ImplementationError::ParsingError(
362 "keyid contains non-printable ASCII characters".into(),
363 )
364 })?
365 .to_owned(),
366 ),
367 );
368
369 sfv_parameters.insert(
370 sfv::KeyRef::constant("nonce").to_owned(),
371 sfv::BareItem::String(
372 sfv::StringRef::from_str(&self.nonce)
373 .map_err(|_| {
374 ImplementationError::ParsingError(
375 "nonce contains non-printable ASCII characters".into(),
376 )
377 })?
378 .to_owned(),
379 ),
380 );
381
382 sfv_parameters.insert(
383 sfv::KeyRef::constant("tag").to_owned(),
384 sfv::BareItem::String(
385 sfv::StringRef::from_str(&self.tag)
386 .map_err(|_| {
387 ImplementationError::ParsingError(
388 "tag contains non-printable ASCII characters".into(),
389 )
390 })?
391 .to_owned(),
392 ),
393 );
394
395 let created = SystemTime::now()
396 .duration_since(UNIX_EPOCH)
397 .map_err(ImplementationError::TimeError)?;
398 let expiry = created + expires;
399
400 let created_as_i64 = i64::try_from(created.as_secs()).map_err(|_| {
401 ImplementationError::ParsingError(
402 "Clock time does not fit in i64, verfy your clock is set correctly".into(),
403 )
404 })?;
405 let expires_as_i64 = i64::try_from(expiry.as_secs()).map_err(|_| {
406 ImplementationError::ParsingError(
407 "Clcok time + `expires` value does not fit in i64, verfy your duration is valid"
408 .into(),
409 )
410 })?;
411
412 sfv_parameters.insert(
413 sfv::KeyRef::constant("created").to_owned(),
414 sfv::BareItem::Integer(sfv::Integer::constant(created_as_i64)),
415 );
416
417 sfv_parameters.insert(
418 sfv::KeyRef::constant("expires").to_owned(),
419 sfv::BareItem::Integer(sfv::Integer::constant(expires_as_i64)),
420 );
421
422 let (signature_base, signature_params_content) = SignatureBase {
423 components: components_to_cover,
424 parameters: sfv_parameters.into(),
425 }
426 .into_ascii()?;
427
428 let signature = match self.algorithm {
429 Algorithm::Ed25519 => {
430 use ed25519_dalek::{Signer, SigningKey};
431 let signing_key_dalek = SigningKey::try_from(signing_key.as_slice())
432 .map_err(|_| ImplementationError::InvalidKeyLength)?;
433
434 sfv::Item {
435 bare_item: sfv::BareItem::ByteSequence(
436 signing_key_dalek.sign(signature_base.as_bytes()).to_vec(),
437 ),
438 params: sfv::Parameters::new(),
439 }
440 .serialize_value()
441 }
442 };
443
444 message.register_header_contents(signature_params_content, signature);
445
446 Ok(())
447 }
448}
449
450#[derive(Clone, Debug)]
451struct ParsedLabel {
452 signature: Vec<u8>,
453 base: SignatureBase,
454}
455
456#[derive(Clone, Debug)]
458pub struct MessageVerifier {
459 parsed: ParsedLabel,
460 algorithm: Algorithm,
461}
462
463#[derive(Clone, Debug)]
466pub struct SignatureTiming {
467 pub generation: Duration,
469 pub verification: Duration,
471}
472
473impl MessageVerifier {
474 pub fn parse<P>(
484 message: &impl SignedMessage,
485 alg: Option<Algorithm>,
486 pick: P,
487 ) -> Result<Self, ImplementationError>
488 where
489 P: Fn(&(sfv::Key, sfv::InnerList)) -> bool,
490 {
491 let unparsed_signature_header =
492 message
493 .fetch_signature_header()
494 .ok_or(ImplementationError::ParsingError(
495 "No `Signature` header value ".into(),
496 ))?;
497
498 let unparsed_signature_input =
499 message
500 .fetch_signature_input()
501 .ok_or(ImplementationError::ParsingError(
502 "No `Signature-Input` value ".into(),
503 ))?;
504
505 let signature_input = sfv::Parser::new(&unparsed_signature_input)
506 .parse_dictionary()
507 .map_err(|e| {
508 ImplementationError::ParsingError(format!(
509 "Failed to parse `Signature-Input` header into sfv::Dictionary: {e}"
510 ))
511 })?;
512
513 let mut signature_header = sfv::Parser::new(&unparsed_signature_header)
514 .parse_dictionary()
515 .map_err(|e| {
516 ImplementationError::ParsingError(format!(
517 "Failed to parse `Signature` header into sfv::Dictionary: {e}"
518 ))
519 })?;
520
521 let (label, innerlist) = signature_input
522 .into_iter()
523 .filter_map(|(label, listentry)| match listentry {
524 sfv::ListEntry::InnerList(inner_list) => Some((label, inner_list)),
525 sfv::ListEntry::Item(_) => None,
526 })
527 .find(pick)
528 .ok_or(ImplementationError::ParsingError(
529 "No matching label and signature base found".into(),
530 ))?;
531
532 let signature = match signature_header.shift_remove(&label).ok_or(
533 ImplementationError::ParsingError("No matching signature found from label".into()),
534 )? {
535 sfv::ListEntry::Item(sfv::Item {
536 bare_item,
537 params: _,
538 }) => match bare_item {
539 sfv::GenericBareItem::ByteSequence(sequence) => sequence,
540 other_type => {
541 return Err(ImplementationError::ParsingError(format!(
542 "Invalid type for signature found, expected byte sequence: {other_type:?}"
543 )));
544 }
545 },
546 other_type @ sfv::ListEntry::InnerList(_) => {
547 return Err(ImplementationError::ParsingError(format!(
548 "Invalid type for signature found, expected byte sequence: {other_type:?}"
549 )));
550 }
551 };
552
553 let builder = SignatureBaseBuilder::try_from(innerlist)?;
554 let base = builder.into_signature_base(message)?;
555
556 let algorithm = match alg {
557 Some(algorithm) => algorithm,
558 None => base
559 .get_details()
560 .algorithm
561 .clone()
562 .ok_or(ImplementationError::UnsupportedAlgorithm)?,
563 };
564
565 Ok(MessageVerifier {
566 parsed: ParsedLabel { signature, base },
567 algorithm,
568 })
569 }
570
571 pub fn get_details(&self) -> ParameterDetails {
574 self.parsed.base.parameters.details.clone()
575 }
576
577 pub fn verify(
587 self,
588 keyring: &KeyRing,
589 key_id: Option<Thumbprint>,
590 ) -> Result<SignatureTiming, ImplementationError> {
591 let keying_material = (match key_id {
592 Some(key) => keyring.get(&key),
593 None => self
594 .parsed
595 .base
596 .parameters
597 .details
598 .keyid
599 .as_ref()
600 .and_then(|key| keyring.get(key)),
601 })
602 .ok_or(ImplementationError::NoSuchKey)?;
603 let generation = Instant::now();
604 let (base_representation, _) = self.parsed.base.into_ascii()?;
605 let generation = generation.elapsed();
606 match self.algorithm {
607 Algorithm::Ed25519 => {
608 use ed25519_dalek::{Signature, Verifier, VerifyingKey};
609 let verifying_key = VerifyingKey::try_from(keying_material.as_slice())
610 .map_err(|_| ImplementationError::InvalidKeyLength)?;
611
612 let sig = Signature::try_from(self.parsed.signature.as_slice())
613 .map_err(|_| ImplementationError::InvalidSignatureLength)?;
614
615 let verification = Instant::now();
616 verifying_key
617 .verify(base_representation.as_bytes(), &sig)
618 .map_err(|_| ImplementationError::FailedToVerify)
619 .map(|()| SignatureTiming {
620 generation,
621 verification: verification.elapsed(),
622 })
623 }
624 }
625 }
626
627 pub fn is_expired(&self) -> Option<bool> {
629 self.parsed.base.is_expired()
630 }
631}
632
633pub trait WebBotAuthSignedMessage: SignedMessage {
636 fn fetch_signature_agent(&self) -> Option<String>;
638}
639
640#[derive(Clone, Debug)]
642pub struct WebBotAuthVerifier {
643 message_verifier: MessageVerifier,
644 key_directory: Option<String>,
646}
647
648impl WebBotAuthVerifier {
649 pub fn parse(
657 message: &impl WebBotAuthSignedMessage,
658 algorithm: Option<Algorithm>,
659 ) -> Result<Self, ImplementationError> {
660 let signature_agent = match message.fetch_signature_agent() {
661 Some(agent) => Some(sfv::Parser::new(&agent).parse_item().map_err(|e| {
662 ImplementationError::ParsingError(format!(
663 "Failed to parse `Signature-Agent` into valid sfv::Item: {e}"
664 ))
665 })?),
666 None => None,
667 };
668
669 let key_directory = signature_agent.and_then(|item| {
670 item.bare_item
671 .as_string()
672 .filter(|link| {
673 link.as_str().starts_with("https") || link.as_str().starts_with("data")
674 })
675 .map(std::string::ToString::to_string)
676 });
677
678 let web_bot_auth_verifier = Self {
679 message_verifier: MessageVerifier::parse(message, algorithm, |(_, innerlist)| {
680 innerlist.params.contains_key("keyid")
681 && innerlist.params.contains_key("tag")
682 && innerlist.params.contains_key("expires")
683 && innerlist.params.contains_key("created")
684 && innerlist
685 .params
686 .get("tag")
687 .and_then(|tag| tag.as_string())
688 .is_some_and(|tag| tag.as_str() == "web-bot-auth")
689 && innerlist.items.iter().any(|item| {
690 *item == sfv::Item::new(sfv::StringRef::constant("@authority"))
691 || (key_directory.is_some()
692 && *item
693 == sfv::Item::new(sfv::StringRef::constant("signature-agent")))
694 })
695 })?,
696 key_directory,
697 };
698
699 Ok(web_bot_auth_verifier)
700 }
701
702 pub fn verify(
714 self,
715 keyring: &KeyRing,
716 key_id: Option<Thumbprint>,
717 enforce_key_directory_lookup: bool,
718 ) -> Result<SignatureTiming, ImplementationError> {
719 if (!enforce_key_directory_lookup && self.key_directory.is_some())
720 || self.key_directory.is_none()
721 {
722 return self.message_verifier.verify(keyring, key_id);
723 }
724
725 Err(ImplementationError::WebBotAuth(
726 WebBotAuthError::NotImplemented,
727 ))
728 }
729
730 pub fn get_details(&self) -> ParameterDetails {
733 self.message_verifier.get_details()
734 }
735
736 pub fn possibly_insecure(&self) -> bool {
740 self.message_verifier.is_expired().unwrap_or(false)
741
742 }
744}
745
746#[cfg(test)]
747mod tests {
748
749 use components::{DerivedComponent, HTTPField, HTTPFieldParametersSet};
750 use indexmap::IndexMap;
751
752 use super::*;
753
754 struct StandardTestVector {}
755
756 impl SignedMessage for StandardTestVector {
757 fn fetch_signature_header(&self) -> Option<String> {
758 Some("sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned())
759 }
760 fn fetch_signature_input(&self) -> Option<String> {
761 Some(r#"sig1=("@authority");created=1735689600;keyid="poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U";alg="ed25519";expires=1735693200;nonce="gubxywVx7hzbYKatLgzuKDllDAIXAkz41PydU7aOY7vT+Mb3GJNxW0qD4zJ+IOQ1NVtg+BNbTCRUMt1Ojr5BgA==";tag="web-bot-auth""#.to_owned())
762 }
763 fn lookup_component(&self, name: &CoveredComponent) -> Option<String> {
764 match *name {
765 CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
766 Some("example.com".to_string())
767 }
768 _ => None,
769 }
770 }
771 }
772
773 impl WebBotAuthSignedMessage for StandardTestVector {
774 fn fetch_signature_agent(&self) -> Option<String> {
775 None
776 }
777 }
778
779 #[test]
780 fn test_parsing_as_http_signature() {
781 let test = StandardTestVector {};
782 let verifier = MessageVerifier::parse(&test, None, |(_, _)| true).unwrap();
783 let expected_signature_params = "(\"@authority\");created=1735689600;keyid=\"poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U\";alg=\"ed25519\";expires=1735693200;nonce=\"gubxywVx7hzbYKatLgzuKDllDAIXAkz41PydU7aOY7vT+Mb3GJNxW0qD4zJ+IOQ1NVtg+BNbTCRUMt1Ojr5BgA==\";tag=\"web-bot-auth\"";
784 let expected_base = format!(
785 "\"@authority\": example.com\n\"@signature-params\": {expected_signature_params}"
786 );
787 let (base, signature_params) = verifier.parsed.base.into_ascii().unwrap();
788 assert_eq!(base, expected_base.as_str());
789 assert_eq!(signature_params, expected_signature_params);
790 }
791
792 #[test]
793 fn test_verifying_as_http_signature() {
794 let test = StandardTestVector {};
795 let public_key: [u8; ed25519_dalek::PUBLIC_KEY_LENGTH] = [
796 0x26, 0xb4, 0x0b, 0x8f, 0x93, 0xff, 0xf3, 0xd8, 0x97, 0x11, 0x2f, 0x7e, 0xbc, 0x58,
797 0x2b, 0x23, 0x2d, 0xbd, 0x72, 0x51, 0x7d, 0x08, 0x2f, 0xe8, 0x3c, 0xfb, 0x30, 0xdd,
798 0xce, 0x43, 0xd1, 0xbb,
799 ];
800 let keyring: KeyRing = HashMap::from_iter([(
801 "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U".to_string(),
802 public_key.to_vec(),
803 )]);
804 let verifier = MessageVerifier::parse(&test, None, |(_, _)| true).unwrap();
805 let timing = verifier.verify(&keyring, None).unwrap();
806 assert!(timing.generation.as_nanos() > 0);
807 assert!(timing.verification.as_nanos() > 0);
808 }
809
810 #[test]
811 fn test_verifying_as_web_bot_auth() {
812 let test = StandardTestVector {};
813 let public_key: [u8; ed25519_dalek::PUBLIC_KEY_LENGTH] = [
814 0x26, 0xb4, 0x0b, 0x8f, 0x93, 0xff, 0xf3, 0xd8, 0x97, 0x11, 0x2f, 0x7e, 0xbc, 0x58,
815 0x2b, 0x23, 0x2d, 0xbd, 0x72, 0x51, 0x7d, 0x08, 0x2f, 0xe8, 0x3c, 0xfb, 0x30, 0xdd,
816 0xce, 0x43, 0xd1, 0xbb,
817 ];
818 let keyring: KeyRing = HashMap::from_iter([(
819 "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U".to_string(),
820 public_key.to_vec(),
821 )]);
822 let verifier = WebBotAuthVerifier::parse(&test, None).unwrap();
823 assert!(verifier.possibly_insecure());
825 let timing = verifier.verify(&keyring, None, false).unwrap();
826 assert!(timing.generation.as_nanos() > 0);
827 assert!(timing.verification.as_nanos() > 0);
828 }
829
830 #[test]
831 fn test_signing_then_verifying() {
832 struct MyTest {
833 signature_input: String,
834 signature_header: String,
835 }
836
837 impl UnsignedMessage for MyTest {
838 fn fetch_components_to_cover(&self) -> IndexMap<CoveredComponent, String> {
839 IndexMap::from_iter([(
840 CoveredComponent::Derived(DerivedComponent::Authority { req: false }),
841 "example.com".to_string(),
842 )])
843 }
844
845 fn register_header_contents(
846 &mut self,
847 signature_input: String,
848 signature_header: String,
849 ) {
850 self.signature_input = format!("sig1={signature_input}");
851 self.signature_header = format!("sig1={signature_header}");
852 }
853 }
854
855 impl SignedMessage for MyTest {
856 fn fetch_signature_header(&self) -> Option<String> {
857 Some(self.signature_header.clone())
858 }
859 fn fetch_signature_input(&self) -> Option<String> {
860 Some(self.signature_input.clone())
861 }
862 fn lookup_component(&self, name: &CoveredComponent) -> Option<String> {
863 match *name {
864 CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
865 Some("example.com".to_string())
866 }
867 _ => None,
868 }
869 }
870 }
871
872 impl WebBotAuthSignedMessage for MyTest {
873 fn fetch_signature_agent(&self) -> Option<String> {
874 None
875 }
876 }
877
878 let public_key: [u8; ed25519_dalek::PUBLIC_KEY_LENGTH] = [
879 0x26, 0xb4, 0x0b, 0x8f, 0x93, 0xff, 0xf3, 0xd8, 0x97, 0x11, 0x2f, 0x7e, 0xbc, 0x58,
880 0x2b, 0x23, 0x2d, 0xbd, 0x72, 0x51, 0x7d, 0x08, 0x2f, 0xe8, 0x3c, 0xfb, 0x30, 0xdd,
881 0xce, 0x43, 0xd1, 0xbb,
882 ];
883
884 let private_key: [u8; ed25519_dalek::SECRET_KEY_LENGTH] = [
885 0x9f, 0x83, 0x62, 0xf8, 0x7a, 0x48, 0x4a, 0x95, 0x4e, 0x6e, 0x74, 0x0c, 0x5b, 0x4c,
886 0x0e, 0x84, 0x22, 0x91, 0x39, 0xa2, 0x0a, 0xa8, 0xab, 0x56, 0xff, 0x66, 0x58, 0x6f,
887 0x6a, 0x7d, 0x29, 0xc5,
888 ];
889
890 let keyring: KeyRing = HashMap::from_iter([(
891 "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U".to_string(),
892 public_key.to_vec(),
893 )]);
894
895 let signer = MessageSigner {
896 algorithm: Algorithm::Ed25519,
897 keyid: "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U".into(),
898 nonce: "end-to-end-test".into(),
899 tag: "web-bot-auth".into(),
900 };
901
902 let mut mytest = MyTest {
903 signature_input: String::new(),
904 signature_header: String::new(),
905 };
906
907 signer
908 .generate_signature_headers_content(
909 &mut mytest,
910 Duration::from_secs(10),
911 &private_key.to_vec(),
912 )
913 .unwrap();
914
915 let verifier = WebBotAuthVerifier::parse(&mytest, None).unwrap();
916 assert!(!verifier.possibly_insecure());
917
918 let timing = verifier.verify(&keyring, None, false).unwrap();
919 assert!(timing.generation.as_nanos() > 0);
920 assert!(timing.verification.as_nanos() > 0);
921 }
922
923 #[test]
924 fn test_missing_tags_break_web_bot_auth() {
925 struct MissingParametersTestVector {}
926
927 impl SignedMessage for MissingParametersTestVector {
928 fn fetch_signature_header(&self) -> Option<String> {
929 Some("sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned())
930 }
931 fn fetch_signature_input(&self) -> Option<String> {
932 Some(r#"sig1=("@authority");created=1735689600;keyid="poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U";alg="ed25519";expires=1735693200;nonce="gubxywVx7hzbYKatLgzuKDllDAIXAkz41PydU7aOY7vT+Mb3GJNxW0qD4zJ+IOQ1NVtg+BNbTCRUMt1Ojr5BgA==";tag="not-web-bot-auth""#.to_owned())
933 }
934 fn lookup_component(&self, name: &CoveredComponent) -> Option<String> {
935 match *name {
936 CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
937 Some("example.com".to_string())
938 }
939 _ => None,
940 }
941 }
942 }
943
944 impl WebBotAuthSignedMessage for MissingParametersTestVector {
945 fn fetch_signature_agent(&self) -> Option<String> {
946 None
947 }
948 }
949
950 let test = MissingParametersTestVector {};
951 WebBotAuthVerifier::parse(&test, None).expect_err("This should not have parsed");
952 }
953
954 #[test]
955 fn test_signing() {
956 struct SigningTest {}
957 impl UnsignedMessage for SigningTest {
958 fn fetch_components_to_cover(&self) -> IndexMap<CoveredComponent, String> {
959 IndexMap::from_iter([
960 (
961 CoveredComponent::Derived(DerivedComponent::Method { req: false }),
962 "POST".to_string(),
963 ),
964 (
965 CoveredComponent::Derived(DerivedComponent::Authority { req: false }),
966 "example.com".to_string(),
967 ),
968 (
969 CoveredComponent::HTTP(HTTPField {
970 name: "content-length".to_string(),
971 parameters: HTTPFieldParametersSet(vec![]),
972 }),
973 "18".to_string(),
974 ),
975 ])
976 }
977
978 fn register_header_contents(
979 &mut self,
980 _signature_input: String,
981 _signature_header: String,
982 ) {
983 }
984 }
985
986 let signer = MessageSigner {
987 algorithm: Algorithm::Ed25519,
988 keyid: "test".into(),
989 nonce: "another-test".into(),
990 tag: "web-bot-auth".into(),
991 };
992
993 let private_key: [u8; ed25519_dalek::SECRET_KEY_LENGTH] = [
994 0x9f, 0x83, 0x62, 0xf8, 0x7a, 0x48, 0x4a, 0x95, 0x4e, 0x6e, 0x74, 0x0c, 0x5b, 0x4c,
995 0x0e, 0x84, 0x22, 0x91, 0x39, 0xa2, 0x0a, 0xa8, 0xab, 0x56, 0xff, 0x66, 0x58, 0x6f,
996 0x6a, 0x7d, 0x29, 0xc5,
997 ];
998
999 let mut test = SigningTest {};
1000
1001 assert!(
1002 signer
1003 .generate_signature_headers_content(
1004 &mut test,
1005 Duration::from_secs(10),
1006 &private_key.to_vec()
1007 )
1008 .is_ok()
1009 );
1010 }
1011
1012 #[test]
1013 fn signature_base_generates_the_expected_representation() {
1014 let sigbase = SignatureBase {
1015 components: IndexMap::from_iter([
1016 (
1017 CoveredComponent::Derived(DerivedComponent::Method { req: false }),
1018 "POST".to_string(),
1019 ),
1020 (
1021 CoveredComponent::Derived(DerivedComponent::Authority { req: false }),
1022 "example.com".to_string(),
1023 ),
1024 (
1025 CoveredComponent::HTTP(HTTPField {
1026 name: "content-length".to_string(),
1027 parameters: HTTPFieldParametersSet(vec![]),
1028 }),
1029 "18".to_string(),
1030 ),
1031 ]),
1032 parameters: IndexMap::from_iter([
1033 (
1034 sfv::Key::from_string("keyid".into()).unwrap(),
1035 sfv::BareItem::String(sfv::String::from_string("test".to_string()).unwrap()),
1036 ),
1037 (
1038 sfv::Key::from_string("created".into()).unwrap(),
1039 sfv::BareItem::Integer(sfv::Integer::constant(1_618_884_473_i64)),
1040 ),
1041 ])
1042 .into(),
1043 };
1044
1045 let expected_base = "\"@method\": POST\n\"@authority\": example.com\n\"content-length\": 18\n\"@signature-params\": (\"@method\" \"@authority\" \"content-length\");keyid=\"test\";created=1618884473";
1046 let (base, _) = sigbase.into_ascii().unwrap();
1047 assert_eq!(base, expected_base);
1048 }
1049}