Skip to main content

recall_wire/
signature.rs

1//! Signing a request, and checking one: RFC 9421 HTTP Message Signatures
2//! with Ed25519, over an RFC 9530 `Content-Digest` of the body.
3//!
4//! A device holds an Ed25519 key pair it generated; the private key never
5//! leaves it. Every request it sends carries three headers:
6//!
7//! ```text
8//! Content-Digest: sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:
9//! Signature-Input: sig1=("@method" "@authority" "@path" "@query" "content-digest" "recall-protocol");created=1790000000;keyid="dev_…";nonce="…";alg="ed25519"
10//! Signature: sig1=:…:
11//! ```
12//!
13//! No secret travels with the request, so one copied out of a proxy log is
14//! worth nothing once its `created` window has passed, and the nonce stops
15//! it being replayed inside the window.
16//!
17//! # Why these components
18//!
19//! The design sketch covered `@target-uri`. It is the right idea and the
20//! wrong component for this deployment: Traefik terminates TLS, so the
21//! server sees plain HTTP and cannot reconstruct the scheme the client
22//! signed. It does pass `Host` through, so the signature covers the same
23//! URI in the pieces the server can see: `@authority`, `@path` and
24//! `@query`. `content-digest` binds the body, and is always sent and
25//! always covered, a GET carrying the digest of an empty body, so the
26//! server never has to decide whether a request "has" a body.
27//! `recall-protocol` is covered so the protocol a request claims cannot be
28//! changed in transit.
29//!
30//! # What is implemented
31//!
32//! Only the subset Recall sends: one signature per request, labelled
33//! [`LABEL`]; covered components that are plain strings, with no component
34//! parameters; the four derived components above; and header fields by
35//! name. Structured-field parsing follows RFC 8941 for the types those
36//! headers use. Anything else fails to parse, and the server refuses a
37//! request whose signature headers do not parse with a 401 that names the
38//! header, rather than falling back to treating it as unsigned.
39//!
40//! Ed25519 is `ed25519-dalek`, which is pure Rust: the server is built
41//! static against musl and links no C crypto library.
42
43use 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
51/// The header carrying the body's digest (RFC 9530).
52pub const CONTENT_DIGEST_HEADER: &str = "content-digest";
53
54/// The header describing what a signature covers (RFC 9421 §4.1).
55pub const SIGNATURE_INPUT_HEADER: &str = "signature-input";
56
57/// The header carrying the signature itself (RFC 9421 §4.2).
58pub const SIGNATURE_HEADER: &str = "signature";
59
60/// The label Recall signs under, and the only one the server reads.
61pub const LABEL: &str = "sig1";
62
63/// The one algorithm Recall signs with, by its RFC 9421 registry name.
64pub const ALGORITHM: &str = "ed25519";
65
66/// What every Recall signature covers, in the order the client signs them.
67/// The server requires all of them and accepts them in any order.
68pub const COVERED_COMPONENTS: [&str; 6] = [
69    "@method",
70    "@authority",
71    "@path",
72    "@query",
73    CONTENT_DIGEST_HEADER,
74    crate::PROTOCOL_HEADER,
75];
76
77/// How far `created` may be behind the server's clock, in seconds. The
78/// server also remembers every nonce for this long after its `created`.
79pub const WINDOW_SECONDS: u64 = 60;
80
81/// How far `created` may be ahead of the server's clock, in seconds: a
82/// little, for a client whose clock runs slightly fast, and no more.
83///
84/// It is small because a server remembers nonces only in memory. A
85/// signature dated ahead of the clock outlives the process that accepted
86/// it by as much as it was ahead, and the process after it cannot tell
87/// the replay from the first sending; so a server that has just started
88/// refuses every signature dated up to this far past its start, and the
89/// more this is, the longer after each start that lasts.
90pub const MAX_AHEAD_SECONDS: u64 = 5;
91
92/// The longest nonce the server accepts. It keeps each one in memory for
93/// [`WINDOW_SECONDS`], so it is bounded.
94pub const MAX_NONCE_LEN: usize = 128;
95
96/// Why a signature, or something it depends on, was not accepted. The
97/// messages are safe to show a user and name what to fix.
98#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
99pub enum SignatureError {
100    /// A header was present but did not parse.
101    #[error("malformed {0} header")]
102    Malformed(&'static str),
103    /// `Signature-Input` or `Signature` has no member labelled [`LABEL`].
104    #[error("no signature labelled sig1")]
105    NoLabel,
106    /// A parameter the server requires is absent.
107    #[error("the signature has no {0} parameter")]
108    MissingParameter(&'static str),
109    /// A component Recall requires is not covered.
110    #[error(
111        "the signature must cover @method, @authority, @path, @query, content-digest and recall-protocol"
112    )]
113    NotCovered,
114    /// A component is listed twice (RFC 9421 §2.5, step 2.1).
115    #[error("the signature covers {0} twice")]
116    Duplicate(String),
117    /// A covered component has no value in this request.
118    #[error("the signature covers {0}, which this request does not have")]
119    MissingComponent(String),
120    /// `alg` names something other than [`ALGORITHM`].
121    #[error("unsupported signature algorithm {0:?}")]
122    Algorithm(String),
123    /// The nonce is empty or longer than [`MAX_NONCE_LEN`].
124    #[error("the nonce must be 1 to 128 characters")]
125    Nonce,
126    /// `created` is further behind the server's clock than the window.
127    #[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        /// How far off it was, in seconds.
132        skew: u64,
133        /// How far off it may be.
134        window: u64,
135    },
136    /// `created` is further ahead of the server's clock than
137    /// [`MAX_AHEAD_SECONDS`].
138    #[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        /// How far ahead it was, in seconds.
143        ahead: u64,
144        /// How far ahead it may be.
145        allowed: u64,
146    },
147    /// `expires` has passed.
148    #[error("the signature has expired")]
149    Expired,
150    /// `Content-Digest` has no `sha-256` member.
151    #[error("content-digest has no sha-256 value")]
152    NoDigest,
153    /// `Content-Digest` does not match the body received.
154    #[error("content-digest does not match the body")]
155    DigestMismatch,
156    /// The signature base holds something that is not ASCII (§2.5, step 4).
157    #[error("the signature base is not ASCII")]
158    NotAscii,
159    /// The signature does not verify with the device's key.
160    #[error("the signature does not verify")]
161    BadSignature,
162    /// A public key is not an acceptable Ed25519 key.
163    #[error("public_key must be an Ed25519 public key: 32 bytes, base64url without padding")]
164    PublicKey,
165}
166
167// ---------------------------------------------------------------------------
168// keys
169// ---------------------------------------------------------------------------
170
171/// A public key as it travels in JSON: the raw 32 bytes, base64url, no
172/// padding.
173pub fn encode_public_key(key: &VerifyingKey) -> String {
174    URL_SAFE_NO_PAD.encode(key.as_bytes())
175}
176
177/// Reads a public key sent as [`encode_public_key`] writes it.
178///
179/// A key of small order is refused: every signature "verifies" under one,
180/// so accepting it would let whoever enrolled it sign as anyone holding it.
181pub 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
193/// What a person compares to confirm two screens show the same key:
194/// `SHA256:` and the unpadded base64 of the key's SHA-256, the shape
195/// `ssh-keygen -l` prints.
196pub fn fingerprint(key: &VerifyingKey) -> String {
197    format!(
198        "SHA256:{}",
199        STANDARD_NO_PAD.encode(Sha256::digest(key.as_bytes()))
200    )
201}
202
203// ---------------------------------------------------------------------------
204// Content-Digest (RFC 9530)
205// ---------------------------------------------------------------------------
206
207/// The `Content-Digest` value for `body`: its SHA-256 as a structured-field
208/// byte sequence. An empty body has a digest too, and a GET sends it.
209pub fn content_digest(body: &[u8]) -> String {
210    format!("sha-256=:{}:", STANDARD.encode(Sha256::digest(body)))
211}
212
213/// Checks a received `Content-Digest` against the body that arrived.
214///
215/// Only `sha-256` is read. Other algorithms may be present and are ignored,
216/// as RFC 9530 §2 allows a recipient to.
217pub 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
225/// The `sha-256` value a `Content-Digest` carries.
226fn 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// ---------------------------------------------------------------------------
238// the request a signature is about
239// ---------------------------------------------------------------------------
240
241/// The parts of a request the derived components come from.
242#[derive(Debug, Clone, Copy, PartialEq, Eq)]
243pub struct Target<'a> {
244    /// The method, as sent: `GET`, `POST`.
245    pub method: &'a str,
246    /// Host and port, already through [`normalize_authority`].
247    pub authority: &'a str,
248    /// The path, percent-encoding untouched, without the query.
249    pub path: &'a str,
250    /// The query string without its `?`, as sent; [`None`] when there is
251    /// none.
252    pub query: Option<&'a str>,
253}
254
255impl Target<'_> {
256    /// A derived component's value, or [`None`] for one Recall does not
257    /// implement.
258    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            // RFC 9421 §2.2.6: an empty path is a single slash.
263            "@path" => Some(if self.path.is_empty() {
264                "/".to_string()
265            } else {
266                self.path.to_string()
267            }),
268            // §2.2.7: the leading "?" is part of the value, and a request
269            // with no query covers "?" alone.
270            "@query" => Some(format!("?{}", self.query.unwrap_or(""))),
271            _ => None,
272        }
273    }
274}
275
276/// The `@authority` value both sides compute: host and port, lowercased,
277/// without a default port (RFC 9421 §2.2.3).
278///
279/// The server cannot tell which port was the default, because behind a
280/// TLS-terminating proxy it never learns the scheme. It drops both `:80`
281/// and `:443`, and so does the client, so the two agree whatever the
282/// scheme; a server reachable on 443 and 80 at one host is one server.
283pub 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// ---------------------------------------------------------------------------
294// Signature-Input
295// ---------------------------------------------------------------------------
296
297/// A value in a signature's parameters.
298#[derive(Debug, Clone, PartialEq, Eq)]
299pub enum Param {
300    /// An integer, such as `created`.
301    Integer(i64),
302    /// A string, such as `keyid`.
303    String(String),
304    /// A token.
305    Token(String),
306    /// A boolean; a parameter with no value is `true`.
307    Boolean(bool),
308    /// A byte sequence.
309    Bytes(Vec<u8>),
310}
311
312/// One signature's covered components and parameters: a
313/// `Signature-Input` member, and the `@signature-params` line of the
314/// signature base.
315///
316/// The order of both lists is kept exactly as received, because the
317/// signature base re-serializes them and any other order is a different
318/// base (RFC 9421 §2.3).
319#[derive(Debug, Clone, PartialEq, Eq)]
320pub struct SignatureInput {
321    /// Component names, in order.
322    pub components: Vec<String>,
323    /// Parameters, in order.
324    pub params: Vec<(String, Param)>,
325}
326
327impl SignatureInput {
328    /// What the Recall client signs: [`COVERED_COMPONENTS`], then
329    /// `created`, `keyid`, `nonce` and `alg`, in that order.
330    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    /// Reads the member labelled `label` from a `Signature-Input` value.
343    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            // Component parameters (`;req`, `;key=`, `;sf`...) are not
354            // part of what Recall signs.
355            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    /// The inner list, with parameters, exactly as it appears after
367    /// `sig1=` and after `"@signature-params": `.
368    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    /// `created`, as a UNIX time.
424    pub fn created(&self) -> Option<i64> {
425        self.integer_param("created")
426    }
427
428    /// `keyid`: for Recall, the device id.
429    pub fn keyid(&self) -> Option<&str> {
430        self.string_param("keyid")
431    }
432
433    /// `nonce`.
434    pub fn nonce(&self) -> Option<&str> {
435        self.string_param("nonce")
436    }
437
438    /// `alg`, when present.
439    pub fn alg(&self) -> Option<&str> {
440        self.string_param("alg")
441    }
442
443    /// Builds the signature base (RFC 9421 §2.5), taking each covered
444    /// component's value from `value`.
445    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            // A value spanning lines would forge a line of its own.
458            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    /// Checks the parameters and coverage against what Recall requires of
475    /// every signature, given the verifier's clock as a UNIX time.
476    /// `created` may be up to `window` seconds behind that clock, and up
477    /// to [`MAX_AHEAD_SECONDS`] ahead of it.
478    pub fn check_profile(&self, now: i64, window: u64) -> Result<(), SignatureError> {
479        // RFC 9421 §3.2 step 6.5: an algorithm named in the signature has
480        // to agree with the key's. The key is Ed25519, so only that name
481        // may appear; leaving it out is allowed, the key decides.
482        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
538/// Reads the signature labelled `label` from a `Signature` value.
539pub 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
548// ---------------------------------------------------------------------------
549// signing and verifying
550// ---------------------------------------------------------------------------
551
552/// Signs a signature base with Ed25519 (RFC 9421 §3.3.6).
553pub fn sign(key: &SigningKey, base: &str) -> Vec<u8> {
554    key.sign(base.as_bytes()).to_bytes().to_vec()
555}
556
557/// Verifies an Ed25519 signature over a signature base.
558///
559/// Strict verification: it also refuses the non-canonical encodings plain
560/// RFC 8032 verification lets through, so one request cannot be given two
561/// valid signatures.
562pub 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
570/// A covered component's value: derived ones from `target`, anything else
571/// from the header of that name through `field`.
572fn 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/// The three headers a signed request carries, as [`sign_request`] makes
585/// them.
586#[derive(Debug, Clone, PartialEq, Eq)]
587pub struct SignedHeaders {
588    /// For [`CONTENT_DIGEST_HEADER`].
589    pub content_digest: String,
590    /// For [`SIGNATURE_INPUT_HEADER`].
591    pub signature_input: String,
592    /// For [`SIGNATURE_HEADER`].
593    pub signature: String,
594}
595
596/// Signs a request the way the Recall client does: [`COVERED_COMPONENTS`]
597/// over `target`, with `protocol` as the `Recall-Protocol` header the
598/// request also sends, and the digest of `body`.
599///
600/// `created` is the current UNIX time and `nonce` a fresh random string
601/// that is never reused; both are the caller's so this stays
602/// deterministic.
603pub 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
627/// A signed request's headers as they arrived: everything
628/// [`verify_headers`] reads.
629pub struct Received<'a> {
630    /// The `Signature-Input` member labelled [`LABEL`].
631    pub input: &'a SignatureInput,
632    /// The `Signature` member labelled [`LABEL`].
633    pub signature: &'a [u8],
634    /// Where the derived components come from.
635    pub target: Target<'a>,
636    /// A header's value by lowercase name, with repeated headers joined by
637    /// `", "` (RFC 9110 §5.3).
638    pub field: &'a dyn Fn(&str) -> Option<String>,
639}
640
641/// Everything about a signed request that can be checked from its headers
642/// alone, given the verifier's clock as a UNIX time: the profile
643/// ([`SignatureInput::check_profile`]), a `Content-Digest` with a sha-256
644/// value, and the signature against `key`.
645///
646/// The signature covers the `Content-Digest` header, not the body, so it
647/// can be verified before a byte of the body is read: a server need only
648/// read a body for a request its device really signed. The body must then
649/// be checked against the digest with [`check_content_digest`], or the
650/// signature proves nothing about it; [`verify_request`] does both.
651///
652/// What needs the server's state, which key a `keyid` names and whether a
653/// nonce was already used, is the caller's.
654pub 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
663/// [`verify_headers`], but also hands back the exact signature base that
664/// verified: RFC 9421 §2.5's bytes, ASCII, one covered component per line.
665/// An audit leaf keeps this alongside the signature itself, so the leaf
666/// alone — no live request, no replay of `@method`/`@path`/`@query` — is
667/// enough to check that the device really signed it.
668pub 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
685/// The `sha-256` value a `Content-Digest` header carries, re-encoded as
686/// standard base64 rather than the structured-field byte sequence it
687/// arrived in — what an audit leaf's `request.body_sha256` records.
688pub fn content_digest_base64(field: &str) -> Result<String, SignatureError> {
689    Ok(STANDARD.encode(sha256_of(field)?))
690}
691
692/// [`verify_headers`], then the body against `Content-Digest`: the whole
693/// of what can be checked without state.
694pub 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
706// ---------------------------------------------------------------------------
707// structured fields (RFC 8941), the part these headers use
708// ---------------------------------------------------------------------------
709
710mod sf {
711    //! Parsing Dictionaries of Items and Inner Lists, per RFC 8941 §4.2.
712    //! Decimals are not supported: nothing Recall reads uses them, and a
713    //! field that fails to parse is ignored as a whole (§4.2).
714
715    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    /// §4.1.6: a string, quoted, with `"` and `\` escaped.
735    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    /// §4.2 with §4.2.2: `None` for anything that does not parse.
752    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            // §4.2.2 step 2.4: a later duplicate replaces the earlier one.
770            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        /// §4.2.3.3.
820        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        /// §4.2.1.1.
836        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        /// §4.2.3.2.
859        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        /// §4.2.3.1.
878        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        /// §4.2.4, integers only.
890        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        /// §4.2.5.
905        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        /// §4.2.6.
927        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        /// §4.2.7. Padding is optional on the way in, as the RFC asks of
943        /// parsers.
944        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        /// §4.2.8.
966        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    // -----------------------------------------------------------------------
984    // Known answers from the RFCs
985    //
986    // RFC 9421, 9530 and 8941 were read from the plain-text copies vendored
987    // in https://github.com/mnot/rfc-refs (rfcs/rfc9421.txt, rfc9530.txt,
988    // rfc8941.txt at commit b4d8a7b2e544c78c754275f08535d06234bbd9c2), and
989    // RFC 9421's appendix was cross-checked against the working group's
990    // source for it, https://github.com/httpwg/http-extensions
991    // archive/draft-ietf-httpbis-message-signatures.md at commit
992    // f66f269dfd744e778f53fb38ce96ce04a1323fff. rfc-editor.org itself was
993    // not reachable from where these were written. Lines the RFC wraps
994    // under RFC 8792 ('\' at the end of a line) are joined here.
995    // -----------------------------------------------------------------------
996
997    /// RFC 9421 Appendix B.1.4, `test-key-ed25519`, from its JWK form.
998    const TEST_KEY_ED25519_D: &str = "n4Ni-HpISpVObnQMW0wOhCKROaIKqKtW_2ZYb2p9KcU";
999    const TEST_KEY_ED25519_X: &str = "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs";
1000    /// The same key's PKCS #8 PEM bodies, from the same appendix.
1001    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    /// B.1.4: the JWK's `d` is the private key whose public half is `x`,
1016    /// and both agree with the PEM encodings of the same key: the raw key
1017    /// is the last 32 bytes of each DER body.
1018    #[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    /// B.2.6: "Signing a Request Using ed25519". Ed25519 is deterministic,
1041    /// so signing the same base with the same key must give exactly the
1042    /// RFC's bytes, not merely something that verifies.
1043    #[test]
1044    fn rfc9421_b_2_6_signing_a_request_using_ed25519() {
1045        // The test-request of Appendix B.2.
1046        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        // Parsing and serializing again is the identity: the
1068        // @signature-params line is rebuilt from what was parsed.
1069        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    /// RFC 9530 Appendix B.1 and B.2: `{"hello": "world"}` and a line
1104    /// feed, and empty content.
1105    #[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    /// RFC 9421 Appendix B.2's test-request carries a sha-512 digest only.
1118    /// It parses, and is refused for having no sha-256 rather than
1119    /// mistaken for a mismatch.
1120    #[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    // -----------------------------------------------------------------------
1133    // Recall's profile
1134    // -----------------------------------------------------------------------
1135
1136    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    /// Headers as a server would read them from the signed request.
1148    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        // The protocol header is covered too.
1242        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    /// A minute behind the verifier's clock, and only a few seconds ahead
1267    /// of it.
1268    #[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    /// Strict verification, not plain RFC 8032 (the review's mutation M5,
1301    /// `verify_strict` to `verify`). A signature whose R is the identity, a
1302    /// point of small order, satisfies the plain equation when S = k·a, so
1303    /// anyone who can make one verifies without it being what a signer
1304    /// produces. Plain verification accepts it; this must not.
1305    #[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; // the identity point, compressed
1316        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    /// A signature whose S is not reduced below the group order L is the
1342    /// same equation in a second encoding, so one request would have two
1343    /// valid signatures. ed25519-dalek refuses it in both its plain and
1344    /// strict checks; this pins that it stays refused.
1345    #[test]
1346    fn a_signature_whose_s_is_not_reduced_is_refused() {
1347        use curve25519_dalek::Scalar;
1348
1349        // L = 2^252 + 27742317777372353535851937790883648493, little-endian.
1350        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        // alg may be left out, the key decides; but it may not disagree.
1408        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        // §2.2.7's own example: percent-encoding is left alone.
1477        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            // Padded, which the wire format does not use.
1505            "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs=",
1506            // 31 bytes.
1507            "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0",
1508        ] {
1509            assert_eq!(
1510                parse_public_key(bad),
1511                Err(SignatureError::PublicKey),
1512                "{bad:?}"
1513            );
1514        }
1515        // The identity point has small order: every signature would
1516        // verify under it.
1517        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    // -----------------------------------------------------------------------
1537    // structured-field parsing
1538    // -----------------------------------------------------------------------
1539
1540    #[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        // A later duplicate replaces the earlier one.
1573        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        // Padding is optional on a byte sequence.
1579        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}