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 wimsey_jose::{SigningKey, VerifyingKey, SIGNATURE_LEN};
8
9use crate::error::HttpSigError;
10use crate::message::{Component, ComponentSource};
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    /// Enforce the WIMSE **response**-signature profile instead of the request
112    /// one: same mandatory parameters, but `wimse-aud` is a request-only
113    /// parameter and `wimse-req-nonce` is required whenever the client demanded
114    /// a signed response.
115    ///
116    /// Whether it was demanded is taken from `expected_req_nonce` being set,
117    /// since the client that sent the request is the only party that knows, and
118    /// it is also the only party that can check the returned value.
119    pub wimse_response_profile: bool,
120    /// If set, the response's `wimse-req-nonce` must equal this value — the
121    /// `nonce` the client put on its own request. Checking it is what stops a
122    /// response signed for one request being replayed against another.
123    pub expected_req_nonce: Option<String>,
124}
125
126/// Errors unless `params` satisfies the WIMSE profile for a **request**
127/// signature (Section 3 of `draft-ietf-wimse-http-signature`).
128///
129/// The profile makes `created`, `expires`, `nonce` and `tag` mandatory on every
130/// message and `wimse-aud` mandatory on requests, and forbids `keyid` and `alg`
131/// — the signing key travels in the WIT and the algorithm is pinned by that
132/// WIT's `cnf` JWK, so repeating either here would only invite confusion.
133///
134/// # Errors
135///
136/// Returns [`HttpSigError::MissingParameter`], [`HttpSigError::ForbiddenParameter`]
137/// or [`HttpSigError::WrongTag`] for the first rule the parameters break.
138pub fn check_request_profile(params: &SignatureParams) -> Result<(), HttpSigError> {
139    if params.keyid.is_some() {
140        return Err(HttpSigError::ForbiddenParameter("keyid"));
141    }
142    if params.alg.is_some() {
143        return Err(HttpSigError::ForbiddenParameter("alg"));
144    }
145    if params.created.is_none() {
146        return Err(HttpSigError::MissingParameter("created"));
147    }
148    if params.expires.is_none() {
149        return Err(HttpSigError::MissingParameter("expires"));
150    }
151    if params.nonce.is_none() {
152        return Err(HttpSigError::MissingParameter("nonce"));
153    }
154    match params.tag.as_deref() {
155        None => return Err(HttpSigError::MissingParameter("tag")),
156        Some(tag) if tag != WIMSE_TAG => {
157            return Err(HttpSigError::WrongTag {
158                found: tag.to_owned(),
159            })
160        }
161        Some(_) => {}
162    }
163    if params.wimse_aud.is_none() {
164        return Err(HttpSigError::MissingParameter("wimse-aud"));
165    }
166    Ok(())
167}
168
169/// Errors unless `params` satisfies the WIMSE profile for a **response**
170/// signature (Section 3 of `draft-ietf-wimse-http-signature`).
171///
172/// `created`, `expires`, `nonce` and `tag` are mandatory on every message, and
173/// `keyid` and `alg` are forbidden on every message, exactly as for a request.
174/// The difference is at the ends: `wimse-aud` names the service a *request* is
175/// for and has no meaning coming back, while `wimse-req-nonce` carries the
176/// requesting client's nonce and is required when that client asked for a
177/// signed response.
178///
179/// # Errors
180///
181/// Returns [`HttpSigError::MissingParameter`], [`HttpSigError::ForbiddenParameter`]
182/// or [`HttpSigError::WrongTag`] for the first rule the parameters break.
183pub fn check_response_profile(
184    params: &SignatureParams,
185    response_signing_required: bool,
186) -> Result<(), HttpSigError> {
187    if params.keyid.is_some() {
188        return Err(HttpSigError::ForbiddenParameter("keyid"));
189    }
190    if params.alg.is_some() {
191        return Err(HttpSigError::ForbiddenParameter("alg"));
192    }
193    if params.created.is_none() {
194        return Err(HttpSigError::MissingParameter("created"));
195    }
196    if params.expires.is_none() {
197        return Err(HttpSigError::MissingParameter("expires"));
198    }
199    if params.nonce.is_none() {
200        return Err(HttpSigError::MissingParameter("nonce"));
201    }
202    match params.tag.as_deref() {
203        None => return Err(HttpSigError::MissingParameter("tag")),
204        Some(tag) if tag != WIMSE_TAG => {
205            return Err(HttpSigError::WrongTag {
206                found: tag.to_owned(),
207            })
208        }
209        Some(_) => {}
210    }
211    if params.wimse_aud.is_some() {
212        return Err(HttpSigError::ForbiddenParameter("wimse-aud"));
213    }
214    if response_signing_required && params.wimse_req_nonce.is_none() {
215        return Err(HttpSigError::MissingParameter("wimse-req-nonce"));
216    }
217    Ok(())
218}
219
220/// The components the WIMSE profile requires a **response** signature to cover,
221/// given the headers the response actually carries.
222///
223/// Section 3 names `@status`, `@method;req` and `@request-target;req`, plus
224/// `Content-Type` and `Content-Digest` when present and the WIT. The two `;req`
225/// components are the interesting ones: without them a signed response could be
226/// lifted onto a different request.
227#[must_use]
228pub fn response_components(headers: &[(String, String)]) -> Vec<Component> {
229    let mut components = vec![
230        Component::Status,
231        Component::Req(Box::new(Component::Method)),
232        Component::Req(Box::new(Component::RequestTarget)),
233    ];
234    for name in ["content-type", "content-digest", "workload-identity-token"] {
235        if headers.iter().any(|(n, _)| n.eq_ignore_ascii_case(name)) {
236            components.push(Component::header(name));
237        }
238    }
239    components
240}
241
242fn sf_string(value: &str) -> String {
243    let mut out = String::with_capacity(value.len() + 2);
244    out.push('"');
245    for c in value.chars() {
246        if c == '\\' || c == '"' {
247            out.push('\\');
248        }
249        out.push(c);
250    }
251    out.push('"');
252    out
253}
254
255fn serialize_params_value(components: &[Component], params: &SignatureParams) -> String {
256    let inner = components
257        .iter()
258        .map(Component::quoted_id)
259        .collect::<Vec<_>>()
260        .join(" ");
261    let mut s = format!("({inner})");
262    if let Some(created) = params.created {
263        let _ = write!(s, ";created={created}");
264    }
265    if let Some(expires) = params.expires {
266        let _ = write!(s, ";expires={expires}");
267    }
268    if let Some(keyid) = &params.keyid {
269        let _ = write!(s, ";keyid={}", sf_string(keyid));
270    }
271    if let Some(alg) = &params.alg {
272        let _ = write!(s, ";alg={}", sf_string(alg));
273    }
274    if let Some(nonce) = &params.nonce {
275        let _ = write!(s, ";nonce={}", sf_string(nonce));
276    }
277    if let Some(tag) = &params.tag {
278        let _ = write!(s, ";tag={}", sf_string(tag));
279    }
280    if let Some(aud) = &params.wimse_aud {
281        let _ = write!(s, ";wimse-aud={}", sf_string(aud));
282    }
283    if let Some(sign_response) = params.wimse_sign_response {
284        // RFC 8941 Section 4.1.1.2: a Boolean `true` parameter MUST omit its
285        // value, so it serializes as the bare parameter name.
286        if sign_response {
287            s.push_str(";wimse-sign-response");
288        } else {
289            s.push_str(";wimse-sign-response=?0");
290        }
291    }
292    if let Some(req_nonce) = &params.wimse_req_nonce {
293        let _ = write!(s, ";wimse-req-nonce={}", sf_string(req_nonce));
294    }
295    s
296}
297
298fn signature_base_from_params_str(
299    message: &impl ComponentSource,
300    components: &[Component],
301    params_value: &str,
302) -> Result<String, HttpSigError> {
303    // The received parameter substring is untrusted; a bare CR or LF in it would
304    // forge extra signature-base lines.
305    if params_value.contains(['\r', '\n']) {
306        return Err(HttpSigError::Parse(
307            "signature parameters contain CR or LF".to_owned(),
308        ));
309    }
310    let mut base = String::new();
311    for component in components {
312        let value = message.component_value(component)?;
313        // A bare CR or LF in a value would forge extra signature-base lines.
314        if value.contains(['\r', '\n']) {
315            return Err(HttpSigError::InvalidComponentValue(component.quoted_id()));
316        }
317        base.push_str(&component.quoted_id());
318        base.push_str(": ");
319        base.push_str(&value);
320        base.push('\n');
321    }
322    base.push_str("\"@signature-params\": ");
323    base.push_str(params_value);
324    Ok(base)
325}
326
327/// Builds the RFC 9421 signature base for `request` over `components` with
328/// `params`.
329///
330/// # Errors
331///
332/// Returns [`HttpSigError::MissingComponent`] if a covered header is absent.
333pub fn signature_base(
334    message: &impl ComponentSource,
335    components: &[Component],
336    params: &SignatureParams,
337) -> Result<String, HttpSigError> {
338    let params_value = serialize_params_value(components, params);
339    signature_base_from_params_str(message, components, &params_value)
340}
341
342/// Signs `request` over `components`, producing `Signature-Input` and
343/// `Signature` field values under `label`.
344///
345/// # Errors
346///
347/// Returns [`HttpSigError::MissingComponent`] if a covered header is absent.
348pub fn sign(
349    message: &impl ComponentSource,
350    components: &[Component],
351    params: &SignatureParams,
352    label: &str,
353    signing_key: &SigningKey,
354) -> Result<SignedSignature, HttpSigError> {
355    let params_value = serialize_params_value(components, params);
356    let base = signature_base_from_params_str(message, components, &params_value)?;
357    Ok(SignedSignature {
358        signature_input: format!("{label}={params_value}"),
359        signature: format!(
360            "{label}=:{}:",
361            STANDARD.encode(signing_key.sign(base.as_bytes()))
362        ),
363    })
364}
365
366/// Splits a single-member dictionary field value `label=rest` at the first `=`.
367fn split_member(value: &str) -> Result<(&str, &str), HttpSigError> {
368    let value = value.trim();
369    let eq = value
370        .find('=')
371        .ok_or_else(|| HttpSigError::Parse("missing `=` in dictionary member".to_owned()))?;
372    let label = value[..eq].trim();
373    if label.is_empty() {
374        return Err(HttpSigError::Parse("empty signature label".to_owned()));
375    }
376    Ok((label, &value[eq + 1..]))
377}
378
379fn parse_sf_string(token: &str) -> Result<String, HttpSigError> {
380    let inner = token
381        .strip_prefix('"')
382        .and_then(|t| t.strip_suffix('"'))
383        .ok_or_else(|| HttpSigError::Parse(format!("not a string: {token}")))?;
384    let mut out = String::with_capacity(inner.len());
385    let mut chars = inner.chars();
386    while let Some(c) = chars.next() {
387        if c == '\\' {
388            match chars.next() {
389                Some(next @ ('\\' | '"')) => out.push(next),
390                _ => return Err(HttpSigError::Parse("bad string escape".to_owned())),
391            }
392        } else {
393            out.push(c);
394        }
395    }
396    Ok(out)
397}
398
399/// The byte index of the first unescaped, unquoted `target` in `s`, respecting
400/// RFC 8941 string quoting so a delimiter inside a `"..."` value is skipped.
401fn find_unquoted(s: &str, target: char) -> Option<usize> {
402    let mut in_quotes = false;
403    let mut escaped = false;
404    for (idx, c) in s.char_indices() {
405        if escaped {
406            escaped = false;
407        } else if in_quotes && c == '\\' {
408            escaped = true;
409        } else if c == '"' {
410            in_quotes = !in_quotes;
411        } else if c == target && !in_quotes {
412            return Some(idx);
413        }
414    }
415    None
416}
417
418/// Splits `s` on unquoted `;`, keeping delimiters inside `"..."` values intact.
419fn split_unquoted_semicolons(s: &str) -> Vec<&str> {
420    let mut parts = Vec::new();
421    let mut start = 0;
422    let mut in_quotes = false;
423    let mut escaped = false;
424    for (idx, c) in s.char_indices() {
425        if escaped {
426            escaped = false;
427        } else if in_quotes && c == '\\' {
428            escaped = true;
429        } else if c == '"' {
430            in_quotes = !in_quotes;
431        } else if c == ';' && !in_quotes {
432            parts.push(&s[start..idx]);
433            start = idx + 1;
434        }
435    }
436    parts.push(&s[start..]);
437    parts
438}
439
440fn parse_params(rest: &str, params: &mut SignatureParams) -> Result<(), HttpSigError> {
441    for part in split_unquoted_semicolons(rest) {
442        let part = part.trim();
443        if part.is_empty() {
444            continue;
445        }
446        let Some((name, raw)) = part.split_once('=') else {
447            // A valueless parameter is Boolean `true`, per RFC 8941
448            // Section 4.1.1.2. Unknown ones are still ignored.
449            if part == "wimse-sign-response" {
450                params.wimse_sign_response = Some(true);
451            }
452            continue;
453        };
454        let name = name.trim();
455        let raw = raw.trim();
456        match name {
457            "created" => {
458                params.created = Some(parse_int(raw)?);
459            }
460            "expires" => {
461                params.expires = Some(parse_int(raw)?);
462            }
463            "keyid" => params.keyid = Some(parse_sf_string(raw)?),
464            "alg" => params.alg = Some(parse_sf_string(raw)?),
465            "nonce" => params.nonce = Some(parse_sf_string(raw)?),
466            "tag" => params.tag = Some(parse_sf_string(raw)?),
467            "wimse-aud" => params.wimse_aud = Some(parse_sf_string(raw)?),
468            "wimse-sign-response" => params.wimse_sign_response = Some(parse_sf_boolean(raw)?),
469            "wimse-req-nonce" => params.wimse_req_nonce = Some(parse_sf_string(raw)?),
470            // Unknown parameters are ignored, per structured-field extensibility.
471            _ => {}
472        }
473    }
474    Ok(())
475}
476
477/// Parses an RFC 8941 Boolean (`?0` or `?1`) given explicitly.
478fn parse_sf_boolean(raw: &str) -> Result<bool, HttpSigError> {
479    match raw {
480        "?1" => Ok(true),
481        "?0" => Ok(false),
482        other => Err(HttpSigError::Parse(format!("not a boolean: {other}"))),
483    }
484}
485
486fn parse_int(raw: &str) -> Result<u64, HttpSigError> {
487    raw.trim()
488        .parse()
489        .map_err(|_| HttpSigError::Parse(format!("not an integer: {raw}")))
490}
491
492/// Parses a `Signature-Input` field value into its label, covered components,
493/// parameters, and the verbatim parameters substring used in the base.
494fn parse_signature_input(
495    value: &str,
496) -> Result<(String, Vec<Component>, SignatureParams, String), HttpSigError> {
497    let (label, rest) = split_member(value)?;
498    let rest = rest.trim();
499    if !rest.starts_with('(') {
500        return Err(HttpSigError::Parse(
501            "inner list must start with `(`".to_owned(),
502        ));
503    }
504    // Find the inner list's closing `)`, ignoring any `)` inside a quoted value.
505    let close = find_unquoted(rest, ')')
506        .ok_or_else(|| HttpSigError::Parse("missing `)` in inner list".to_owned()))?;
507    let inner = &rest[1..close];
508
509    let mut components = Vec::new();
510    for token in inner.split_whitespace() {
511        components.push(Component::from_quoted_id(token)?);
512    }
513
514    let mut params = SignatureParams::default();
515    parse_params(&rest[close + 1..], &mut params)?;
516
517    Ok((label.to_owned(), components, params, rest.to_owned()))
518}
519
520/// Parses a `Signature` field value into its label and 64-byte signature.
521fn parse_signature(value: &str) -> Result<(String, [u8; SIGNATURE_LEN]), HttpSigError> {
522    let (label, rest) = split_member(value)?;
523    let b64 = rest
524        .trim()
525        .strip_prefix(':')
526        .and_then(|t| t.strip_suffix(':'))
527        .ok_or_else(|| HttpSigError::Parse("byte sequence must be wrapped in `:`".to_owned()))?;
528    let bytes = STANDARD
529        .decode(b64)
530        .map_err(|_| HttpSigError::MalformedSignature)?;
531    let array: [u8; SIGNATURE_LEN] = bytes
532        .try_into()
533        .map_err(|_| HttpSigError::MalformedSignature)?;
534    Ok((label.to_owned(), array))
535}
536
537/// Verifies an HTTP message signature on `request`.
538///
539/// Reconstructs the signature base from the components named in
540/// `signature_input` (using the received parameter string verbatim, so the base
541/// is byte-exact), verifies it against `verifying_key`, and applies the checks
542/// in `config`. Fails closed on any deviation.
543///
544/// A successful return proves only that the covered components were signed with
545/// `verifying_key`. It does **not** by itself guarantee any particular
546/// component was covered — use [`VerifyConfig::required_components`] to require
547/// them — nor does it check the message body: if `content-digest` is covered,
548/// the caller MUST also recompute and compare it against the received body with
549/// [`verify_content_digest`](crate::verify_content_digest). Freshness and
550/// replay defense (unique `nonce` / bounded age) are also the caller's
551/// responsibility; see `max_age`.
552///
553/// # Errors
554///
555/// Returns the corresponding [`HttpSigError`] for an unparsable field, a label
556/// mismatch, a missing covered header, an unexpected `alg`, a malformed or
557/// invalid signature, a missing required component, or a stale, expired,
558/// future-dated, or inverted-window signature.
559pub fn verify(
560    message: &impl ComponentSource,
561    signature_input: &str,
562    signature: &str,
563    verifying_key: &VerifyingKey,
564    config: &VerifyConfig,
565) -> Result<VerifiedSignature, HttpSigError> {
566    let (input_label, components, params, params_value) = parse_signature_input(signature_input)?;
567    let (sig_label, sig_bytes) = parse_signature(signature)?;
568
569    if input_label != sig_label {
570        return Err(HttpSigError::LabelMismatch);
571    }
572    if let Some(expected) = &config.label {
573        if expected != &input_label {
574            return Err(HttpSigError::LabelMismatch);
575        }
576    }
577    if config.wimse_profile {
578        check_request_profile(&params)?;
579    }
580    if config.wimse_response_profile {
581        check_response_profile(&params, config.expected_req_nonce.is_some())?;
582    }
583    if let Some(alg) = &params.alg {
584        if alg != ALG {
585            return Err(HttpSigError::UnsupportedAlg { found: alg.clone() });
586        }
587    }
588
589    let base = signature_base_from_params_str(message, &components, &params_value)?;
590    verifying_key
591        .verify(base.as_bytes(), &sig_bytes)
592        .map_err(|_| HttpSigError::InvalidSignature)?;
593
594    for required in &config.required_components {
595        if !components.contains(required) {
596            return Err(HttpSigError::MissingRequiredComponent(required.quoted_id()));
597        }
598    }
599    // Only now, with the parameters proven authentic, is `wimse-aud` worth
600    // acting on. Checking it earlier would decide authorization from data an
601    // attacker still controls, and would let a forged message be told apart by
602    // whether it guessed the audience.
603    if let Some(expected) = &config.expected_audience {
604        if params.wimse_aud.as_ref() != Some(expected) {
605            return Err(HttpSigError::AudienceMismatch);
606        }
607    }
608    // Section 3.4: a client that demanded a signed response MUST check the nonce
609    // comes back, which is what stops a response being replayed onto another
610    // request.
611    if let Some(expected) = &config.expected_req_nonce {
612        if params.wimse_req_nonce.as_ref() != Some(expected) {
613            return Err(HttpSigError::RequestNonceMismatch);
614        }
615    }
616
617    if let (Some(created), Some(expires)) = (params.created, params.expires) {
618        if expires < created {
619            return Err(HttpSigError::InvalidTimeWindow);
620        }
621    }
622    // A `max_age` without a `now` would silently skip the freshness check; fail
623    // closed rather than give a false sense of enforcement.
624    if config.max_age.is_some() && config.now.is_none() {
625        return Err(HttpSigError::TooOld);
626    }
627    if let Some(now) = config.now {
628        if let Some(expires) = params.expires {
629            if now > expires.saturating_add(config.leeway) {
630                return Err(HttpSigError::Expired);
631            }
632        }
633        if let Some(created) = params.created {
634            if created > now.saturating_add(config.leeway) {
635                return Err(HttpSigError::CreatedInFuture);
636            }
637        }
638        if let Some(max_age) = config.max_age {
639            let created = params.created.ok_or(HttpSigError::TooOld)?;
640            if now.saturating_sub(created) > max_age {
641                return Err(HttpSigError::TooOld);
642            }
643        }
644    }
645
646    Ok(VerifiedSignature {
647        label: input_label,
648        components,
649        params,
650    })
651}
652
653#[cfg(test)]
654mod tests {
655    use wimsey_jose::SigningKey;
656
657    use super::{sign, signature_base, verify, SignatureParams, VerifyConfig, ALG};
658    use crate::error::HttpSigError;
659    use crate::message::{Component, HttpExchange, HttpRequest};
660
661    // The canonical RFC 9421 test request (Section 2.5).
662    fn rfc_request() -> HttpRequest {
663        HttpRequest {
664            method: "POST".to_owned(),
665            authority: "example.com".to_owned(),
666            path: "/foo".to_owned(),
667            query: Some("param=Value&Pet=dog".to_owned()),
668            headers: vec![
669                ("Host".to_owned(), "example.com".to_owned()),
670                ("Date".to_owned(), "Tue, 20 Apr 2021 02:07:55 GMT".to_owned()),
671                ("Content-Type".to_owned(), "application/json".to_owned()),
672                (
673                    "Content-Digest".to_owned(),
674                    "sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:".to_owned(),
675                ),
676                ("Content-Length".to_owned(), "18".to_owned()),
677            ],
678        }
679    }
680
681    fn rfc_components() -> Vec<Component> {
682        vec![
683            Component::Method,
684            Component::Authority,
685            Component::Path,
686            Component::header("content-digest"),
687            Component::header("content-length"),
688            Component::header("content-type"),
689        ]
690    }
691
692    // Known-answer test: the signature base must match RFC 9421 Section 2.5
693    // byte-for-byte.
694    #[test]
695    fn signature_base_matches_rfc_9421() {
696        let params = SignatureParams {
697            created: Some(1_618_884_473),
698            keyid: Some("test-key-rsa-pss".to_owned()),
699            ..SignatureParams::default()
700        };
701        let base = signature_base(&rfc_request(), &rfc_components(), &params).unwrap();
702
703        let expected = concat!(
704            "\"@method\": POST\n",
705            "\"@authority\": example.com\n",
706            "\"@path\": /foo\n",
707            "\"content-digest\": sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:\n",
708            "\"content-length\": 18\n",
709            "\"content-type\": application/json\n",
710            "\"@signature-params\": (\"@method\" \"@authority\" \"@path\" \"content-digest\" \"content-length\" \"content-type\");created=1618884473;keyid=\"test-key-rsa-pss\""
711        );
712        assert_eq!(base, expected);
713    }
714
715    fn ed25519_params() -> SignatureParams {
716        SignatureParams {
717            created: Some(1_700_000_000),
718            keyid: Some("issuer-key-1".to_owned()),
719            alg: Some(ALG.to_owned()),
720            ..SignatureParams::default()
721        }
722    }
723
724    #[test]
725    fn round_trips() {
726        let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
727        let request = rfc_request();
728        let components = rfc_components();
729        let signed = sign(&request, &components, &ed25519_params(), "sig1", &key).unwrap();
730
731        let verified = verify(
732            &request,
733            &signed.signature_input,
734            &signed.signature,
735            &key.verifying_key(),
736            &VerifyConfig::default(),
737        )
738        .unwrap();
739        assert_eq!(verified.label, "sig1");
740        assert_eq!(verified.components, components);
741        assert_eq!(verified.params.keyid.as_deref(), Some("issuer-key-1"));
742    }
743
744    #[test]
745    fn rejects_a_tampered_request() {
746        let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
747        let mut request = rfc_request();
748        let components = rfc_components();
749        let signed = sign(&request, &components, &ed25519_params(), "sig1", &key).unwrap();
750
751        // Change a covered header after signing.
752        request
753            .headers
754            .push(("Content-Length".to_owned(), "19".to_owned()));
755        request
756            .headers
757            .retain(|(n, v)| !(n == "Content-Length" && v == "18"));
758
759        let err = verify(
760            &request,
761            &signed.signature_input,
762            &signed.signature,
763            &key.verifying_key(),
764            &VerifyConfig::default(),
765        );
766        assert!(matches!(err, Err(HttpSigError::InvalidSignature)));
767    }
768
769    #[test]
770    fn rejects_the_wrong_key() {
771        let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
772        let other = SigningKey::from_ed25519_seed(&[6u8; 32]);
773        let request = rfc_request();
774        let signed = sign(&request, &rfc_components(), &ed25519_params(), "sig1", &key).unwrap();
775
776        let err = verify(
777            &request,
778            &signed.signature_input,
779            &signed.signature,
780            &other.verifying_key(),
781            &VerifyConfig::default(),
782        );
783        assert!(matches!(err, Err(HttpSigError::InvalidSignature)));
784    }
785
786    #[test]
787    fn rejects_a_missing_covered_header() {
788        let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
789        let request = HttpRequest {
790            headers: vec![],
791            ..rfc_request()
792        };
793        let err = sign(&request, &rfc_components(), &ed25519_params(), "sig1", &key);
794        assert!(matches!(err, Err(HttpSigError::MissingComponent(_))));
795    }
796
797    #[test]
798    fn enforces_expiry() {
799        let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
800        let request = rfc_request();
801        let params = SignatureParams {
802            created: Some(1_700_000_000),
803            expires: Some(1_700_000_300),
804            keyid: Some("k".to_owned()),
805            alg: Some(ALG.to_owned()),
806            ..SignatureParams::default()
807        };
808        let signed = sign(&request, &rfc_components(), &params, "sig1", &key).unwrap();
809
810        let config = VerifyConfig {
811            now: Some(1_700_000_301),
812            ..VerifyConfig::default()
813        };
814        let err = verify(
815            &request,
816            &signed.signature_input,
817            &signed.signature,
818            &key.verifying_key(),
819            &config,
820        );
821        assert!(matches!(err, Err(HttpSigError::Expired)));
822    }
823
824    #[test]
825    fn rejects_a_label_mismatch() {
826        let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
827        let request = rfc_request();
828        let signed = sign(&request, &rfc_components(), &ed25519_params(), "sig1", &key).unwrap();
829
830        let config = VerifyConfig {
831            label: Some("other".to_owned()),
832            ..VerifyConfig::default()
833        };
834        let err = verify(
835            &request,
836            &signed.signature_input,
837            &signed.signature,
838            &key.verifying_key(),
839            &config,
840        );
841        assert!(matches!(err, Err(HttpSigError::LabelMismatch)));
842    }
843
844    #[test]
845    fn is_deterministic() {
846        let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
847        let request = rfc_request();
848        let components = rfc_components();
849        let a = sign(&request, &components, &ed25519_params(), "sig1", &key).unwrap();
850        let b = sign(&request, &components, &ed25519_params(), "sig1", &key).unwrap();
851        assert_eq!(a.signature_input, b.signature_input);
852        assert_eq!(a.signature, b.signature);
853    }
854
855    #[test]
856    fn rejects_a_missing_required_component() {
857        let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
858        let request = rfc_request();
859        // Signature covers method and path only.
860        let signed = sign(
861            &request,
862            &[Component::Method, Component::Path],
863            &ed25519_params(),
864            "sig1",
865            &key,
866        )
867        .unwrap();
868
869        let config = VerifyConfig {
870            required_components: vec![Component::header("content-digest")],
871            ..VerifyConfig::default()
872        };
873        let err = verify(
874            &request,
875            &signed.signature_input,
876            &signed.signature,
877            &key.verifying_key(),
878            &config,
879        );
880        assert!(matches!(
881            err,
882            Err(HttpSigError::MissingRequiredComponent(_))
883        ));
884    }
885
886    #[test]
887    fn rejects_a_non_ed25519_alg() {
888        let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
889        let request = rfc_request();
890        let params = SignatureParams {
891            created: Some(1_700_000_000),
892            keyid: Some("k".to_owned()),
893            alg: Some("rsa-pss".to_owned()),
894            ..SignatureParams::default()
895        };
896        let signed = sign(&request, &rfc_components(), &params, "sig1", &key).unwrap();
897
898        let err = verify(
899            &request,
900            &signed.signature_input,
901            &signed.signature,
902            &key.verifying_key(),
903            &VerifyConfig::default(),
904        );
905        assert!(matches!(err, Err(HttpSigError::UnsupportedAlg { .. })));
906    }
907
908    #[test]
909    fn rejects_crlf_in_a_covered_header() {
910        let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
911        let mut request = rfc_request();
912        request
913            .headers
914            .push(("X-Evil".to_owned(), "ok\n\"@path\": /evil".to_owned()));
915
916        let err = sign(
917            &request,
918            &[Component::Method, Component::header("x-evil")],
919            &ed25519_params(),
920            "sig1",
921            &key,
922        );
923        assert!(matches!(err, Err(HttpSigError::InvalidComponentValue(_))));
924    }
925
926    #[test]
927    fn rejects_an_inverted_time_window() {
928        let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
929        let request = rfc_request();
930        let params = SignatureParams {
931            created: Some(1_700_000_300),
932            expires: Some(1_700_000_000),
933            keyid: Some("k".to_owned()),
934            alg: Some(ALG.to_owned()),
935            ..SignatureParams::default()
936        };
937        let signed = sign(&request, &rfc_components(), &params, "sig1", &key).unwrap();
938
939        let err = verify(
940            &request,
941            &signed.signature_input,
942            &signed.signature,
943            &key.verifying_key(),
944            &VerifyConfig::default(),
945        );
946        assert!(matches!(err, Err(HttpSigError::InvalidTimeWindow)));
947    }
948
949    #[test]
950    fn enforces_max_age() {
951        let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
952        let request = rfc_request();
953        // created is 1_700_000_000.
954        let signed = sign(&request, &rfc_components(), &ed25519_params(), "sig1", &key).unwrap();
955
956        let config = VerifyConfig {
957            now: Some(1_700_000_400),
958            max_age: Some(60),
959            ..VerifyConfig::default()
960        };
961        let err = verify(
962            &request,
963            &signed.signature_input,
964            &signed.signature,
965            &key.verifying_key(),
966            &config,
967        );
968        assert!(matches!(err, Err(HttpSigError::TooOld)));
969    }
970
971    #[test]
972    fn tolerates_unknown_boolean_parameters() {
973        use base64::{engine::general_purpose::STANDARD, Engine};
974
975        let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
976        let request = rfc_request();
977        // A params value carrying a boolean parameter `;ext` (no value).
978        let params_value = "(\"@method\" \"@path\");created=1700000000;ext";
979        let base = format!(
980            "\"@method\": {}\n\"@path\": {}\n\"@signature-params\": {params_value}",
981            request.method, request.path,
982        );
983        let signature = STANDARD.encode(key.sign(base.as_bytes()));
984        let signature_input = format!("sig1={params_value}");
985        let signature = format!("sig1=:{signature}:");
986
987        let verified = verify(
988            &request,
989            &signature_input,
990            &signature,
991            &key.verifying_key(),
992            &VerifyConfig::default(),
993        )
994        .unwrap();
995        assert_eq!(verified.params.created, Some(1_700_000_000));
996    }
997
998    #[test]
999    fn content_digest_helper_binds_the_body() {
1000        use crate::message::{content_digest_sha256, verify_content_digest};
1001
1002        let body = br#"{"amount":100}"#;
1003        let header = content_digest_sha256(body);
1004        assert!(verify_content_digest(&header, body));
1005        assert!(!verify_content_digest(&header, b"tampered"));
1006    }
1007
1008    #[test]
1009    fn round_trips_params_with_quoted_delimiters() {
1010        // A keyid whose value legitimately contains `;`, `)` and `"` (all valid
1011        // inside an RFC 8941 string) must survive the round trip.
1012        let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
1013        let request = rfc_request();
1014        let params = SignatureParams {
1015            created: Some(1_700_000_000),
1016            keyid: Some("weird;key)with\"quote".to_owned()),
1017            alg: Some(ALG.to_owned()),
1018            ..SignatureParams::default()
1019        };
1020        let signed = sign(&request, &rfc_components(), &params, "sig1", &key).unwrap();
1021
1022        let verified = verify(
1023            &request,
1024            &signed.signature_input,
1025            &signed.signature,
1026            &key.verifying_key(),
1027            &VerifyConfig::default(),
1028        )
1029        .unwrap();
1030        assert_eq!(
1031            verified.params.keyid.as_deref(),
1032            Some("weird;key)with\"quote")
1033        );
1034    }
1035
1036    #[test]
1037    fn rejects_crlf_in_signature_params() {
1038        use base64::{engine::general_purpose::STANDARD, Engine};
1039
1040        let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
1041        let request = rfc_request();
1042        // A newline smuggled into the parameters (parses, but must be rejected).
1043        let signature_input = "sig1=(\"@method\")\n;created=1700000000";
1044        let signature = format!("sig1=:{}:", STANDARD.encode([0u8; 64]));
1045
1046        let err = verify(
1047            &request,
1048            signature_input,
1049            &signature,
1050            &key.verifying_key(),
1051            &VerifyConfig::default(),
1052        );
1053        assert!(matches!(err, Err(HttpSigError::Parse(_))));
1054    }
1055
1056    use base64::{engine::general_purpose::STANDARD, Engine};
1057
1058    use super::{check_request_profile, WIMSE_TAG};
1059
1060    /// A parameter set that satisfies every rule of the profile.
1061    fn wimse_params() -> SignatureParams {
1062        SignatureParams {
1063            created: Some(1_700_000_000),
1064            expires: Some(1_700_000_300),
1065            nonce: Some("abcd1111".to_owned()),
1066            tag: Some(WIMSE_TAG.to_owned()),
1067            wimse_aud: Some("https://svcb.example.com/gimme-ice-cream".to_owned()),
1068            ..SignatureParams::default()
1069        }
1070    }
1071
1072    fn wimse_config() -> VerifyConfig {
1073        VerifyConfig {
1074            wimse_profile: true,
1075            ..VerifyConfig::default()
1076        }
1077    }
1078
1079    fn sign_with(params: &SignatureParams) -> (SigningKey, HttpRequest, super::SignedSignature) {
1080        let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
1081        let request = rfc_request();
1082        let signed = sign(&request, &rfc_components(), params, "wimse", &key).unwrap();
1083        (key, request, signed)
1084    }
1085
1086    #[test]
1087    fn accepts_a_profile_conforming_signature() {
1088        let (key, request, signed) = sign_with(&wimse_params());
1089        let verified = verify(
1090            &request,
1091            &signed.signature_input,
1092            &signed.signature,
1093            &key.verifying_key(),
1094            &wimse_config(),
1095        )
1096        .unwrap();
1097        assert_eq!(verified.params.tag.as_deref(), Some(WIMSE_TAG));
1098    }
1099
1100    // `keyid` and `alg` MUST NOT be used: the key travels in the WIT and the
1101    // algorithm is pinned by that WIT's `cnf` JWK.
1102    #[test]
1103    fn profile_rejects_keyid_and_alg() {
1104        for (label, params) in [
1105            (
1106                "keyid",
1107                SignatureParams {
1108                    keyid: Some("k".to_owned()),
1109                    ..wimse_params()
1110                },
1111            ),
1112            (
1113                "alg",
1114                SignatureParams {
1115                    alg: Some(ALG.to_owned()),
1116                    ..wimse_params()
1117                },
1118            ),
1119        ] {
1120            let (key, request, signed) = sign_with(&params);
1121            let err = verify(
1122                &request,
1123                &signed.signature_input,
1124                &signed.signature,
1125                &key.verifying_key(),
1126                &wimse_config(),
1127            );
1128            assert!(
1129                matches!(err, Err(HttpSigError::ForbiddenParameter(p)) if p == label),
1130                "expected `{label}` to be rejected, got {err:?}"
1131            );
1132        }
1133    }
1134
1135    #[test]
1136    fn profile_requires_the_mandatory_parameters() {
1137        let cases: [(&str, SignatureParams); 5] = [
1138            (
1139                "created",
1140                SignatureParams {
1141                    created: None,
1142                    ..wimse_params()
1143                },
1144            ),
1145            (
1146                "expires",
1147                SignatureParams {
1148                    expires: None,
1149                    ..wimse_params()
1150                },
1151            ),
1152            (
1153                "nonce",
1154                SignatureParams {
1155                    nonce: None,
1156                    ..wimse_params()
1157                },
1158            ),
1159            (
1160                "tag",
1161                SignatureParams {
1162                    tag: None,
1163                    ..wimse_params()
1164                },
1165            ),
1166            (
1167                "wimse-aud",
1168                SignatureParams {
1169                    wimse_aud: None,
1170                    ..wimse_params()
1171                },
1172            ),
1173        ];
1174        for (name, params) in cases {
1175            let err = check_request_profile(&params);
1176            assert!(
1177                matches!(err, Err(HttpSigError::MissingParameter(p)) if p == name),
1178                "expected `{name}` to be required, got {err:?}"
1179            );
1180        }
1181    }
1182
1183    #[test]
1184    fn profile_rejects_a_foreign_tag() {
1185        let params = SignatureParams {
1186            tag: Some("something-else".to_owned()),
1187            ..wimse_params()
1188        };
1189        let (key, request, signed) = sign_with(&params);
1190        let err = verify(
1191            &request,
1192            &signed.signature_input,
1193            &signed.signature,
1194            &key.verifying_key(),
1195            &wimse_config(),
1196        );
1197        assert!(matches!(err, Err(HttpSigError::WrongTag { .. })));
1198    }
1199
1200    // A signature is only bound to this service if its `wimse-aud` is checked;
1201    // one minted for a peer must not verify here.
1202    #[test]
1203    fn rejects_a_signature_minted_for_another_audience() {
1204        let (key, request, signed) = sign_with(&wimse_params());
1205        let config = VerifyConfig {
1206            expected_audience: Some("https://svcc.example.com/other".to_owned()),
1207            ..wimse_config()
1208        };
1209        let err = verify(
1210            &request,
1211            &signed.signature_input,
1212            &signed.signature,
1213            &key.verifying_key(),
1214            &config,
1215        );
1216        assert!(matches!(err, Err(HttpSigError::AudienceMismatch)));
1217    }
1218
1219    // The audience must be judged only after the signature proves the parameters
1220    // authentic. A forged message must report the forgery, not whether it
1221    // happened to guess the audience — otherwise an attacker who cannot sign
1222    // anything can still probe for the audience a service answers to.
1223    #[test]
1224    fn reports_a_forgery_as_invalid_regardless_of_audience() {
1225        let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
1226        let request = rfc_request();
1227        let (_, _, signed) = sign_with(&wimse_params());
1228        let forged = format!("wimse=:{}:", STANDARD.encode([0u8; 64]));
1229
1230        for audience in [
1231            "https://svcb.example.com/gimme-ice-cream",
1232            "https://wrong.example/inbox",
1233        ] {
1234            let config = VerifyConfig {
1235                expected_audience: Some(audience.to_owned()),
1236                ..wimse_config()
1237            };
1238            let err = verify(
1239                &request,
1240                &signed.signature_input,
1241                &forged,
1242                &key.verifying_key(),
1243                &config,
1244            );
1245            assert!(
1246                matches!(err, Err(HttpSigError::InvalidSignature)),
1247                "expected InvalidSignature for audience {audience}, got {err:?}"
1248            );
1249        }
1250    }
1251
1252    #[test]
1253    fn accepts_the_matching_audience() {
1254        let (key, request, signed) = sign_with(&wimse_params());
1255        let config = VerifyConfig {
1256            expected_audience: Some("https://svcb.example.com/gimme-ice-cream".to_owned()),
1257            ..wimse_config()
1258        };
1259        assert!(verify(
1260            &request,
1261            &signed.signature_input,
1262            &signed.signature,
1263            &key.verifying_key(),
1264            &config,
1265        )
1266        .is_ok());
1267    }
1268
1269    #[test]
1270    fn serializes_sign_response_as_a_bare_boolean() {
1271        let params = SignatureParams {
1272            wimse_sign_response: Some(true),
1273            ..wimse_params()
1274        };
1275        let (key, request, signed) = sign_with(&params);
1276        assert!(signed.signature_input.ends_with(";wimse-sign-response"));
1277
1278        let verified = verify(
1279            &request,
1280            &signed.signature_input,
1281            &signed.signature,
1282            &key.verifying_key(),
1283            &wimse_config(),
1284        )
1285        .unwrap();
1286        assert_eq!(verified.params.wimse_sign_response, Some(true));
1287    }
1288
1289    #[test]
1290    fn round_trips_an_explicit_false_sign_response() {
1291        let params = SignatureParams {
1292            wimse_sign_response: Some(false),
1293            ..wimse_params()
1294        };
1295        let (key, request, signed) = sign_with(&params);
1296        assert!(signed.signature_input.ends_with(";wimse-sign-response=?0"));
1297
1298        let verified = verify(
1299            &request,
1300            &signed.signature_input,
1301            &signed.signature,
1302            &key.verifying_key(),
1303            &wimse_config(),
1304        )
1305        .unwrap();
1306        assert_eq!(verified.params.wimse_sign_response, Some(false));
1307    }
1308
1309    #[test]
1310    fn round_trips_the_response_nonce_binding() {
1311        let params = SignatureParams {
1312            wimse_req_nonce: Some("abcd1111".to_owned()),
1313            ..wimse_params()
1314        };
1315        let (key, request, signed) = sign_with(&params);
1316        let verified = verify(
1317            &request,
1318            &signed.signature_input,
1319            &signed.signature,
1320            &key.verifying_key(),
1321            &wimse_config(),
1322        )
1323        .unwrap();
1324        assert_eq!(verified.params.wimse_req_nonce.as_deref(), Some("abcd1111"));
1325    }
1326
1327    // The profile is opt-in: a plain RFC 9421 signature must still verify with
1328    // the default config, which is what the known-answer test above relies on.
1329    #[test]
1330    fn profile_is_off_by_default() {
1331        let (key, request, signed) = sign_with(&ed25519_params());
1332        assert!(verify(
1333            &request,
1334            &signed.signature_input,
1335            &signed.signature,
1336            &key.verifying_key(),
1337            &VerifyConfig::default(),
1338        )
1339        .is_ok());
1340    }
1341
1342    use crate::message::HttpResponse;
1343
1344    fn rfc_response() -> HttpResponse {
1345        HttpResponse {
1346            status: 200,
1347            headers: vec![
1348                ("Content-Type".to_owned(), "application/json".to_owned()),
1349                (
1350                    "Workload-Identity-Token".to_owned(),
1351                    "eyJ0eXAi.wit.value".to_owned(),
1352                ),
1353            ],
1354        }
1355    }
1356
1357    fn response_params() -> SignatureParams {
1358        SignatureParams {
1359            created: Some(1_700_000_000),
1360            expires: Some(1_700_000_300),
1361            nonce: Some("resp-2222".to_owned()),
1362            tag: Some(WIMSE_TAG.to_owned()),
1363            wimse_req_nonce: Some("abcd1111".to_owned()),
1364            ..SignatureParams::default()
1365        }
1366    }
1367
1368    fn response_config() -> VerifyConfig {
1369        VerifyConfig {
1370            wimse_response_profile: true,
1371            expected_req_nonce: Some("abcd1111".to_owned()),
1372            ..VerifyConfig::default()
1373        }
1374    }
1375
1376    #[test]
1377    fn signs_and_verifies_a_response() {
1378        let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
1379        let request = rfc_request();
1380        let response = rfc_response();
1381        let exchange = HttpExchange {
1382            response: &response,
1383            request: &request,
1384        };
1385        let components = super::response_components(&response.headers);
1386        let signed = sign(&exchange, &components, &response_params(), "wimse", &key).unwrap();
1387
1388        assert!(signed
1389            .signature_input
1390            .contains(r#""@status" "@method";req "@request-target";req"#));
1391
1392        let verified = verify(
1393            &exchange,
1394            &signed.signature_input,
1395            &signed.signature,
1396            &key.verifying_key(),
1397            &response_config(),
1398        )
1399        .unwrap();
1400        assert_eq!(verified.params.wimse_req_nonce.as_deref(), Some("abcd1111"));
1401    }
1402
1403    #[test]
1404    fn a_response_cannot_be_lifted_onto_another_request() {
1405        let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
1406        let request = rfc_request();
1407        let response = rfc_response();
1408        let signed = sign(
1409            &HttpExchange {
1410                response: &response,
1411                request: &request,
1412            },
1413            &super::response_components(&response.headers),
1414            &response_params(),
1415            "wimse",
1416            &key,
1417        )
1418        .unwrap();
1419
1420        let other_request = HttpRequest {
1421            path: "/admin".to_owned(),
1422            ..rfc_request()
1423        };
1424        let err = verify(
1425            &HttpExchange {
1426                response: &response,
1427                request: &other_request,
1428            },
1429            &signed.signature_input,
1430            &signed.signature,
1431            &key.verifying_key(),
1432            &response_config(),
1433        );
1434        assert!(matches!(err, Err(HttpSigError::InvalidSignature)));
1435    }
1436
1437    #[test]
1438    fn rejects_a_response_answering_a_different_request() {
1439        let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
1440        let request = rfc_request();
1441        let response = rfc_response();
1442        let exchange = HttpExchange {
1443            response: &response,
1444            request: &request,
1445        };
1446        let signed = sign(
1447            &exchange,
1448            &super::response_components(&response.headers),
1449            &response_params(),
1450            "wimse",
1451            &key,
1452        )
1453        .unwrap();
1454
1455        let config = VerifyConfig {
1456            expected_req_nonce: Some("some-other-nonce".to_owned()),
1457            ..response_config()
1458        };
1459        let err = verify(
1460            &exchange,
1461            &signed.signature_input,
1462            &signed.signature,
1463            &key.verifying_key(),
1464            &config,
1465        );
1466        assert!(matches!(err, Err(HttpSigError::RequestNonceMismatch)));
1467    }
1468
1469    #[test]
1470    fn response_profile_requires_the_returned_nonce() {
1471        let params = SignatureParams {
1472            wimse_req_nonce: None,
1473            ..response_params()
1474        };
1475        assert!(matches!(
1476            super::check_response_profile(&params, true),
1477            Err(HttpSigError::MissingParameter("wimse-req-nonce"))
1478        ));
1479        // ...but only when the client demanded a signed response.
1480        assert!(super::check_response_profile(&params, false).is_ok());
1481    }
1482
1483    // Forbidden rather than ignored: silently accepting it would hide a sender
1484    // that thinks it is still addressing someone.
1485    #[test]
1486    fn response_profile_forbids_the_request_audience() {
1487        let params = SignatureParams {
1488            wimse_aud: Some("https://svcb.example.com/x".to_owned()),
1489            ..response_params()
1490        };
1491        assert!(matches!(
1492            super::check_response_profile(&params, true),
1493            Err(HttpSigError::ForbiddenParameter("wimse-aud"))
1494        ));
1495    }
1496
1497    #[test]
1498    fn a_request_has_no_status_component() {
1499        let err = rfc_request().component_value(&Component::Status);
1500        assert!(matches!(err, Err(HttpSigError::UnsupportedComponent(_))));
1501    }
1502
1503    #[test]
1504    fn rejects_max_age_without_now() {
1505        let key = SigningKey::from_ed25519_seed(&[5u8; 32]);
1506        let request = rfc_request();
1507        let signed = sign(&request, &rfc_components(), &ed25519_params(), "sig1", &key).unwrap();
1508
1509        // `max_age` set but `now` unset must fail closed, not silently skip.
1510        let config = VerifyConfig {
1511            max_age: Some(60),
1512            ..VerifyConfig::default()
1513        };
1514        let err = verify(
1515            &request,
1516            &signed.signature_input,
1517            &signed.signature,
1518            &key.verifying_key(),
1519            &config,
1520        );
1521        assert!(matches!(err, Err(HttpSigError::TooOld)));
1522    }
1523}