1use base64::engine::general_purpose::{STANDARD, STANDARD_NO_PAD, URL_SAFE_NO_PAD};
44use base64::engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig};
45use base64::Engine;
46use ed25519_dalek::{Signature, Signer};
47use sha2::{Digest, Sha256};
48
49pub use ed25519_dalek::{SigningKey, VerifyingKey};
50
51pub const CONTENT_DIGEST_HEADER: &str = "content-digest";
53
54pub const SIGNATURE_INPUT_HEADER: &str = "signature-input";
56
57pub const SIGNATURE_HEADER: &str = "signature";
59
60pub const LABEL: &str = "sig1";
62
63pub const ALGORITHM: &str = "ed25519";
65
66pub const COVERED_COMPONENTS: [&str; 6] = [
69 "@method",
70 "@authority",
71 "@path",
72 "@query",
73 CONTENT_DIGEST_HEADER,
74 crate::PROTOCOL_HEADER,
75];
76
77pub const WINDOW_SECONDS: u64 = 60;
80
81pub const MAX_AHEAD_SECONDS: u64 = 5;
91
92pub const MAX_NONCE_LEN: usize = 128;
95
96#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
99pub enum SignatureError {
100 #[error("malformed {0} header")]
102 Malformed(&'static str),
103 #[error("no signature labelled sig1")]
105 NoLabel,
106 #[error("the signature has no {0} parameter")]
108 MissingParameter(&'static str),
109 #[error(
111 "the signature must cover @method, @authority, @path, @query, content-digest and recall-protocol"
112 )]
113 NotCovered,
114 #[error("the signature covers {0} twice")]
116 Duplicate(String),
117 #[error("the signature covers {0}, which this request does not have")]
119 MissingComponent(String),
120 #[error("unsupported signature algorithm {0:?}")]
122 Algorithm(String),
123 #[error("the nonce must be 1 to 128 characters")]
125 Nonce,
126 #[error(
128 "signature created {skew} seconds from the server's clock, more than the {window} allowed; check this machine's clock"
129 )]
130 Clock {
131 skew: u64,
133 window: u64,
135 },
136 #[error(
139 "signature created {ahead} seconds ahead of the server's clock, more than the {allowed} allowed; check this machine's clock"
140 )]
141 Ahead {
142 ahead: u64,
144 allowed: u64,
146 },
147 #[error("the signature has expired")]
149 Expired,
150 #[error("content-digest has no sha-256 value")]
152 NoDigest,
153 #[error("content-digest does not match the body")]
155 DigestMismatch,
156 #[error("the signature base is not ASCII")]
158 NotAscii,
159 #[error("the signature does not verify")]
161 BadSignature,
162 #[error("public_key must be an Ed25519 public key: 32 bytes, base64url without padding")]
164 PublicKey,
165}
166
167pub fn encode_public_key(key: &VerifyingKey) -> String {
174 URL_SAFE_NO_PAD.encode(key.as_bytes())
175}
176
177pub fn parse_public_key(text: &str) -> Result<VerifyingKey, SignatureError> {
182 let bytes = URL_SAFE_NO_PAD
183 .decode(text.trim())
184 .map_err(|_| SignatureError::PublicKey)?;
185 let raw: [u8; 32] = bytes.try_into().map_err(|_| SignatureError::PublicKey)?;
186 let key = VerifyingKey::from_bytes(&raw).map_err(|_| SignatureError::PublicKey)?;
187 if key.is_weak() {
188 return Err(SignatureError::PublicKey);
189 }
190 Ok(key)
191}
192
193pub fn fingerprint(key: &VerifyingKey) -> String {
197 format!(
198 "SHA256:{}",
199 STANDARD_NO_PAD.encode(Sha256::digest(key.as_bytes()))
200 )
201}
202
203pub fn content_digest(body: &[u8]) -> String {
210 format!("sha-256=:{}:", STANDARD.encode(Sha256::digest(body)))
211}
212
213pub fn check_content_digest(field: &str, body: &[u8]) -> Result<(), SignatureError> {
218 if sha256_of(field)?.as_slice() == Sha256::digest(body).as_slice() {
219 Ok(())
220 } else {
221 Err(SignatureError::DigestMismatch)
222 }
223}
224
225fn sha256_of(field: &str) -> Result<Vec<u8>, SignatureError> {
227 let dict = sf::dictionary(field).ok_or(SignatureError::Malformed("content-digest"))?;
228 dict.into_iter()
229 .find(|(k, _)| k == "sha-256")
230 .and_then(|(_, m)| match m {
231 sf::Member::Item(sf::Item::Bytes(b), _) => Some(b),
232 _ => None,
233 })
234 .ok_or(SignatureError::NoDigest)
235}
236
237#[derive(Debug, Clone, Copy, PartialEq, Eq)]
243pub struct Target<'a> {
244 pub method: &'a str,
246 pub authority: &'a str,
248 pub path: &'a str,
250 pub query: Option<&'a str>,
253}
254
255impl Target<'_> {
256 fn derived(&self, name: &str) -> Option<String> {
259 match name {
260 "@method" => Some(self.method.to_string()),
261 "@authority" => Some(self.authority.to_string()),
262 "@path" => Some(if self.path.is_empty() {
264 "/".to_string()
265 } else {
266 self.path.to_string()
267 }),
268 "@query" => Some(format!("?{}", self.query.unwrap_or(""))),
271 _ => None,
272 }
273 }
274}
275
276pub fn normalize_authority(host: &str) -> String {
284 let host = host.trim().to_ascii_lowercase();
285 for default in [":443", ":80"] {
286 if let Some(stripped) = host.strip_suffix(default) {
287 return stripped.to_string();
288 }
289 }
290 host
291}
292
293#[derive(Debug, Clone, PartialEq, Eq)]
299pub enum Param {
300 Integer(i64),
302 String(String),
304 Token(String),
306 Boolean(bool),
308 Bytes(Vec<u8>),
310}
311
312#[derive(Debug, Clone, PartialEq, Eq)]
320pub struct SignatureInput {
321 pub components: Vec<String>,
323 pub params: Vec<(String, Param)>,
325}
326
327impl SignatureInput {
328 pub fn recall(created: i64, keyid: &str, nonce: &str) -> Self {
331 Self {
332 components: COVERED_COMPONENTS.iter().map(|c| c.to_string()).collect(),
333 params: vec![
334 ("created".to_string(), Param::Integer(created)),
335 ("keyid".to_string(), Param::String(keyid.to_string())),
336 ("nonce".to_string(), Param::String(nonce.to_string())),
337 ("alg".to_string(), Param::String(ALGORITHM.to_string())),
338 ],
339 }
340 }
341
342 pub fn parse(field: &str, label: &str) -> Result<Self, SignatureError> {
344 const WHAT: &str = "signature-input";
345 let dict = sf::dictionary(field).ok_or(SignatureError::Malformed(WHAT))?;
346 let (items, params) = match dict.into_iter().find(|(k, _)| k == label) {
347 Some((_, sf::Member::InnerList(items, params))) => (items, params),
348 Some(_) => return Err(SignatureError::Malformed(WHAT)),
349 None => return Err(SignatureError::NoLabel),
350 };
351 let mut components = Vec::with_capacity(items.len());
352 for (item, item_params) in items {
353 match item {
356 sf::Item::String(name) if item_params.is_empty() => components.push(name),
357 _ => return Err(SignatureError::Malformed(WHAT)),
358 }
359 }
360 Ok(Self {
361 components,
362 params: params.into_iter().map(|(k, v)| (k, v.into())).collect(),
363 })
364 }
365
366 pub fn serialize(&self) -> String {
369 let mut out = String::from("(");
370 for (i, c) in self.components.iter().enumerate() {
371 if i > 0 {
372 out.push(' ');
373 }
374 sf::write_string(&mut out, c);
375 }
376 out.push(')');
377 for (key, value) in &self.params {
378 out.push(';');
379 out.push_str(key);
380 match value {
381 Param::Boolean(true) => {}
382 Param::Boolean(false) => out.push_str("=?0"),
383 Param::Integer(n) => {
384 out.push('=');
385 out.push_str(&n.to_string());
386 }
387 Param::String(s) => {
388 out.push('=');
389 sf::write_string(&mut out, s);
390 }
391 Param::Token(t) => {
392 out.push('=');
393 out.push_str(t);
394 }
395 Param::Bytes(b) => {
396 out.push_str("=:");
397 out.push_str(&STANDARD.encode(b));
398 out.push(':');
399 }
400 }
401 }
402 out
403 }
404
405 fn param(&self, name: &str) -> Option<&Param> {
406 self.params.iter().find(|(k, _)| k == name).map(|(_, v)| v)
407 }
408
409 fn string_param(&self, name: &str) -> Option<&str> {
410 match self.param(name)? {
411 Param::String(s) => Some(s),
412 _ => None,
413 }
414 }
415
416 fn integer_param(&self, name: &str) -> Option<i64> {
417 match self.param(name)? {
418 Param::Integer(n) => Some(*n),
419 _ => None,
420 }
421 }
422
423 pub fn created(&self) -> Option<i64> {
425 self.integer_param("created")
426 }
427
428 pub fn keyid(&self) -> Option<&str> {
430 self.string_param("keyid")
431 }
432
433 pub fn nonce(&self) -> Option<&str> {
435 self.string_param("nonce")
436 }
437
438 pub fn alg(&self) -> Option<&str> {
440 self.string_param("alg")
441 }
442
443 pub fn signature_base(
446 &self,
447 value: impl Fn(&str) -> Option<String>,
448 ) -> Result<String, SignatureError> {
449 let mut out = String::new();
450 let mut seen: Vec<&str> = Vec::with_capacity(self.components.len());
451 for name in &self.components {
452 if seen.contains(&name.as_str()) {
453 return Err(SignatureError::Duplicate(name.clone()));
454 }
455 seen.push(name);
456 let v = value(name).ok_or_else(|| SignatureError::MissingComponent(name.clone()))?;
457 if v.contains('\n') || v.contains('\r') {
459 return Err(SignatureError::MissingComponent(name.clone()));
460 }
461 sf::write_string(&mut out, name);
462 out.push_str(": ");
463 out.push_str(&v);
464 out.push('\n');
465 }
466 out.push_str("\"@signature-params\": ");
467 out.push_str(&self.serialize());
468 if !out.is_ascii() {
469 return Err(SignatureError::NotAscii);
470 }
471 Ok(out)
472 }
473
474 pub fn check_profile(&self, now: i64, window: u64) -> Result<(), SignatureError> {
479 if let Some(param) = self.param("alg") {
483 match param {
484 Param::String(a) if a == ALGORITHM => {}
485 Param::String(a) => return Err(SignatureError::Algorithm(a.clone())),
486 _ => return Err(SignatureError::Algorithm(String::new())),
487 }
488 }
489 if self.keyid().is_none() {
490 return Err(SignatureError::MissingParameter("keyid"));
491 }
492 let nonce = self
493 .nonce()
494 .ok_or(SignatureError::MissingParameter("nonce"))?;
495 if nonce.is_empty() || nonce.len() > MAX_NONCE_LEN {
496 return Err(SignatureError::Nonce);
497 }
498 let created = self
499 .created()
500 .ok_or(SignatureError::MissingParameter("created"))?;
501 let skew = now.abs_diff(created);
502 if created > now && skew > MAX_AHEAD_SECONDS {
503 return Err(SignatureError::Ahead {
504 ahead: skew,
505 allowed: MAX_AHEAD_SECONDS,
506 });
507 }
508 if skew > window {
509 return Err(SignatureError::Clock { skew, window });
510 }
511 if let Some(expires) = self.integer_param("expires") {
512 if expires < now {
513 return Err(SignatureError::Expired);
514 }
515 }
516 if !COVERED_COMPONENTS
517 .iter()
518 .all(|c| self.components.iter().any(|have| have == c))
519 {
520 return Err(SignatureError::NotCovered);
521 }
522 Ok(())
523 }
524}
525
526impl From<sf::Item> for Param {
527 fn from(item: sf::Item) -> Self {
528 match item {
529 sf::Item::Integer(n) => Param::Integer(n),
530 sf::Item::String(s) => Param::String(s),
531 sf::Item::Token(t) => Param::Token(t),
532 sf::Item::Boolean(b) => Param::Boolean(b),
533 sf::Item::Bytes(b) => Param::Bytes(b),
534 }
535 }
536}
537
538pub fn parse_signature(field: &str, label: &str) -> Result<Vec<u8>, SignatureError> {
540 let dict = sf::dictionary(field).ok_or(SignatureError::Malformed("signature"))?;
541 match dict.into_iter().find(|(k, _)| k == label) {
542 Some((_, sf::Member::Item(sf::Item::Bytes(b), _))) => Ok(b),
543 Some(_) => Err(SignatureError::Malformed("signature")),
544 None => Err(SignatureError::NoLabel),
545 }
546}
547
548pub fn sign(key: &SigningKey, base: &str) -> Vec<u8> {
554 key.sign(base.as_bytes()).to_bytes().to_vec()
555}
556
557pub fn verify(key: &VerifyingKey, base: &str, signature: &[u8]) -> Result<(), SignatureError> {
563 let bytes: [u8; 64] = signature
564 .try_into()
565 .map_err(|_| SignatureError::BadSignature)?;
566 key.verify_strict(base.as_bytes(), &Signature::from_bytes(&bytes))
567 .map_err(|_| SignatureError::BadSignature)
568}
569
570fn component_value(
573 target: &Target<'_>,
574 field: &dyn Fn(&str) -> Option<String>,
575 name: &str,
576) -> Option<String> {
577 if name.starts_with('@') {
578 target.derived(name)
579 } else {
580 field(name).map(|v| v.trim().to_string())
581 }
582}
583
584#[derive(Debug, Clone, PartialEq, Eq)]
587pub struct SignedHeaders {
588 pub content_digest: String,
590 pub signature_input: String,
592 pub signature: String,
594}
595
596pub fn sign_request(
604 key: &SigningKey,
605 keyid: &str,
606 target: &Target<'_>,
607 protocol: &str,
608 body: &[u8],
609 created: i64,
610 nonce: &str,
611) -> Result<SignedHeaders, SignatureError> {
612 let digest = content_digest(body);
613 let input = SignatureInput::recall(created, keyid, nonce);
614 let field = |name: &str| match name {
615 CONTENT_DIGEST_HEADER => Some(digest.clone()),
616 crate::PROTOCOL_HEADER => Some(protocol.to_string()),
617 _ => None,
618 };
619 let base = input.signature_base(|name| component_value(target, &field, name))?;
620 Ok(SignedHeaders {
621 signature_input: format!("{LABEL}={}", input.serialize()),
622 signature: format!("{LABEL}=:{}:", STANDARD.encode(sign(key, &base))),
623 content_digest: digest,
624 })
625}
626
627pub struct Received<'a> {
630 pub input: &'a SignatureInput,
632 pub signature: &'a [u8],
634 pub target: Target<'a>,
636 pub field: &'a dyn Fn(&str) -> Option<String>,
639}
640
641pub fn verify_headers(
655 req: &Received<'_>,
656 key: &VerifyingKey,
657 now: i64,
658 window: u64,
659) -> Result<(), SignatureError> {
660 verify_headers_and_base(req, key, now, window).map(|_base| ())
661}
662
663pub fn verify_headers_and_base(
669 req: &Received<'_>,
670 key: &VerifyingKey,
671 now: i64,
672 window: u64,
673) -> Result<String, SignatureError> {
674 req.input.check_profile(now, window)?;
675 let digest = (req.field)(CONTENT_DIGEST_HEADER)
676 .ok_or_else(|| SignatureError::MissingComponent(CONTENT_DIGEST_HEADER.to_string()))?;
677 sha256_of(&digest)?;
678 let base = req
679 .input
680 .signature_base(|name| component_value(&req.target, req.field, name))?;
681 verify(key, &base, req.signature)?;
682 Ok(base)
683}
684
685pub fn content_digest_base64(field: &str) -> Result<String, SignatureError> {
689 Ok(STANDARD.encode(sha256_of(field)?))
690}
691
692pub fn verify_request(
695 req: &Received<'_>,
696 body: &[u8],
697 key: &VerifyingKey,
698 now: i64,
699 window: u64,
700) -> Result<(), SignatureError> {
701 verify_headers(req, key, now, window)?;
702 let digest = (req.field)(CONTENT_DIGEST_HEADER).unwrap_or_default();
703 check_content_digest(&digest, body)
704}
705
706mod sf {
711 use super::*;
716
717 #[derive(Debug, Clone, PartialEq, Eq)]
718 pub(super) enum Item {
719 Integer(i64),
720 String(String),
721 Token(String),
722 Boolean(bool),
723 Bytes(Vec<u8>),
724 }
725
726 pub(super) type Params = Vec<(String, Item)>;
727
728 #[derive(Debug, Clone, PartialEq, Eq)]
729 pub(super) enum Member {
730 Item(Item, Params),
731 InnerList(Vec<(Item, Params)>, Params),
732 }
733
734 pub(super) fn write_string(out: &mut String, s: &str) {
736 out.push('"');
737 for c in s.chars() {
738 if c == '"' || c == '\\' {
739 out.push('\\');
740 }
741 out.push(c);
742 }
743 out.push('"');
744 }
745
746 struct Parser<'a> {
747 s: &'a [u8],
748 at: usize,
749 }
750
751 pub(super) fn dictionary(input: &str) -> Option<Vec<(String, Member)>> {
753 if !input.is_ascii() {
754 return None;
755 }
756 let mut p = Parser {
757 s: input.as_bytes(),
758 at: 0,
759 };
760 p.skip_sp();
761 let mut dict: Vec<(String, Member)> = Vec::new();
762 while !p.done() {
763 let key = p.key()?;
764 let member = if p.eat(b'=') {
765 p.item_or_inner_list()?
766 } else {
767 Member::Item(Item::Boolean(true), p.params()?)
768 };
769 match dict.iter_mut().find(|(k, _)| *k == key) {
771 Some(existing) => existing.1 = member,
772 None => dict.push((key, member)),
773 }
774 p.skip_ows();
775 if p.done() {
776 break;
777 }
778 if !p.eat(b',') {
779 return None;
780 }
781 p.skip_ows();
782 if p.done() {
783 return None;
784 }
785 }
786 Some(dict)
787 }
788
789 impl Parser<'_> {
790 fn done(&self) -> bool {
791 self.at >= self.s.len()
792 }
793
794 fn peek(&self) -> Option<u8> {
795 self.s.get(self.at).copied()
796 }
797
798 fn eat(&mut self, c: u8) -> bool {
799 if self.peek() == Some(c) {
800 self.at += 1;
801 true
802 } else {
803 false
804 }
805 }
806
807 fn skip_sp(&mut self) {
808 while self.peek() == Some(b' ') {
809 self.at += 1;
810 }
811 }
812
813 fn skip_ows(&mut self) {
814 while matches!(self.peek(), Some(b' ' | b'\t')) {
815 self.at += 1;
816 }
817 }
818
819 fn key(&mut self) -> Option<String> {
821 let start = self.at;
822 match self.peek()? {
823 b'a'..=b'z' | b'*' => self.at += 1,
824 _ => return None,
825 }
826 while matches!(
827 self.peek(),
828 Some(b'a'..=b'z' | b'0'..=b'9' | b'_' | b'-' | b'.' | b'*')
829 ) {
830 self.at += 1;
831 }
832 Some(String::from_utf8_lossy(&self.s[start..self.at]).into_owned())
833 }
834
835 fn item_or_inner_list(&mut self) -> Option<Member> {
837 if self.eat(b'(') {
838 let mut items = Vec::new();
839 loop {
840 self.skip_sp();
841 if self.eat(b')') {
842 return Some(Member::InnerList(items, self.params()?));
843 }
844 if self.done() {
845 return None;
846 }
847 let item = self.bare_item()?;
848 items.push((item, self.params()?));
849 if !matches!(self.peek(), Some(b' ' | b')')) {
850 return None;
851 }
852 }
853 }
854 let item = self.bare_item()?;
855 Some(Member::Item(item, self.params()?))
856 }
857
858 fn params(&mut self) -> Option<Params> {
860 let mut params: Params = Vec::new();
861 while self.eat(b';') {
862 self.skip_sp();
863 let key = self.key()?;
864 let value = if self.eat(b'=') {
865 self.bare_item()?
866 } else {
867 Item::Boolean(true)
868 };
869 match params.iter_mut().find(|(k, _)| *k == key) {
870 Some(existing) => existing.1 = value,
871 None => params.push((key, value)),
872 }
873 }
874 Some(params)
875 }
876
877 fn bare_item(&mut self) -> Option<Item> {
879 match self.peek()? {
880 b'-' | b'0'..=b'9' => self.integer(),
881 b'"' => self.string(),
882 b':' => self.bytes(),
883 b'?' => self.boolean(),
884 b'A'..=b'Z' | b'a'..=b'z' | b'*' => self.token(),
885 _ => None,
886 }
887 }
888
889 fn integer(&mut self) -> Option<Item> {
891 let negative = self.eat(b'-');
892 let start = self.at;
893 while matches!(self.peek(), Some(b'0'..=b'9')) {
894 self.at += 1;
895 }
896 let digits = &self.s[start..self.at];
897 if digits.is_empty() || digits.len() > 15 || self.peek() == Some(b'.') {
898 return None;
899 }
900 let n: i64 = std::str::from_utf8(digits).ok()?.parse().ok()?;
901 Some(Item::Integer(if negative { -n } else { n }))
902 }
903
904 fn string(&mut self) -> Option<Item> {
906 self.at += 1;
907 let mut out = String::new();
908 loop {
909 let c = self.peek()?;
910 self.at += 1;
911 match c {
912 b'"' => return Some(Item::String(out)),
913 b'\\' => match self.peek()? {
914 c @ (b'"' | b'\\') => {
915 self.at += 1;
916 out.push(c as char);
917 }
918 _ => return None,
919 },
920 0x20..=0x7e => out.push(c as char),
921 _ => return None,
922 }
923 }
924 }
925
926 fn token(&mut self) -> Option<Item> {
928 let start = self.at;
929 self.at += 1;
930 while let Some(c) = self.peek() {
931 let tchar = c.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~:/".contains(&c);
932 if !tchar {
933 break;
934 }
935 self.at += 1;
936 }
937 Some(Item::Token(
938 String::from_utf8_lossy(&self.s[start..self.at]).into_owned(),
939 ))
940 }
941
942 fn bytes(&mut self) -> Option<Item> {
945 self.at += 1;
946 let start = self.at;
947 while self.peek()? != b':' {
948 let c = self.peek()?;
949 if !(c.is_ascii_alphanumeric() || c == b'+' || c == b'/' || c == b'=') {
950 return None;
951 }
952 self.at += 1;
953 }
954 let b64 = &self.s[start..self.at];
955 self.at += 1;
956 const LENIENT: GeneralPurpose = GeneralPurpose::new(
957 &base64::alphabet::STANDARD,
958 GeneralPurposeConfig::new()
959 .with_decode_padding_mode(DecodePaddingMode::Indifferent)
960 .with_decode_allow_trailing_bits(true),
961 );
962 LENIENT.decode(b64).ok().map(Item::Bytes)
963 }
964
965 fn boolean(&mut self) -> Option<Item> {
967 self.at += 1;
968 let v = match self.peek()? {
969 b'0' => false,
970 b'1' => true,
971 _ => return None,
972 };
973 self.at += 1;
974 Some(Item::Boolean(v))
975 }
976 }
977}
978
979#[cfg(test)]
980mod tests {
981 use super::*;
982
983 const TEST_KEY_ED25519_D: &str = "n4Ni-HpISpVObnQMW0wOhCKROaIKqKtW_2ZYb2p9KcU";
999 const TEST_KEY_ED25519_X: &str = "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs";
1000 const TEST_KEY_ED25519_PUBLIC_PEM: &str =
1002 "MCowBQYDK2VwAyEAJrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=";
1003 const TEST_KEY_ED25519_PRIVATE_PEM: &str =
1004 "MC4CAQAwBQYDK2VwBCIEIJ+DYvh6SEqVTm50DFtMDoQikTmiCqirVv9mWG9qfSnF";
1005
1006 fn test_key() -> SigningKey {
1007 let d: [u8; 32] = URL_SAFE_NO_PAD
1008 .decode(TEST_KEY_ED25519_D)
1009 .unwrap()
1010 .try_into()
1011 .unwrap();
1012 SigningKey::from_bytes(&d)
1013 }
1014
1015 #[test]
1019 fn rfc9421_b_1_4_test_key_ed25519_is_one_key_in_both_encodings() {
1020 let key = test_key();
1021 assert_eq!(encode_public_key(&key.verifying_key()), TEST_KEY_ED25519_X);
1022
1023 let public_der = STANDARD.decode(TEST_KEY_ED25519_PUBLIC_PEM).unwrap();
1024 assert_eq!(
1025 &public_der[public_der.len() - 32..],
1026 key.verifying_key().as_bytes()
1027 );
1028 let private_der = STANDARD.decode(TEST_KEY_ED25519_PRIVATE_PEM).unwrap();
1029 assert_eq!(
1030 &private_der[private_der.len() - 32..],
1031 key.to_bytes().as_slice()
1032 );
1033
1034 assert_eq!(
1035 parse_public_key(TEST_KEY_ED25519_X).unwrap(),
1036 key.verifying_key()
1037 );
1038 }
1039
1040 #[test]
1044 fn rfc9421_b_2_6_signing_a_request_using_ed25519() {
1045 let target = Target {
1047 method: "POST",
1048 authority: &normalize_authority("example.com"),
1049 path: "/foo",
1050 query: Some("param=Value&Pet=dog"),
1051 };
1052 let field = |name: &str| -> Option<String> {
1053 match name {
1054 "date" => Some("Tue, 20 Apr 2021 02:07:55 GMT".into()),
1055 "content-type" => Some("application/json".into()),
1056 "content-length" => Some("18".into()),
1057 _ => None,
1058 }
1059 };
1060
1061 let signature_input = concat!(
1062 r#"sig-b26=("date" "@method" "@path" "@authority" "#,
1063 r#""content-type" "content-length");created=1618884473"#,
1064 r#";keyid="test-key-ed25519""#,
1065 );
1066 let input = SignatureInput::parse(signature_input, "sig-b26").unwrap();
1067 assert_eq!(format!("sig-b26={}", input.serialize()), signature_input);
1070
1071 let base = input
1072 .signature_base(|name| component_value(&target, &field, name))
1073 .unwrap();
1074 let want_base = concat!(
1075 "\"date\": Tue, 20 Apr 2021 02:07:55 GMT\n",
1076 "\"@method\": POST\n",
1077 "\"@path\": /foo\n",
1078 "\"@authority\": example.com\n",
1079 "\"content-type\": application/json\n",
1080 "\"content-length\": 18\n",
1081 r#""@signature-params": ("date" "@method" "@path" "@authority" "#,
1082 r#""content-type" "content-length");created=1618884473"#,
1083 r#";keyid="test-key-ed25519""#,
1084 );
1085 assert_eq!(base, want_base);
1086
1087 let signature_field = concat!(
1088 "sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1",
1089 "u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw==:",
1090 );
1091 let want = parse_signature(signature_field, "sig-b26").unwrap();
1092 assert_eq!(sign(&test_key(), &base), want);
1093 verify(&test_key().verifying_key(), &base, &want).unwrap();
1094
1095 let mut tampered = want.clone();
1096 tampered[0] ^= 1;
1097 assert_eq!(
1098 verify(&test_key().verifying_key(), &base, &tampered),
1099 Err(SignatureError::BadSignature)
1100 );
1101 }
1102
1103 #[test]
1106 fn rfc9530_content_digest_examples() {
1107 assert_eq!(
1108 content_digest(b"{\"hello\": \"world\"}\n"),
1109 "sha-256=:RK/0qy18MlBSVnWgjwz6lZEWjP/lF5HF9bvEF8FabDg=:"
1110 );
1111 assert_eq!(
1112 content_digest(b""),
1113 "sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:"
1114 );
1115 }
1116
1117 #[test]
1121 fn a_digest_without_sha_256_is_named_as_such() {
1122 let field = concat!(
1123 "sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+T",
1124 "aPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:",
1125 );
1126 assert_eq!(
1127 check_content_digest(field, b"{\"hello\": \"world\"}"),
1128 Err(SignatureError::NoDigest)
1129 );
1130 }
1131
1132 const NOW: i64 = 1_790_000_000;
1137
1138 fn target(query: Option<&'static str>) -> Target<'static> {
1139 Target {
1140 method: "GET",
1141 authority: "recall.example.com",
1142 path: "/sync",
1143 query,
1144 }
1145 }
1146
1147 fn headers(signed: &SignedHeaders) -> impl Fn(&str) -> Option<String> + '_ {
1149 move |name: &str| match name {
1150 CONTENT_DIGEST_HEADER => Some(signed.content_digest.clone()),
1151 SIGNATURE_INPUT_HEADER => Some(signed.signature_input.clone()),
1152 SIGNATURE_HEADER => Some(signed.signature.clone()),
1153 crate::PROTOCOL_HEADER => Some("1".to_string()),
1154 _ => None,
1155 }
1156 }
1157
1158 fn check(
1159 signed: &SignedHeaders,
1160 target: &Target<'_>,
1161 body: &[u8],
1162 now: i64,
1163 ) -> Result<(), SignatureError> {
1164 let field = headers(signed);
1165 let input = SignatureInput::parse(&field(SIGNATURE_INPUT_HEADER).unwrap(), LABEL)?;
1166 let signature = parse_signature(&field(SIGNATURE_HEADER).unwrap(), LABEL)?;
1167 let received = Received {
1168 input: &input,
1169 signature: &signature,
1170 target: *target,
1171 field: &field,
1172 };
1173 verify_request(
1174 &received,
1175 body,
1176 &test_key().verifying_key(),
1177 now,
1178 WINDOW_SECONDS,
1179 )
1180 }
1181
1182 #[test]
1183 fn a_signed_request_has_the_documented_shape_and_verifies() {
1184 let t = target(Some("project_key=acme%2Fapp"));
1185 let signed = sign_request(&test_key(), "dev_abc", &t, "1", b"", NOW, "n0nce").unwrap();
1186 assert_eq!(
1187 signed.signature_input,
1188 concat!(
1189 r#"sig1=("@method" "@authority" "@path" "@query" "content-digest" "recall-protocol")"#,
1190 r#";created=1790000000;keyid="dev_abc";nonce="n0nce";alg="ed25519""#,
1191 )
1192 );
1193 assert_eq!(
1194 signed.content_digest,
1195 "sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:"
1196 );
1197 assert!(signed.signature.starts_with("sig1=:") && signed.signature.ends_with(':'));
1198 check(&signed, &t, b"", NOW).unwrap();
1199 }
1200
1201 #[test]
1202 fn every_covered_part_of_the_request_is_bound() {
1203 let body = br#"{"project_key":"acme/app"}"#;
1204 let t = Target {
1205 method: "POST",
1206 authority: "recall.example.com",
1207 path: "/sync",
1208 query: None,
1209 };
1210 let signed = sign_request(&test_key(), "dev_abc", &t, "1", body, NOW, "n").unwrap();
1211 check(&signed, &t, body, NOW).unwrap();
1212
1213 let other = [
1214 Target { method: "PUT", ..t },
1215 Target {
1216 authority: "evil.example.com",
1217 ..t
1218 },
1219 Target {
1220 path: "/admin/stats",
1221 ..t
1222 },
1223 Target {
1224 query: Some("x=1"),
1225 ..t
1226 },
1227 ];
1228 for changed in other {
1229 assert_eq!(
1230 check(&signed, &changed, body, NOW),
1231 Err(SignatureError::BadSignature),
1232 "{changed:?}"
1233 );
1234 }
1235 assert_eq!(
1236 check(&signed, &t, b"{\"project_key\":\"evil\"}", NOW),
1237 Err(SignatureError::DigestMismatch),
1238 "a different body"
1239 );
1240
1241 let field = |name: &str| match name {
1243 crate::PROTOCOL_HEADER => Some("2".to_string()),
1244 other => headers(&signed)(other),
1245 };
1246 let input = SignatureInput::parse(&signed.signature_input, LABEL).unwrap();
1247 let sig = parse_signature(&signed.signature, LABEL).unwrap();
1248 let received = Received {
1249 input: &input,
1250 signature: &sig,
1251 target: t,
1252 field: &field,
1253 };
1254 assert_eq!(
1255 verify_request(
1256 &received,
1257 body,
1258 &test_key().verifying_key(),
1259 NOW,
1260 WINDOW_SECONDS
1261 ),
1262 Err(SignatureError::BadSignature)
1263 );
1264 }
1265
1266 #[test]
1269 fn created_must_be_inside_the_window() {
1270 let t = target(None);
1271 let signed = sign_request(&test_key(), "dev_abc", &t, "1", b"", NOW, "n").unwrap();
1272 check(&signed, &t, b"", NOW + 60).unwrap();
1273 check(&signed, &t, b"", NOW - 5).unwrap();
1274 assert_eq!(
1275 check(&signed, &t, b"", NOW + 61),
1276 Err(SignatureError::Clock {
1277 skew: 61,
1278 window: 60
1279 })
1280 );
1281 assert_eq!(
1282 check(&signed, &t, b"", NOW - 6),
1283 Err(SignatureError::Ahead {
1284 ahead: 6,
1285 allowed: MAX_AHEAD_SECONDS
1286 })
1287 );
1288 for verifier in [NOW - 60, NOW - 3600] {
1289 assert!(matches!(
1290 check(&signed, &t, b"", verifier),
1291 Err(SignatureError::Ahead { .. })
1292 ));
1293 }
1294 assert!(matches!(
1295 check(&signed, &t, b"", NOW + 3600),
1296 Err(SignatureError::Clock { .. })
1297 ));
1298 }
1299
1300 #[test]
1306 fn a_signature_with_a_small_order_r_is_refused() {
1307 use curve25519_dalek::Scalar;
1308 use ed25519_dalek::Verifier;
1309 use sha2::Sha512;
1310
1311 let key = test_key();
1312 let public = key.verifying_key();
1313 let base = "\"@method\": GET";
1314 let mut r = [0u8; 32];
1315 r[0] = 1; let k = Scalar::from_bytes_mod_order_wide(
1317 &Sha512::new()
1318 .chain_update(r)
1319 .chain_update(public.as_bytes())
1320 .chain_update(base.as_bytes())
1321 .finalize()
1322 .into(),
1323 );
1324 let s = k * key.to_scalar();
1325 let mut forged = [0u8; 64];
1326 forged[..32].copy_from_slice(&r);
1327 forged[32..].copy_from_slice(s.as_bytes());
1328
1329 assert!(
1330 public
1331 .verify(base.as_bytes(), &Signature::from_bytes(&forged))
1332 .is_ok(),
1333 "plain verification accepts it, which is the point of the test"
1334 );
1335 assert_eq!(
1336 verify(&public, base, &forged),
1337 Err(SignatureError::BadSignature)
1338 );
1339 }
1340
1341 #[test]
1346 fn a_signature_whose_s_is_not_reduced_is_refused() {
1347 use curve25519_dalek::Scalar;
1348
1349 let l: [u8; 32] = [
1351 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9,
1352 0xde, 0x14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10,
1353 ];
1354 assert_eq!(Scalar::from_bytes_mod_order(l), Scalar::ZERO, "that is L");
1355
1356 let key = test_key();
1357 let base = "\"@method\": GET";
1358 let good = sign(&key, base);
1359 let mut bad = good.clone();
1360 let mut carry = 0u16;
1361 for i in 0..32 {
1362 let sum = u16::from(good[32 + i]) + u16::from(l[i]) + carry;
1363 bad[32 + i] = sum as u8;
1364 carry = sum >> 8;
1365 }
1366 assert_eq!(carry, 0, "S + L fits in 32 bytes");
1367 verify(&key.verifying_key(), base, &good).unwrap();
1368 assert_eq!(
1369 verify(&key.verifying_key(), base, &bad),
1370 Err(SignatureError::BadSignature)
1371 );
1372 }
1373
1374 #[test]
1375 fn another_key_does_not_verify() {
1376 let t = target(None);
1377 let other = SigningKey::from_bytes(&[7; 32]);
1378 let signed = sign_request(&other, "dev_abc", &t, "1", b"", NOW, "n").unwrap();
1379 assert_eq!(
1380 check(&signed, &t, b"", NOW),
1381 Err(SignatureError::BadSignature)
1382 );
1383 }
1384
1385 #[test]
1386 fn the_profile_requires_every_component_and_parameter() {
1387 let full = SignatureInput::recall(NOW, "dev_abc", "n");
1388 full.check_profile(NOW, 60).unwrap();
1389
1390 let mut missing = full.clone();
1391 missing.components.retain(|c| c != "@query");
1392 assert_eq!(
1393 missing.check_profile(NOW, 60),
1394 Err(SignatureError::NotCovered)
1395 );
1396
1397 for (param, want) in [
1398 ("keyid", SignatureError::MissingParameter("keyid")),
1399 ("nonce", SignatureError::MissingParameter("nonce")),
1400 ("created", SignatureError::MissingParameter("created")),
1401 ] {
1402 let mut without = full.clone();
1403 without.params.retain(|(k, _)| k != param);
1404 assert_eq!(without.check_profile(NOW, 60), Err(want), "{param}");
1405 }
1406
1407 let mut no_alg = full.clone();
1409 no_alg.params.retain(|(k, _)| k != "alg");
1410 no_alg.check_profile(NOW, 60).unwrap();
1411 let mut rsa = full.clone();
1412 rsa.params[3].1 = Param::String("rsa-pss-sha512".into());
1413 assert_eq!(
1414 rsa.check_profile(NOW, 60),
1415 Err(SignatureError::Algorithm("rsa-pss-sha512".into()))
1416 );
1417
1418 let long = SignatureInput::recall(NOW, "dev_abc", &"n".repeat(MAX_NONCE_LEN + 1));
1419 assert_eq!(long.check_profile(NOW, 60), Err(SignatureError::Nonce));
1420 let empty = SignatureInput::recall(NOW, "dev_abc", "");
1421 assert_eq!(empty.check_profile(NOW, 60), Err(SignatureError::Nonce));
1422
1423 let mut expired = full.clone();
1424 expired
1425 .params
1426 .push(("expires".into(), Param::Integer(NOW - 1)));
1427 assert_eq!(expired.check_profile(NOW, 60), Err(SignatureError::Expired));
1428 }
1429
1430 #[test]
1431 fn a_component_listed_twice_is_refused() {
1432 let mut twice = SignatureInput::recall(NOW, "dev_abc", "n");
1433 twice.components.push("@method".into());
1434 let t = target(None);
1435 assert_eq!(
1436 twice.signature_base(|n| component_value(&t, &|_| Some("1".into()), n)),
1437 Err(SignatureError::Duplicate("@method".into()))
1438 );
1439 }
1440
1441 #[test]
1442 fn an_unknown_derived_component_or_absent_header_is_refused() {
1443 let t = target(None);
1444 let input = SignatureInput {
1445 components: vec!["@target-uri".into()],
1446 params: vec![],
1447 };
1448 assert_eq!(
1449 input.signature_base(|n| component_value(&t, &|_| None, n)),
1450 Err(SignatureError::MissingComponent("@target-uri".into()))
1451 );
1452 let input = SignatureInput {
1453 components: vec!["recall-protocol".into()],
1454 params: vec![],
1455 };
1456 assert_eq!(
1457 input.signature_base(|n| component_value(&t, &|_| None, n)),
1458 Err(SignatureError::MissingComponent("recall-protocol".into()))
1459 );
1460 }
1461
1462 #[test]
1463 fn query_and_path_follow_rfc9421() {
1464 let t = Target {
1465 method: "GET",
1466 authority: "h",
1467 path: "",
1468 query: None,
1469 };
1470 assert_eq!(t.derived("@path").as_deref(), Some("/"));
1471 assert_eq!(t.derived("@query").as_deref(), Some("?"));
1472 let t = Target {
1473 query: Some("param=value&foo=bar&baz=bat%2Dman"),
1474 ..t
1475 };
1476 assert_eq!(
1478 t.derived("@query").as_deref(),
1479 Some("?param=value&foo=bar&baz=bat%2Dman")
1480 );
1481 }
1482
1483 #[test]
1484 fn authority_is_lowercased_without_a_default_port() {
1485 assert_eq!(
1486 normalize_authority("Recall.Example.COM"),
1487 "recall.example.com"
1488 );
1489 assert_eq!(
1490 normalize_authority("recall.example.com:443"),
1491 "recall.example.com"
1492 );
1493 assert_eq!(normalize_authority("127.0.0.1:80"), "127.0.0.1");
1494 assert_eq!(normalize_authority("127.0.0.1:8787"), "127.0.0.1:8787");
1495 assert_eq!(normalize_authority("[::1]:443"), "[::1]");
1496 }
1497
1498 #[test]
1499 fn public_keys_are_32_bytes_of_base64url_and_never_weak() {
1500 assert!(parse_public_key(TEST_KEY_ED25519_X).is_ok());
1501 for bad in [
1502 "",
1503 "not base64!",
1504 "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs=",
1506 "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0",
1508 ] {
1509 assert_eq!(
1510 parse_public_key(bad),
1511 Err(SignatureError::PublicKey),
1512 "{bad:?}"
1513 );
1514 }
1515 let mut identity = [0u8; 32];
1518 identity[0] = 1;
1519 assert_eq!(
1520 parse_public_key(&URL_SAFE_NO_PAD.encode(identity)),
1521 Err(SignatureError::PublicKey)
1522 );
1523 }
1524
1525 #[test]
1526 fn a_fingerprint_is_the_sha256_of_the_raw_key() {
1527 let key = test_key().verifying_key();
1528 let want = format!(
1529 "SHA256:{}",
1530 STANDARD_NO_PAD.encode(Sha256::digest(key.as_bytes()))
1531 );
1532 assert_eq!(fingerprint(&key), want);
1533 assert!(!fingerprint(&key).contains('='));
1534 }
1535
1536 #[test]
1541 fn dictionaries_parse_per_rfc8941() {
1542 let d = sf::dictionary(r#"a=1, b="x\"y", c=:AQI=:;p, d=(1 "two");q=?0, e"#).unwrap();
1543 assert_eq!(d.len(), 5);
1544 assert_eq!(
1545 d[0],
1546 ("a".into(), sf::Member::Item(sf::Item::Integer(1), vec![]))
1547 );
1548 assert_eq!(
1549 d[1],
1550 (
1551 "b".into(),
1552 sf::Member::Item(sf::Item::String("x\"y".into()), vec![])
1553 )
1554 );
1555 assert_eq!(
1556 d[2],
1557 (
1558 "c".into(),
1559 sf::Member::Item(
1560 sf::Item::Bytes(vec![1, 2]),
1561 vec![("p".into(), sf::Item::Boolean(true))]
1562 )
1563 )
1564 );
1565 assert_eq!(
1566 d[4],
1567 (
1568 "e".into(),
1569 sf::Member::Item(sf::Item::Boolean(true), vec![])
1570 )
1571 );
1572 let d = sf::dictionary("a=1, a=2").unwrap();
1574 assert_eq!(
1575 d,
1576 vec![("a".into(), sf::Member::Item(sf::Item::Integer(2), vec![]))]
1577 );
1578 assert_eq!(
1580 sf::dictionary("a=:AQI:").unwrap()[0].1,
1581 sf::Member::Item(sf::Item::Bytes(vec![1, 2]), vec![])
1582 );
1583
1584 for bad in [
1585 "a=1,",
1586 "A=1",
1587 "a=1.5",
1588 "a=\"unterminated",
1589 "a=\"bad \\x escape\"",
1590 "a=(1 2",
1591 "a=(1\"x\")",
1592 "a=:not base64!:",
1593 "a=?2",
1594 "a=1 b=2",
1595 "a=1234567890123456",
1596 "a=\"é\"",
1597 ] {
1598 assert_eq!(sf::dictionary(bad), None, "{bad:?}");
1599 }
1600 }
1601
1602 #[test]
1603 fn strings_serialize_with_escapes_and_round_trip() {
1604 let input = SignatureInput {
1605 components: vec!["a\"b".into(), "c\\d".into()],
1606 params: vec![
1607 ("keyid".into(), Param::String("k\"1".into())),
1608 ("flag".into(), Param::Boolean(true)),
1609 ("off".into(), Param::Boolean(false)),
1610 ("t".into(), Param::Token("tok/en".into())),
1611 ("b".into(), Param::Bytes(vec![0xff])),
1612 ("created".into(), Param::Integer(-5)),
1613 ],
1614 };
1615 let text = input.serialize();
1616 assert_eq!(
1617 text,
1618 r#"("a\"b" "c\\d");keyid="k\"1";flag;off=?0;t=tok/en;b=:/w==:;created=-5"#
1619 );
1620 assert_eq!(
1621 SignatureInput::parse(&format!("sig1={text}"), "sig1").unwrap(),
1622 input
1623 );
1624 }
1625
1626 #[test]
1627 fn the_label_must_be_sig1_and_components_may_not_carry_parameters() {
1628 let t = format!(
1629 "other={}",
1630 SignatureInput::recall(NOW, "d", "n").serialize()
1631 );
1632 assert_eq!(
1633 SignatureInput::parse(&t, LABEL),
1634 Err(SignatureError::NoLabel)
1635 );
1636 assert_eq!(
1637 parse_signature("other=:AA==:", LABEL),
1638 Err(SignatureError::NoLabel)
1639 );
1640 assert_eq!(
1641 SignatureInput::parse(r#"sig1=("@query-param";name="Pet")"#, LABEL),
1642 Err(SignatureError::Malformed("signature-input"))
1643 );
1644 assert_eq!(
1645 SignatureInput::parse("sig1=:AA==:", LABEL),
1646 Err(SignatureError::Malformed("signature-input"))
1647 );
1648 assert_eq!(
1649 parse_signature("sig1=(\"x\")", LABEL),
1650 Err(SignatureError::Malformed("signature"))
1651 );
1652 }
1653}