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#[derive(Clone, Debug)]
15pub struct SignatureParams {
16 pub raw: sfv::Parameters,
18 pub details: ParameterDetails,
20}
21
22#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct ParameterDetails {
25 pub algorithm: Option<Algorithm>,
27 pub created: Option<i64>,
29 pub expires: Option<i64>,
31 pub keyid: Option<String>,
33 pub nonce: Option<String>,
35 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
89pub struct SecurityAdvisory {
92 pub is_expired: Option<bool>,
95 pub nonce_is_invalid: Option<bool>,
98}
99
100impl ParameterDetails {
101 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 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 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#[derive(Clone, Debug)]
185pub struct SignatureBase {
186 pub components: IndexMap<CoveredComponent, String>,
188 pub parameters: SignatureParams,
190}
191
192impl SignatureBase {
193 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
232pub trait SignedMessage {
235 fn lookup_component(&self, name: &CoveredComponent) -> Vec<String>;
247}
248
249pub trait UnsignedMessage {
252 fn fetch_components_to_cover(&self) -> IndexMap<CoveredComponent, String>;
257 fn register_header_contents(&mut self, signature_input: String, signature_header: String);
262}
263
264pub struct MessageSigner {
267 pub keyid: String,
269 pub nonce: String,
271 pub tag: String,
273}
274
275impl MessageSigner {
276 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#[derive(Clone, Debug)]
391pub struct ParsedLabel {
392 pub label: sfv::Key,
394 pub signature: Vec<u8>,
396 pub base: SignatureBase,
399}
400
401#[derive(Clone, Debug)]
403pub struct MessageVerifier {
404 pub parsed: ParsedLabel,
406}
407
408#[derive(Clone, Debug)]
411pub struct SignatureTiming {
412 pub generation: Duration,
414 pub verification: Duration,
416}
417
418impl MessageVerifier {
419 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 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}