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#[derive(Clone, Debug)]
12pub struct SignatureParams {
13 pub raw: sfv::Parameters,
15 pub details: ParameterDetails,
17}
18
19#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct ParameterDetails {
22 pub algorithm: Option<Algorithm>,
24 pub created: Option<i64>,
26 pub expires: Option<i64>,
28 pub keyid: Option<String>,
30 pub nonce: Option<String>,
32 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
86pub struct SecurityAdvisory {
89 pub is_expired: Option<bool>,
92 pub nonce_is_invalid: Option<bool>,
95}
96
97impl ParameterDetails {
98 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 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#[derive(Clone, Debug)]
168pub struct SignatureBase {
169 pub components: IndexMap<CoveredComponent, String>,
171 pub parameters: SignatureParams,
173}
174
175impl SignatureBase {
176 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
215pub trait SignedMessage {
218 fn fetch_all_signature_headers(&self) -> Vec<String>;
226 fn fetch_all_signature_inputs(&self) -> Vec<String>;
235 fn lookup_component(&self, name: &CoveredComponent) -> Option<String>;
243}
244
245pub trait UnsignedMessage {
248 fn fetch_components_to_cover(&self) -> IndexMap<CoveredComponent, String>;
253 fn register_header_contents(&mut self, signature_input: String, signature_header: String);
258}
259
260pub struct MessageSigner {
263 pub keyid: String,
265 pub nonce: String,
267 pub tag: String,
269}
270
271impl MessageSigner {
272 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#[derive(Clone, Debug)]
388pub struct ParsedLabel {
389 pub signature: Vec<u8>,
391 pub base: SignatureBase,
394}
395
396#[derive(Clone, Debug)]
398pub struct MessageVerifier {
399 pub parsed: ParsedLabel,
401}
402
403#[derive(Clone, Debug)]
406pub struct SignatureTiming {
407 pub generation: Duration,
409 pub verification: Duration,
411}
412
413impl MessageVerifier {
414 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 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}