Skip to main content

wimsey_httpsig/
signature.rs

1//! RFC 9421 signature parameters, signature-base construction, signing and
2//! verification.
3
4use std::fmt::Write as _;
5
6use base64::{engine::general_purpose::STANDARD, Engine};
7use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
8
9use crate::error::HttpSigError;
10use crate::message::{Component, HttpRequest};
11
12/// The signature algorithm name this crate emits and accepts (RFC 9421
13/// Section 3.3.6).
14///
15/// The WIMSE profile forbids the `alg` parameter outright — the algorithm is
16/// pinned by the `cnf` JWK in the WIT — so this is only used when the crate is
17/// driven as a plain RFC 9421 implementation.
18pub const ALG: &str = "ed25519";
19
20/// The `tag` value identifying a WIMSE workload-to-workload signature.
21pub const WIMSE_TAG: &str = "wimse-workload-to-workload";
22
23/// The signature label the draft recommends when a message carries a single
24/// signature.
25pub const WIMSE_LABEL: &str = "wimse";
26
27/// RFC 9421 signature parameters, serialized after the covered-component list.
28///
29/// The last three are the signature metadata parameters registered by
30/// `draft-ietf-wimse-http-signature`; the rest are the RFC 9421 originals. Field
31/// order is the serialization order, which keeps a signature base reproducible.
32#[derive(Debug, Clone, Default, PartialEq, Eq)]
33pub struct SignatureParams {
34    /// Creation time, in seconds since the Unix epoch (`created`).
35    pub created: Option<u64>,
36    /// Expiry time, in seconds since the Unix epoch (`expires`).
37    pub expires: Option<u64>,
38    /// The key identifier (`keyid`). Forbidden by the WIMSE profile.
39    pub keyid: Option<String>,
40    /// The signature algorithm (`alg`). Forbidden by the WIMSE profile.
41    pub alg: Option<String>,
42    /// A unique nonce (`nonce`).
43    pub nonce: Option<String>,
44    /// An application-specific tag (`tag`); [`WIMSE_TAG`] under the profile.
45    pub tag: Option<String>,
46    /// The audience the request is intended for (`wimse-aud`). Required on a
47    /// WIMSE request signature.
48    pub wimse_aud: Option<String>,
49    /// Whether the client requires the response to be signed
50    /// (`wimse-sign-response`). A Boolean parameter: `true` serializes as a bare
51    /// parameter name, per RFC 8941 Section 4.1.1.2.
52    pub wimse_sign_response: Option<bool>,
53    /// On a response signature, the `nonce` from the request being answered
54    /// (`wimse-req-nonce`), which binds the response to that request.
55    pub wimse_req_nonce: Option<String>,
56}
57
58/// The header field values produced by [`sign`].
59#[derive(Debug, Clone)]
60pub struct SignedSignature {
61    /// The `Signature-Input` field value (for example `sig1=(...);created=...`).
62    pub signature_input: String,
63    /// The `Signature` field value (for example `sig1=:<base64>:`).
64    pub signature: String,
65}
66
67/// The outcome of a successful [`verify`].
68#[derive(Debug, Clone)]
69pub struct VerifiedSignature {
70    /// The signature label.
71    pub label: String,
72    /// The covered components, in order.
73    pub components: Vec<Component>,
74    /// The parsed signature parameters.
75    pub params: SignatureParams,
76}
77
78/// Options controlling [`verify`].
79///
80/// A bare successful [`verify`] proves only that *some* set of components was
81/// signed with the key. To bind the request, set `required_components` to the
82/// components that must be covered — under the WIMSE profile that is `@method`
83/// and `@request-target`, plus `content-type`, `content-digest`,
84/// `authorization`, `txn-token` and `workload-identity-token` whenever the
85/// message carries them.
86#[derive(Debug, Clone, Default)]
87pub struct VerifyConfig {
88    /// The current time, in seconds since the Unix epoch. When set, `created`
89    /// and `expires` are checked against it.
90    pub now: Option<u64>,
91    /// Clock-skew tolerance, in seconds.
92    pub leeway: u64,
93    /// If set, only this signature label is accepted.
94    pub label: Option<String>,
95    /// Components that MUST be covered by the signature; verification fails if
96    /// any is absent.
97    pub required_components: Vec<Component>,
98    /// If set (together with `now`), the signature's `created` must be present
99    /// and within this many seconds of `now`.
100    pub max_age: Option<u64>,
101    /// Enforce the WIMSE request-signature profile on the received parameters:
102    /// `created`, `expires`, `nonce`, `tag` and `wimse-aud` must all be present,
103    /// `tag` must be [`WIMSE_TAG`], and `keyid` and `alg` must be absent.
104    ///
105    /// Off by default so the crate can also be driven as a plain RFC 9421
106    /// implementation.
107    pub wimse_profile: bool,
108    /// If set, the signature's `wimse-aud` must equal this value. A signature
109    /// is only bound to *this* service if the audience it names is checked.
110    pub expected_audience: Option<String>,
111}
112
113/// Errors unless `params` satisfies the WIMSE profile for a **request**
114/// signature (Section 3 of `draft-ietf-wimse-http-signature`).
115///
116/// The profile makes `created`, `expires`, `nonce` and `tag` mandatory on every
117/// message and `wimse-aud` mandatory on requests, and forbids `keyid` and `alg`
118/// — the signing key travels in the WIT and the algorithm is pinned by that
119/// WIT's `cnf` JWK, so repeating either here would only invite confusion.
120///
121/// # Errors
122///
123/// Returns [`HttpSigError::MissingParameter`], [`HttpSigError::ForbiddenParameter`]
124/// or [`HttpSigError::WrongTag`] for the first rule the parameters break.
125pub fn check_request_profile(params: &SignatureParams) -> Result<(), HttpSigError> {
126    if params.keyid.is_some() {
127        return Err(HttpSigError::ForbiddenParameter("keyid"));
128    }
129    if params.alg.is_some() {
130        return Err(HttpSigError::ForbiddenParameter("alg"));
131    }
132    if params.created.is_none() {
133        return Err(HttpSigError::MissingParameter("created"));
134    }
135    if params.expires.is_none() {
136        return Err(HttpSigError::MissingParameter("expires"));
137    }
138    if params.nonce.is_none() {
139        return Err(HttpSigError::MissingParameter("nonce"));
140    }
141    match params.tag.as_deref() {
142        None => return Err(HttpSigError::MissingParameter("tag")),
143        Some(tag) if tag != WIMSE_TAG => {
144            return Err(HttpSigError::WrongTag {
145                found: tag.to_owned(),
146            })
147        }
148        Some(_) => {}
149    }
150    if params.wimse_aud.is_none() {
151        return Err(HttpSigError::MissingParameter("wimse-aud"));
152    }
153    Ok(())
154}
155
156fn sf_string(value: &str) -> String {
157    let mut out = String::with_capacity(value.len() + 2);
158    out.push('"');
159    for c in value.chars() {
160        if c == '\\' || c == '"' {
161            out.push('\\');
162        }
163        out.push(c);
164    }
165    out.push('"');
166    out
167}
168
169fn serialize_params_value(components: &[Component], params: &SignatureParams) -> String {
170    let inner = components
171        .iter()
172        .map(Component::quoted_id)
173        .collect::<Vec<_>>()
174        .join(" ");
175    let mut s = format!("({inner})");
176    if let Some(created) = params.created {
177        let _ = write!(s, ";created={created}");
178    }
179    if let Some(expires) = params.expires {
180        let _ = write!(s, ";expires={expires}");
181    }
182    if let Some(keyid) = &params.keyid {
183        let _ = write!(s, ";keyid={}", sf_string(keyid));
184    }
185    if let Some(alg) = &params.alg {
186        let _ = write!(s, ";alg={}", sf_string(alg));
187    }
188    if let Some(nonce) = &params.nonce {
189        let _ = write!(s, ";nonce={}", sf_string(nonce));
190    }
191    if let Some(tag) = &params.tag {
192        let _ = write!(s, ";tag={}", sf_string(tag));
193    }
194    if let Some(aud) = &params.wimse_aud {
195        let _ = write!(s, ";wimse-aud={}", sf_string(aud));
196    }
197    if let Some(sign_response) = params.wimse_sign_response {
198        // RFC 8941 Section 4.1.1.2: a Boolean `true` parameter MUST omit its
199        // value, so it serializes as the bare parameter name.
200        if sign_response {
201            s.push_str(";wimse-sign-response");
202        } else {
203            s.push_str(";wimse-sign-response=?0");
204        }
205    }
206    if let Some(req_nonce) = &params.wimse_req_nonce {
207        let _ = write!(s, ";wimse-req-nonce={}", sf_string(req_nonce));
208    }
209    s
210}
211
212fn signature_base_from_params_str(
213    request: &HttpRequest,
214    components: &[Component],
215    params_value: &str,
216) -> Result<String, HttpSigError> {
217    // The received parameter substring is untrusted; a bare CR or LF in it would
218    // forge extra signature-base lines.
219    if params_value.contains(['\r', '\n']) {
220        return Err(HttpSigError::Parse(
221            "signature parameters contain CR or LF".to_owned(),
222        ));
223    }
224    let mut base = String::new();
225    for component in components {
226        let value = request.component_value(component)?;
227        // A bare CR or LF in a value would forge extra signature-base lines.
228        if value.contains(['\r', '\n']) {
229            return Err(HttpSigError::InvalidComponentValue(component.quoted_id()));
230        }
231        base.push_str(&component.quoted_id());
232        base.push_str(": ");
233        base.push_str(&value);
234        base.push('\n');
235    }
236    base.push_str("\"@signature-params\": ");
237    base.push_str(params_value);
238    Ok(base)
239}
240
241/// Builds the RFC 9421 signature base for `request` over `components` with
242/// `params`.
243///
244/// # Errors
245///
246/// Returns [`HttpSigError::MissingComponent`] if a covered header is absent.
247pub fn signature_base(
248    request: &HttpRequest,
249    components: &[Component],
250    params: &SignatureParams,
251) -> Result<String, HttpSigError> {
252    let params_value = serialize_params_value(components, params);
253    signature_base_from_params_str(request, components, &params_value)
254}
255
256/// Signs `request` over `components`, producing `Signature-Input` and
257/// `Signature` field values under `label`.
258///
259/// # Errors
260///
261/// Returns [`HttpSigError::MissingComponent`] if a covered header is absent.
262pub fn sign(
263    request: &HttpRequest,
264    components: &[Component],
265    params: &SignatureParams,
266    label: &str,
267    signing_key: &SigningKey,
268) -> Result<SignedSignature, HttpSigError> {
269    let params_value = serialize_params_value(components, params);
270    let base = signature_base_from_params_str(request, components, &params_value)?;
271    let signature: Signature = signing_key.sign(base.as_bytes());
272    Ok(SignedSignature {
273        signature_input: format!("{label}={params_value}"),
274        signature: format!("{label}=:{}:", STANDARD.encode(signature.to_bytes())),
275    })
276}
277
278/// Splits a single-member dictionary field value `label=rest` at the first `=`.
279fn split_member(value: &str) -> Result<(&str, &str), HttpSigError> {
280    let value = value.trim();
281    let eq = value
282        .find('=')
283        .ok_or_else(|| HttpSigError::Parse("missing `=` in dictionary member".to_owned()))?;
284    let label = value[..eq].trim();
285    if label.is_empty() {
286        return Err(HttpSigError::Parse("empty signature label".to_owned()));
287    }
288    Ok((label, &value[eq + 1..]))
289}
290
291fn parse_sf_string(token: &str) -> Result<String, HttpSigError> {
292    let inner = token
293        .strip_prefix('"')
294        .and_then(|t| t.strip_suffix('"'))
295        .ok_or_else(|| HttpSigError::Parse(format!("not a string: {token}")))?;
296    let mut out = String::with_capacity(inner.len());
297    let mut chars = inner.chars();
298    while let Some(c) = chars.next() {
299        if c == '\\' {
300            match chars.next() {
301                Some(next @ ('\\' | '"')) => out.push(next),
302                _ => return Err(HttpSigError::Parse("bad string escape".to_owned())),
303            }
304        } else {
305            out.push(c);
306        }
307    }
308    Ok(out)
309}
310
311/// The byte index of the first unescaped, unquoted `target` in `s`, respecting
312/// RFC 8941 string quoting so a delimiter inside a `"..."` value is skipped.
313fn find_unquoted(s: &str, target: char) -> Option<usize> {
314    let mut in_quotes = false;
315    let mut escaped = false;
316    for (idx, c) in s.char_indices() {
317        if escaped {
318            escaped = false;
319        } else if in_quotes && c == '\\' {
320            escaped = true;
321        } else if c == '"' {
322            in_quotes = !in_quotes;
323        } else if c == target && !in_quotes {
324            return Some(idx);
325        }
326    }
327    None
328}
329
330/// Splits `s` on unquoted `;`, keeping delimiters inside `"..."` values intact.
331fn split_unquoted_semicolons(s: &str) -> Vec<&str> {
332    let mut parts = Vec::new();
333    let mut start = 0;
334    let mut in_quotes = false;
335    let mut escaped = false;
336    for (idx, c) in s.char_indices() {
337        if escaped {
338            escaped = false;
339        } else if in_quotes && c == '\\' {
340            escaped = true;
341        } else if c == '"' {
342            in_quotes = !in_quotes;
343        } else if c == ';' && !in_quotes {
344            parts.push(&s[start..idx]);
345            start = idx + 1;
346        }
347    }
348    parts.push(&s[start..]);
349    parts
350}
351
352fn parse_params(rest: &str, params: &mut SignatureParams) -> Result<(), HttpSigError> {
353    for part in split_unquoted_semicolons(rest) {
354        let part = part.trim();
355        if part.is_empty() {
356            continue;
357        }
358        let Some((name, raw)) = part.split_once('=') else {
359            // A valueless parameter is Boolean `true`, per RFC 8941
360            // Section 4.1.1.2. Unknown ones are still ignored.
361            if part == "wimse-sign-response" {
362                params.wimse_sign_response = Some(true);
363            }
364            continue;
365        };
366        let name = name.trim();
367        let raw = raw.trim();
368        match name {
369            "created" => {
370                params.created = Some(parse_int(raw)?);
371            }
372            "expires" => {
373                params.expires = Some(parse_int(raw)?);
374            }
375            "keyid" => params.keyid = Some(parse_sf_string(raw)?),
376            "alg" => params.alg = Some(parse_sf_string(raw)?),
377            "nonce" => params.nonce = Some(parse_sf_string(raw)?),
378            "tag" => params.tag = Some(parse_sf_string(raw)?),
379            "wimse-aud" => params.wimse_aud = Some(parse_sf_string(raw)?),
380            "wimse-sign-response" => params.wimse_sign_response = Some(parse_sf_boolean(raw)?),
381            "wimse-req-nonce" => params.wimse_req_nonce = Some(parse_sf_string(raw)?),
382            // Unknown parameters are ignored, per structured-field extensibility.
383            _ => {}
384        }
385    }
386    Ok(())
387}
388
389/// Parses an RFC 8941 Boolean (`?0` or `?1`) given explicitly.
390fn parse_sf_boolean(raw: &str) -> Result<bool, HttpSigError> {
391    match raw {
392        "?1" => Ok(true),
393        "?0" => Ok(false),
394        other => Err(HttpSigError::Parse(format!("not a boolean: {other}"))),
395    }
396}
397
398fn parse_int(raw: &str) -> Result<u64, HttpSigError> {
399    raw.trim()
400        .parse()
401        .map_err(|_| HttpSigError::Parse(format!("not an integer: {raw}")))
402}
403
404/// Parses a `Signature-Input` field value into its label, covered components,
405/// parameters, and the verbatim parameters substring used in the base.
406fn parse_signature_input(
407    value: &str,
408) -> Result<(String, Vec<Component>, SignatureParams, String), HttpSigError> {
409    let (label, rest) = split_member(value)?;
410    let rest = rest.trim();
411    if !rest.starts_with('(') {
412        return Err(HttpSigError::Parse(
413            "inner list must start with `(`".to_owned(),
414        ));
415    }
416    // Find the inner list's closing `)`, ignoring any `)` inside a quoted value.
417    let close = find_unquoted(rest, ')')
418        .ok_or_else(|| HttpSigError::Parse("missing `)` in inner list".to_owned()))?;
419    let inner = &rest[1..close];
420
421    let mut components = Vec::new();
422    for token in inner.split_whitespace() {
423        components.push(Component::from_quoted_id(token)?);
424    }
425
426    let mut params = SignatureParams::default();
427    parse_params(&rest[close + 1..], &mut params)?;
428
429    Ok((label.to_owned(), components, params, rest.to_owned()))
430}
431
432/// Parses a `Signature` field value into its label and 64-byte signature.
433fn parse_signature(value: &str) -> Result<(String, [u8; 64]), HttpSigError> {
434    let (label, rest) = split_member(value)?;
435    let b64 = rest
436        .trim()
437        .strip_prefix(':')
438        .and_then(|t| t.strip_suffix(':'))
439        .ok_or_else(|| HttpSigError::Parse("byte sequence must be wrapped in `:`".to_owned()))?;
440    let bytes = STANDARD
441        .decode(b64)
442        .map_err(|_| HttpSigError::MalformedSignature)?;
443    let array: [u8; 64] = bytes
444        .try_into()
445        .map_err(|_| HttpSigError::MalformedSignature)?;
446    Ok((label.to_owned(), array))
447}
448
449/// Verifies an HTTP message signature on `request`.
450///
451/// Reconstructs the signature base from the components named in
452/// `signature_input` (using the received parameter string verbatim, so the base
453/// is byte-exact), verifies it against `verifying_key`, and applies the checks
454/// in `config`. Fails closed on any deviation.
455///
456/// A successful return proves only that the covered components were signed with
457/// `verifying_key`. It does **not** by itself guarantee any particular
458/// component was covered — use [`VerifyConfig::required_components`] to require
459/// them — nor does it check the message body: if `content-digest` is covered,
460/// the caller MUST also recompute and compare it against the received body with
461/// [`verify_content_digest`](crate::verify_content_digest). Freshness and
462/// replay defense (unique `nonce` / bounded age) are also the caller's
463/// responsibility; see `max_age`.
464///
465/// # Errors
466///
467/// Returns the corresponding [`HttpSigError`] for an unparsable field, a label
468/// mismatch, a missing covered header, an unexpected `alg`, a malformed or
469/// invalid signature, a missing required component, or a stale, expired,
470/// future-dated, or inverted-window signature.
471pub fn verify(
472    request: &HttpRequest,
473    signature_input: &str,
474    signature: &str,
475    verifying_key: &VerifyingKey,
476    config: &VerifyConfig,
477) -> Result<VerifiedSignature, HttpSigError> {
478    let (input_label, components, params, params_value) = parse_signature_input(signature_input)?;
479    let (sig_label, sig_bytes) = parse_signature(signature)?;
480
481    if input_label != sig_label {
482        return Err(HttpSigError::LabelMismatch);
483    }
484    if let Some(expected) = &config.label {
485        if expected != &input_label {
486            return Err(HttpSigError::LabelMismatch);
487        }
488    }
489    if config.wimse_profile {
490        check_request_profile(&params)?;
491    }
492    if let Some(alg) = &params.alg {
493        if alg != ALG {
494            return Err(HttpSigError::UnsupportedAlg { found: alg.clone() });
495        }
496    }
497
498    let base = signature_base_from_params_str(request, &components, &params_value)?;
499    let signature = Signature::from_bytes(&sig_bytes);
500    verifying_key
501        .verify_strict(base.as_bytes(), &signature)
502        .map_err(|_| HttpSigError::InvalidSignature)?;
503
504    for required in &config.required_components {
505        if !components.contains(required) {
506            return Err(HttpSigError::MissingRequiredComponent(required.quoted_id()));
507        }
508    }
509    // Only now, with the parameters proven authentic, is `wimse-aud` worth
510    // acting on. Checking it earlier would decide authorization from data an
511    // attacker still controls, and would let a forged message be told apart by
512    // whether it guessed the audience.
513    if let Some(expected) = &config.expected_audience {
514        if params.wimse_aud.as_ref() != Some(expected) {
515            return Err(HttpSigError::AudienceMismatch);
516        }
517    }
518
519    if let (Some(created), Some(expires)) = (params.created, params.expires) {
520        if expires < created {
521            return Err(HttpSigError::InvalidTimeWindow);
522        }
523    }
524    // A `max_age` without a `now` would silently skip the freshness check; fail
525    // closed rather than give a false sense of enforcement.
526    if config.max_age.is_some() && config.now.is_none() {
527        return Err(HttpSigError::TooOld);
528    }
529    if let Some(now) = config.now {
530        if let Some(expires) = params.expires {
531            if now > expires.saturating_add(config.leeway) {
532                return Err(HttpSigError::Expired);
533            }
534        }
535        if let Some(created) = params.created {
536            if created > now.saturating_add(config.leeway) {
537                return Err(HttpSigError::CreatedInFuture);
538            }
539        }
540        if let Some(max_age) = config.max_age {
541            let created = params.created.ok_or(HttpSigError::TooOld)?;
542            if now.saturating_sub(created) > max_age {
543                return Err(HttpSigError::TooOld);
544            }
545        }
546    }
547
548    Ok(VerifiedSignature {
549        label: input_label,
550        components,
551        params,
552    })
553}
554
555#[cfg(test)]
556mod tests {
557    use ed25519_dalek::SigningKey;
558
559    use super::{sign, signature_base, verify, SignatureParams, VerifyConfig, ALG};
560    use crate::error::HttpSigError;
561    use crate::message::{Component, HttpRequest};
562
563    // The canonical RFC 9421 test request (Section 2.5).
564    fn rfc_request() -> HttpRequest {
565        HttpRequest {
566            method: "POST".to_owned(),
567            authority: "example.com".to_owned(),
568            path: "/foo".to_owned(),
569            query: Some("param=Value&Pet=dog".to_owned()),
570            headers: vec![
571                ("Host".to_owned(), "example.com".to_owned()),
572                ("Date".to_owned(), "Tue, 20 Apr 2021 02:07:55 GMT".to_owned()),
573                ("Content-Type".to_owned(), "application/json".to_owned()),
574                (
575                    "Content-Digest".to_owned(),
576                    "sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:".to_owned(),
577                ),
578                ("Content-Length".to_owned(), "18".to_owned()),
579            ],
580        }
581    }
582
583    fn rfc_components() -> Vec<Component> {
584        vec![
585            Component::Method,
586            Component::Authority,
587            Component::Path,
588            Component::header("content-digest"),
589            Component::header("content-length"),
590            Component::header("content-type"),
591        ]
592    }
593
594    // Known-answer test: the signature base must match RFC 9421 Section 2.5
595    // byte-for-byte.
596    #[test]
597    fn signature_base_matches_rfc_9421() {
598        let params = SignatureParams {
599            created: Some(1_618_884_473),
600            keyid: Some("test-key-rsa-pss".to_owned()),
601            ..SignatureParams::default()
602        };
603        let base = signature_base(&rfc_request(), &rfc_components(), &params).unwrap();
604
605        let expected = concat!(
606            "\"@method\": POST\n",
607            "\"@authority\": example.com\n",
608            "\"@path\": /foo\n",
609            "\"content-digest\": sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:\n",
610            "\"content-length\": 18\n",
611            "\"content-type\": application/json\n",
612            "\"@signature-params\": (\"@method\" \"@authority\" \"@path\" \"content-digest\" \"content-length\" \"content-type\");created=1618884473;keyid=\"test-key-rsa-pss\""
613        );
614        assert_eq!(base, expected);
615    }
616
617    fn ed25519_params() -> SignatureParams {
618        SignatureParams {
619            created: Some(1_700_000_000),
620            keyid: Some("issuer-key-1".to_owned()),
621            alg: Some(ALG.to_owned()),
622            ..SignatureParams::default()
623        }
624    }
625
626    #[test]
627    fn round_trips() {
628        let key = SigningKey::from_bytes(&[5u8; 32]);
629        let request = rfc_request();
630        let components = rfc_components();
631        let signed = sign(&request, &components, &ed25519_params(), "sig1", &key).unwrap();
632
633        let verified = verify(
634            &request,
635            &signed.signature_input,
636            &signed.signature,
637            &key.verifying_key(),
638            &VerifyConfig::default(),
639        )
640        .unwrap();
641        assert_eq!(verified.label, "sig1");
642        assert_eq!(verified.components, components);
643        assert_eq!(verified.params.keyid.as_deref(), Some("issuer-key-1"));
644    }
645
646    #[test]
647    fn rejects_a_tampered_request() {
648        let key = SigningKey::from_bytes(&[5u8; 32]);
649        let mut request = rfc_request();
650        let components = rfc_components();
651        let signed = sign(&request, &components, &ed25519_params(), "sig1", &key).unwrap();
652
653        // Change a covered header after signing.
654        request
655            .headers
656            .push(("Content-Length".to_owned(), "19".to_owned()));
657        request
658            .headers
659            .retain(|(n, v)| !(n == "Content-Length" && v == "18"));
660
661        let err = verify(
662            &request,
663            &signed.signature_input,
664            &signed.signature,
665            &key.verifying_key(),
666            &VerifyConfig::default(),
667        );
668        assert!(matches!(err, Err(HttpSigError::InvalidSignature)));
669    }
670
671    #[test]
672    fn rejects_the_wrong_key() {
673        let key = SigningKey::from_bytes(&[5u8; 32]);
674        let other = SigningKey::from_bytes(&[6u8; 32]);
675        let request = rfc_request();
676        let signed = sign(&request, &rfc_components(), &ed25519_params(), "sig1", &key).unwrap();
677
678        let err = verify(
679            &request,
680            &signed.signature_input,
681            &signed.signature,
682            &other.verifying_key(),
683            &VerifyConfig::default(),
684        );
685        assert!(matches!(err, Err(HttpSigError::InvalidSignature)));
686    }
687
688    #[test]
689    fn rejects_a_missing_covered_header() {
690        let key = SigningKey::from_bytes(&[5u8; 32]);
691        let request = HttpRequest {
692            headers: vec![],
693            ..rfc_request()
694        };
695        let err = sign(&request, &rfc_components(), &ed25519_params(), "sig1", &key);
696        assert!(matches!(err, Err(HttpSigError::MissingComponent(_))));
697    }
698
699    #[test]
700    fn enforces_expiry() {
701        let key = SigningKey::from_bytes(&[5u8; 32]);
702        let request = rfc_request();
703        let params = SignatureParams {
704            created: Some(1_700_000_000),
705            expires: Some(1_700_000_300),
706            keyid: Some("k".to_owned()),
707            alg: Some(ALG.to_owned()),
708            ..SignatureParams::default()
709        };
710        let signed = sign(&request, &rfc_components(), &params, "sig1", &key).unwrap();
711
712        let config = VerifyConfig {
713            now: Some(1_700_000_301),
714            ..VerifyConfig::default()
715        };
716        let err = verify(
717            &request,
718            &signed.signature_input,
719            &signed.signature,
720            &key.verifying_key(),
721            &config,
722        );
723        assert!(matches!(err, Err(HttpSigError::Expired)));
724    }
725
726    #[test]
727    fn rejects_a_label_mismatch() {
728        let key = SigningKey::from_bytes(&[5u8; 32]);
729        let request = rfc_request();
730        let signed = sign(&request, &rfc_components(), &ed25519_params(), "sig1", &key).unwrap();
731
732        let config = VerifyConfig {
733            label: Some("other".to_owned()),
734            ..VerifyConfig::default()
735        };
736        let err = verify(
737            &request,
738            &signed.signature_input,
739            &signed.signature,
740            &key.verifying_key(),
741            &config,
742        );
743        assert!(matches!(err, Err(HttpSigError::LabelMismatch)));
744    }
745
746    #[test]
747    fn is_deterministic() {
748        let key = SigningKey::from_bytes(&[5u8; 32]);
749        let request = rfc_request();
750        let components = rfc_components();
751        let a = sign(&request, &components, &ed25519_params(), "sig1", &key).unwrap();
752        let b = sign(&request, &components, &ed25519_params(), "sig1", &key).unwrap();
753        assert_eq!(a.signature_input, b.signature_input);
754        assert_eq!(a.signature, b.signature);
755    }
756
757    #[test]
758    fn rejects_a_missing_required_component() {
759        let key = SigningKey::from_bytes(&[5u8; 32]);
760        let request = rfc_request();
761        // Signature covers method and path only.
762        let signed = sign(
763            &request,
764            &[Component::Method, Component::Path],
765            &ed25519_params(),
766            "sig1",
767            &key,
768        )
769        .unwrap();
770
771        let config = VerifyConfig {
772            required_components: vec![Component::header("content-digest")],
773            ..VerifyConfig::default()
774        };
775        let err = verify(
776            &request,
777            &signed.signature_input,
778            &signed.signature,
779            &key.verifying_key(),
780            &config,
781        );
782        assert!(matches!(
783            err,
784            Err(HttpSigError::MissingRequiredComponent(_))
785        ));
786    }
787
788    #[test]
789    fn rejects_a_non_ed25519_alg() {
790        let key = SigningKey::from_bytes(&[5u8; 32]);
791        let request = rfc_request();
792        let params = SignatureParams {
793            created: Some(1_700_000_000),
794            keyid: Some("k".to_owned()),
795            alg: Some("rsa-pss".to_owned()),
796            ..SignatureParams::default()
797        };
798        let signed = sign(&request, &rfc_components(), &params, "sig1", &key).unwrap();
799
800        let err = verify(
801            &request,
802            &signed.signature_input,
803            &signed.signature,
804            &key.verifying_key(),
805            &VerifyConfig::default(),
806        );
807        assert!(matches!(err, Err(HttpSigError::UnsupportedAlg { .. })));
808    }
809
810    #[test]
811    fn rejects_crlf_in_a_covered_header() {
812        let key = SigningKey::from_bytes(&[5u8; 32]);
813        let mut request = rfc_request();
814        request
815            .headers
816            .push(("X-Evil".to_owned(), "ok\n\"@path\": /evil".to_owned()));
817
818        let err = sign(
819            &request,
820            &[Component::Method, Component::header("x-evil")],
821            &ed25519_params(),
822            "sig1",
823            &key,
824        );
825        assert!(matches!(err, Err(HttpSigError::InvalidComponentValue(_))));
826    }
827
828    #[test]
829    fn rejects_an_inverted_time_window() {
830        let key = SigningKey::from_bytes(&[5u8; 32]);
831        let request = rfc_request();
832        let params = SignatureParams {
833            created: Some(1_700_000_300),
834            expires: Some(1_700_000_000),
835            keyid: Some("k".to_owned()),
836            alg: Some(ALG.to_owned()),
837            ..SignatureParams::default()
838        };
839        let signed = sign(&request, &rfc_components(), &params, "sig1", &key).unwrap();
840
841        let err = verify(
842            &request,
843            &signed.signature_input,
844            &signed.signature,
845            &key.verifying_key(),
846            &VerifyConfig::default(),
847        );
848        assert!(matches!(err, Err(HttpSigError::InvalidTimeWindow)));
849    }
850
851    #[test]
852    fn enforces_max_age() {
853        let key = SigningKey::from_bytes(&[5u8; 32]);
854        let request = rfc_request();
855        // created is 1_700_000_000.
856        let signed = sign(&request, &rfc_components(), &ed25519_params(), "sig1", &key).unwrap();
857
858        let config = VerifyConfig {
859            now: Some(1_700_000_400),
860            max_age: Some(60),
861            ..VerifyConfig::default()
862        };
863        let err = verify(
864            &request,
865            &signed.signature_input,
866            &signed.signature,
867            &key.verifying_key(),
868            &config,
869        );
870        assert!(matches!(err, Err(HttpSigError::TooOld)));
871    }
872
873    #[test]
874    fn tolerates_unknown_boolean_parameters() {
875        use base64::{engine::general_purpose::STANDARD, Engine};
876        use ed25519_dalek::Signer;
877
878        let key = SigningKey::from_bytes(&[5u8; 32]);
879        let request = rfc_request();
880        // A params value carrying a boolean parameter `;ext` (no value).
881        let params_value = "(\"@method\" \"@path\");created=1700000000;ext";
882        let base = format!(
883            "\"@method\": {}\n\"@path\": {}\n\"@signature-params\": {params_value}",
884            request.method, request.path,
885        );
886        let signature = STANDARD.encode(key.sign(base.as_bytes()).to_bytes());
887        let signature_input = format!("sig1={params_value}");
888        let signature = format!("sig1=:{signature}:");
889
890        let verified = verify(
891            &request,
892            &signature_input,
893            &signature,
894            &key.verifying_key(),
895            &VerifyConfig::default(),
896        )
897        .unwrap();
898        assert_eq!(verified.params.created, Some(1_700_000_000));
899    }
900
901    #[test]
902    fn content_digest_helper_binds_the_body() {
903        use crate::message::{content_digest_sha256, verify_content_digest};
904
905        let body = br#"{"amount":100}"#;
906        let header = content_digest_sha256(body);
907        assert!(verify_content_digest(&header, body));
908        assert!(!verify_content_digest(&header, b"tampered"));
909    }
910
911    #[test]
912    fn round_trips_params_with_quoted_delimiters() {
913        // A keyid whose value legitimately contains `;`, `)` and `"` (all valid
914        // inside an RFC 8941 string) must survive the round trip.
915        let key = SigningKey::from_bytes(&[5u8; 32]);
916        let request = rfc_request();
917        let params = SignatureParams {
918            created: Some(1_700_000_000),
919            keyid: Some("weird;key)with\"quote".to_owned()),
920            alg: Some(ALG.to_owned()),
921            ..SignatureParams::default()
922        };
923        let signed = sign(&request, &rfc_components(), &params, "sig1", &key).unwrap();
924
925        let verified = verify(
926            &request,
927            &signed.signature_input,
928            &signed.signature,
929            &key.verifying_key(),
930            &VerifyConfig::default(),
931        )
932        .unwrap();
933        assert_eq!(
934            verified.params.keyid.as_deref(),
935            Some("weird;key)with\"quote")
936        );
937    }
938
939    #[test]
940    fn rejects_crlf_in_signature_params() {
941        use base64::{engine::general_purpose::STANDARD, Engine};
942
943        let key = SigningKey::from_bytes(&[5u8; 32]);
944        let request = rfc_request();
945        // A newline smuggled into the parameters (parses, but must be rejected).
946        let signature_input = "sig1=(\"@method\")\n;created=1700000000";
947        let signature = format!("sig1=:{}:", STANDARD.encode([0u8; 64]));
948
949        let err = verify(
950            &request,
951            signature_input,
952            &signature,
953            &key.verifying_key(),
954            &VerifyConfig::default(),
955        );
956        assert!(matches!(err, Err(HttpSigError::Parse(_))));
957    }
958
959    use base64::{engine::general_purpose::STANDARD, Engine};
960
961    use super::{check_request_profile, WIMSE_TAG};
962
963    /// A parameter set that satisfies every rule of the profile.
964    fn wimse_params() -> SignatureParams {
965        SignatureParams {
966            created: Some(1_700_000_000),
967            expires: Some(1_700_000_300),
968            nonce: Some("abcd1111".to_owned()),
969            tag: Some(WIMSE_TAG.to_owned()),
970            wimse_aud: Some("https://svcb.example.com/gimme-ice-cream".to_owned()),
971            ..SignatureParams::default()
972        }
973    }
974
975    fn wimse_config() -> VerifyConfig {
976        VerifyConfig {
977            wimse_profile: true,
978            ..VerifyConfig::default()
979        }
980    }
981
982    fn sign_with(params: &SignatureParams) -> (SigningKey, HttpRequest, super::SignedSignature) {
983        let key = SigningKey::from_bytes(&[5u8; 32]);
984        let request = rfc_request();
985        let signed = sign(&request, &rfc_components(), params, "wimse", &key).unwrap();
986        (key, request, signed)
987    }
988
989    #[test]
990    fn accepts_a_profile_conforming_signature() {
991        let (key, request, signed) = sign_with(&wimse_params());
992        let verified = verify(
993            &request,
994            &signed.signature_input,
995            &signed.signature,
996            &key.verifying_key(),
997            &wimse_config(),
998        )
999        .unwrap();
1000        assert_eq!(verified.params.tag.as_deref(), Some(WIMSE_TAG));
1001    }
1002
1003    // `keyid` and `alg` MUST NOT be used: the key travels in the WIT and the
1004    // algorithm is pinned by that WIT's `cnf` JWK.
1005    #[test]
1006    fn profile_rejects_keyid_and_alg() {
1007        for (label, params) in [
1008            (
1009                "keyid",
1010                SignatureParams {
1011                    keyid: Some("k".to_owned()),
1012                    ..wimse_params()
1013                },
1014            ),
1015            (
1016                "alg",
1017                SignatureParams {
1018                    alg: Some(ALG.to_owned()),
1019                    ..wimse_params()
1020                },
1021            ),
1022        ] {
1023            let (key, request, signed) = sign_with(&params);
1024            let err = verify(
1025                &request,
1026                &signed.signature_input,
1027                &signed.signature,
1028                &key.verifying_key(),
1029                &wimse_config(),
1030            );
1031            assert!(
1032                matches!(err, Err(HttpSigError::ForbiddenParameter(p)) if p == label),
1033                "expected `{label}` to be rejected, got {err:?}"
1034            );
1035        }
1036    }
1037
1038    #[test]
1039    fn profile_requires_the_mandatory_parameters() {
1040        let cases: [(&str, SignatureParams); 5] = [
1041            (
1042                "created",
1043                SignatureParams {
1044                    created: None,
1045                    ..wimse_params()
1046                },
1047            ),
1048            (
1049                "expires",
1050                SignatureParams {
1051                    expires: None,
1052                    ..wimse_params()
1053                },
1054            ),
1055            (
1056                "nonce",
1057                SignatureParams {
1058                    nonce: None,
1059                    ..wimse_params()
1060                },
1061            ),
1062            (
1063                "tag",
1064                SignatureParams {
1065                    tag: None,
1066                    ..wimse_params()
1067                },
1068            ),
1069            (
1070                "wimse-aud",
1071                SignatureParams {
1072                    wimse_aud: None,
1073                    ..wimse_params()
1074                },
1075            ),
1076        ];
1077        for (name, params) in cases {
1078            let err = check_request_profile(&params);
1079            assert!(
1080                matches!(err, Err(HttpSigError::MissingParameter(p)) if p == name),
1081                "expected `{name}` to be required, got {err:?}"
1082            );
1083        }
1084    }
1085
1086    #[test]
1087    fn profile_rejects_a_foreign_tag() {
1088        let params = SignatureParams {
1089            tag: Some("something-else".to_owned()),
1090            ..wimse_params()
1091        };
1092        let (key, request, signed) = sign_with(&params);
1093        let err = verify(
1094            &request,
1095            &signed.signature_input,
1096            &signed.signature,
1097            &key.verifying_key(),
1098            &wimse_config(),
1099        );
1100        assert!(matches!(err, Err(HttpSigError::WrongTag { .. })));
1101    }
1102
1103    // A signature is only bound to this service if its `wimse-aud` is checked;
1104    // one minted for a peer must not verify here.
1105    #[test]
1106    fn rejects_a_signature_minted_for_another_audience() {
1107        let (key, request, signed) = sign_with(&wimse_params());
1108        let config = VerifyConfig {
1109            expected_audience: Some("https://svcc.example.com/other".to_owned()),
1110            ..wimse_config()
1111        };
1112        let err = verify(
1113            &request,
1114            &signed.signature_input,
1115            &signed.signature,
1116            &key.verifying_key(),
1117            &config,
1118        );
1119        assert!(matches!(err, Err(HttpSigError::AudienceMismatch)));
1120    }
1121
1122    // The audience must be judged only after the signature proves the parameters
1123    // authentic. A forged message must report the forgery, not whether it
1124    // happened to guess the audience — otherwise an attacker who cannot sign
1125    // anything can still probe for the audience a service answers to.
1126    #[test]
1127    fn reports_a_forgery_as_invalid_regardless_of_audience() {
1128        let key = SigningKey::from_bytes(&[5u8; 32]);
1129        let request = rfc_request();
1130        let (_, _, signed) = sign_with(&wimse_params());
1131        let forged = format!("wimse=:{}:", STANDARD.encode([0u8; 64]));
1132
1133        for audience in [
1134            "https://svcb.example.com/gimme-ice-cream",
1135            "https://wrong.example/inbox",
1136        ] {
1137            let config = VerifyConfig {
1138                expected_audience: Some(audience.to_owned()),
1139                ..wimse_config()
1140            };
1141            let err = verify(
1142                &request,
1143                &signed.signature_input,
1144                &forged,
1145                &key.verifying_key(),
1146                &config,
1147            );
1148            assert!(
1149                matches!(err, Err(HttpSigError::InvalidSignature)),
1150                "expected InvalidSignature for audience {audience}, got {err:?}"
1151            );
1152        }
1153    }
1154
1155    #[test]
1156    fn accepts_the_matching_audience() {
1157        let (key, request, signed) = sign_with(&wimse_params());
1158        let config = VerifyConfig {
1159            expected_audience: Some("https://svcb.example.com/gimme-ice-cream".to_owned()),
1160            ..wimse_config()
1161        };
1162        assert!(verify(
1163            &request,
1164            &signed.signature_input,
1165            &signed.signature,
1166            &key.verifying_key(),
1167            &config,
1168        )
1169        .is_ok());
1170    }
1171
1172    #[test]
1173    fn serializes_sign_response_as_a_bare_boolean() {
1174        let params = SignatureParams {
1175            wimse_sign_response: Some(true),
1176            ..wimse_params()
1177        };
1178        let (key, request, signed) = sign_with(&params);
1179        assert!(signed.signature_input.ends_with(";wimse-sign-response"));
1180
1181        let verified = verify(
1182            &request,
1183            &signed.signature_input,
1184            &signed.signature,
1185            &key.verifying_key(),
1186            &wimse_config(),
1187        )
1188        .unwrap();
1189        assert_eq!(verified.params.wimse_sign_response, Some(true));
1190    }
1191
1192    #[test]
1193    fn round_trips_an_explicit_false_sign_response() {
1194        let params = SignatureParams {
1195            wimse_sign_response: Some(false),
1196            ..wimse_params()
1197        };
1198        let (key, request, signed) = sign_with(&params);
1199        assert!(signed.signature_input.ends_with(";wimse-sign-response=?0"));
1200
1201        let verified = verify(
1202            &request,
1203            &signed.signature_input,
1204            &signed.signature,
1205            &key.verifying_key(),
1206            &wimse_config(),
1207        )
1208        .unwrap();
1209        assert_eq!(verified.params.wimse_sign_response, Some(false));
1210    }
1211
1212    #[test]
1213    fn round_trips_the_response_nonce_binding() {
1214        let params = SignatureParams {
1215            wimse_req_nonce: Some("abcd1111".to_owned()),
1216            ..wimse_params()
1217        };
1218        let (key, request, signed) = sign_with(&params);
1219        let verified = verify(
1220            &request,
1221            &signed.signature_input,
1222            &signed.signature,
1223            &key.verifying_key(),
1224            &wimse_config(),
1225        )
1226        .unwrap();
1227        assert_eq!(verified.params.wimse_req_nonce.as_deref(), Some("abcd1111"));
1228    }
1229
1230    // The profile is opt-in: a plain RFC 9421 signature must still verify with
1231    // the default config, which is what the known-answer test above relies on.
1232    #[test]
1233    fn profile_is_off_by_default() {
1234        let (key, request, signed) = sign_with(&ed25519_params());
1235        assert!(verify(
1236            &request,
1237            &signed.signature_input,
1238            &signed.signature,
1239            &key.verifying_key(),
1240            &VerifyConfig::default(),
1241        )
1242        .is_ok());
1243    }
1244
1245    #[test]
1246    fn rejects_max_age_without_now() {
1247        let key = SigningKey::from_bytes(&[5u8; 32]);
1248        let request = rfc_request();
1249        let signed = sign(&request, &rfc_components(), &ed25519_params(), "sig1", &key).unwrap();
1250
1251        // `max_age` set but `now` unset must fail closed, not silently skip.
1252        let config = VerifyConfig {
1253            max_age: Some(60),
1254            ..VerifyConfig::default()
1255        };
1256        let err = verify(
1257            &request,
1258            &signed.signature_input,
1259            &signed.signature,
1260            &key.verifying_key(),
1261            &config,
1262        );
1263        assert!(matches!(err, Err(HttpSigError::TooOld)));
1264    }
1265}