Skip to main content

actpub_httpsig/rfc9421/
signature_input.rs

1//! Parsing and emitting the `Signature-Input:` header.
2//!
3//! Per RFC 9421 §4.1, this header is a Structured Field dictionary of
4//! inner lists with parameters:
5//!
6//! ```text
7//! Signature-Input: sig1=("@method" "@target-uri" "host");keyid="kid";created=1700000000
8//! ```
9//!
10//! Each entry is identified by a caller-chosen label (`sig1` by
11//! convention); a single request may carry multiple labels so that
12//! middle boxes can attach their own signatures.
13
14use sfv::{BareItem, Dictionary, ListEntry, Parser};
15
16use crate::error::Error;
17use crate::rfc9421::components::Component;
18
19/// Name of the `Signature-Input:` HTTP header.
20pub const SIGNATURE_INPUT_HEADER: &str = "signature-input";
21
22/// Canonical parameter names defined by RFC 9421 §2.3.
23mod param {
24    pub const KEYID: &str = "keyid";
25    pub const ALG: &str = "alg";
26    pub const CREATED: &str = "created";
27    pub const EXPIRES: &str = "expires";
28    pub const NONCE: &str = "nonce";
29    pub const TAG: &str = "tag";
30}
31
32/// One entry of the `Signature-Input:` dictionary: the ordered component
33/// list plus parameters.
34#[derive(Debug, Clone, PartialEq, Eq)]
35#[non_exhaustive]
36pub struct SignatureInput {
37    /// Components covered by the signature, in signing order.
38    pub components: Vec<Component>,
39    /// `keyid=` parameter (mandatory for `ActivityPub` use).
40    pub keyid: Option<String>,
41    /// `alg=` parameter hint; `None` means "detect from the resolved key".
42    pub algorithm: Option<String>,
43    /// `created=` parameter in seconds since the UNIX epoch.
44    pub created: Option<i64>,
45    /// `expires=` parameter in seconds since the UNIX epoch.
46    pub expires: Option<i64>,
47    /// `nonce=` parameter as emitted by the signer, opaque to us.
48    pub nonce: Option<String>,
49    /// `tag=` parameter as emitted by the signer, opaque to us.
50    pub tag: Option<String>,
51}
52
53impl SignatureInput {
54    /// Creates a [`SignatureInput`] covering the given components, with
55    /// every optional parameter left unset. Use the `with_*` builders
56    /// below to populate `keyid`, `created`, `expires`, `nonce` and
57    /// `tag` as needed.
58    #[must_use]
59    pub const fn new(components: Vec<Component>) -> Self {
60        Self {
61            components,
62            keyid: None,
63            algorithm: None,
64            created: None,
65            expires: None,
66            nonce: None,
67            tag: None,
68        }
69    }
70
71    /// Sets the `keyid=` parameter.
72    #[must_use]
73    pub fn with_keyid(mut self, keyid: impl Into<String>) -> Self {
74        self.keyid = Some(keyid.into());
75        self
76    }
77
78    /// Sets the `alg=` parameter.
79    #[must_use]
80    pub fn with_algorithm(mut self, algorithm: impl Into<String>) -> Self {
81        self.algorithm = Some(algorithm.into());
82        self
83    }
84
85    /// Sets the `created=` parameter (seconds since UNIX epoch).
86    #[must_use]
87    pub const fn with_created(mut self, created: i64) -> Self {
88        self.created = Some(created);
89        self
90    }
91
92    /// Sets the `expires=` parameter (seconds since UNIX epoch).
93    #[must_use]
94    pub const fn with_expires(mut self, expires: i64) -> Self {
95        self.expires = Some(expires);
96        self
97    }
98
99    /// Sets the `nonce=` parameter.
100    #[must_use]
101    pub fn with_nonce(mut self, nonce: impl Into<String>) -> Self {
102        self.nonce = Some(nonce.into());
103        self
104    }
105
106    /// Sets the `tag=` parameter.
107    #[must_use]
108    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
109        self.tag = Some(tag.into());
110        self
111    }
112
113    /// Serialises this entry as the inner-list-with-parameters portion
114    /// that appears after `label=`. The full header value is built by
115    /// [`serialise_signature_input_dict`].
116    ///
117    /// # Panics
118    ///
119    /// Panics only if `sfv` fails to serialise a well-formed inner list;
120    /// this is unreachable for the inputs we construct.
121    #[must_use]
122    #[allow(
123        clippy::expect_used,
124        reason = "serialising a well-formed InnerList cannot fail"
125    )]
126    pub fn serialise_inner_list(&self) -> String {
127        use core::fmt::Write as _;
128        let mut out = String::new();
129        out.push('(');
130        for (i, c) in self.components.iter().enumerate() {
131            if i > 0 {
132                out.push(' ');
133            }
134            out.push_str(&c.lexical());
135        }
136        out.push(')');
137        // Parameter order matches RFC 9421 §2.3 (and the order observed
138        // in the Appendix B test vectors): created, expires, nonce,
139        // alg, keyid, tag. Wire-compatible verifiers treat the
140        // dictionary as order-insensitive, but matching the RFC makes
141        // byte-level conformance tests pass out of the box.
142        let infallible = "writing to an owned String is infallible";
143        if let Some(c) = self.created {
144            write!(out, ";created={c}").expect(infallible);
145        }
146        if let Some(e) = self.expires {
147            write!(out, ";expires={e}").expect(infallible);
148        }
149        if let Some(n) = &self.nonce {
150            write!(out, r#";nonce="{n}""#).expect(infallible);
151        }
152        if let Some(alg) = &self.algorithm {
153            write!(out, r#";alg="{alg}""#).expect(infallible);
154        }
155        if let Some(keyid) = &self.keyid {
156            write!(out, r#";keyid="{keyid}""#).expect(infallible);
157        }
158        if let Some(t) = &self.tag {
159            write!(out, r#";tag="{t}""#).expect(infallible);
160        }
161        out
162    }
163}
164
165/// Parses the raw `Signature-Input:` header into a sequence of
166/// (label, [`SignatureInput`]) pairs, preserving insertion order.
167///
168/// # Errors
169///
170/// Returns [`Error::InvalidHeader`] if the header is not a valid sf-dict,
171/// and [`Error::MalformedSignatureHeader`] if any entry's components or
172/// parameters are malformed.
173pub fn parse_signature_input_dict(raw: &str) -> Result<Vec<(String, SignatureInput)>, Error> {
174    let dict: Dictionary =
175        Parser::new(raw)
176            .parse()
177            .map_err(|e: sfv::Error| Error::InvalidHeader {
178                name: SIGNATURE_INPUT_HEADER,
179                reason: e.to_string(),
180            })?;
181
182    let mut out = Vec::with_capacity(dict.len());
183    for (label, entry) in dict {
184        let inner_list = match entry {
185            ListEntry::InnerList(il) => il,
186            ListEntry::Item(_) => {
187                return Err(Error::MalformedSignatureHeader(format!(
188                    "entry `{label}` must be an inner list of components"
189                )));
190            }
191        };
192
193        let components: Vec<Component> = inner_list
194            .items
195            .iter()
196            .map(|item| {
197                let BareItem::String(s) = &item.bare_item else {
198                    return Err(Error::MalformedSignatureHeader(format!(
199                        "entry `{label}` contains a non-string component"
200                    )));
201                };
202                Component::parse(s.as_str())
203            })
204            .collect::<Result<_, _>>()?;
205
206        let label_str = label.as_str();
207        let mut input = SignatureInput {
208            components,
209            keyid: None,
210            algorithm: None,
211            created: None,
212            expires: None,
213            nonce: None,
214            tag: None,
215        };
216
217        for (pname, pvalue) in &inner_list.params {
218            match pname.as_str() {
219                param::KEYID => input.keyid = string_param(pvalue, label_str, param::KEYID)?,
220                param::ALG => input.algorithm = string_param(pvalue, label_str, param::ALG)?,
221                param::CREATED => {
222                    input.created = integer_param(pvalue, label_str, param::CREATED)?;
223                }
224                param::EXPIRES => {
225                    input.expires = integer_param(pvalue, label_str, param::EXPIRES)?;
226                }
227                param::NONCE => input.nonce = string_param(pvalue, label_str, param::NONCE)?,
228                param::TAG => input.tag = string_param(pvalue, label_str, param::TAG)?,
229                _ => {
230                    // Unknown parameters are tolerated per §2.3.
231                }
232            }
233        }
234
235        out.push((label.into(), input));
236    }
237
238    Ok(out)
239}
240
241fn string_param(value: &BareItem, label: &str, param: &str) -> Result<Option<String>, Error> {
242    match value {
243        BareItem::String(s) => Ok(Some(s.as_str().to_owned())),
244        _ => Err(Error::MalformedSignatureHeader(format!(
245            "entry `{label}` has non-string `{param}` parameter"
246        ))),
247    }
248}
249
250fn integer_param(value: &BareItem, label: &str, param: &str) -> Result<Option<i64>, Error> {
251    match value {
252        BareItem::Integer(n) => Ok(Some(i64::from(*n))),
253        _ => Err(Error::MalformedSignatureHeader(format!(
254            "entry `{label}` has non-integer `{param}` parameter"
255        ))),
256    }
257}
258
259/// Serialises a `(label, SignatureInput)` sequence into a single header
260/// value suitable for inserting into an `http::Request`.
261///
262/// # Panics
263///
264/// Panics only if `sfv` fails to serialise a well-formed dictionary; this
265/// is unreachable for the inputs we construct.
266#[must_use]
267#[allow(
268    clippy::expect_used,
269    reason = "serialising a well-formed sf-dictionary cannot fail"
270)]
271pub fn serialise_signature_input_dict(entries: &[(String, SignatureInput)]) -> String {
272    let mut out = String::new();
273    for (i, (label, input)) in entries.iter().enumerate() {
274        if i > 0 {
275            out.push_str(", ");
276        }
277        out.push_str(label);
278        out.push('=');
279        out.push_str(&input.serialise_inner_list());
280    }
281    out
282}
283
284#[cfg(test)]
285mod tests {
286    use pretty_assertions::assert_eq;
287
288    use super::*;
289
290    #[test]
291    fn serialise_matches_rfc9421_example() {
292        // Parameter order mirrors the RFC 9421 Appendix B conventions:
293        // `created` before `keyid`.
294        let input = SignatureInput::new(vec![
295            Component::Method,
296            Component::TargetUri,
297            Component::Header("host".into()),
298            Component::Header("date".into()),
299        ])
300        .with_keyid("test-key-rsa")
301        .with_created(1_618_884_473);
302        let dict = serialise_signature_input_dict(&[("sig1".into(), input)]);
303        assert_eq!(
304            dict,
305            r#"sig1=("@method" "@target-uri" "host" "date");created=1618884473;keyid="test-key-rsa""#,
306        );
307    }
308
309    #[test]
310    fn parse_roundtrips_through_serialise() {
311        let input = SignatureInput::new(vec![Component::Method, Component::Authority])
312            .with_keyid("kid")
313            .with_algorithm("ed25519")
314            .with_created(1_700_000_000)
315            .with_expires(1_700_000_600)
316            .with_nonce("abc")
317            .with_tag("mastodon");
318        let wire = serialise_signature_input_dict(&[("sig".into(), input.clone())]);
319        let parsed = parse_signature_input_dict(&wire).expect("parse");
320        assert_eq!(parsed.len(), 1);
321        assert_eq!(parsed[0].0, "sig");
322        assert_eq!(parsed[0].1, input);
323    }
324
325    #[test]
326    fn entry_of_wrong_shape_is_rejected() {
327        // `sig1` is a bare token here, not an inner list.
328        let wire = "sig1=123";
329        let err = parse_signature_input_dict(wire).expect_err("wrong shape");
330        assert!(matches!(err, Error::MalformedSignatureHeader(_)));
331    }
332
333    #[test]
334    fn unknown_parameters_are_tolerated() {
335        let wire = r#"sig1=("@method");keyid="kid";future_param=42"#;
336        let parsed = parse_signature_input_dict(wire).expect("parse");
337        assert_eq!(parsed[0].1.keyid.as_deref(), Some("kid"));
338    }
339
340    #[test]
341    fn non_string_component_is_rejected() {
342        // Components must be quoted strings, not tokens or integers.
343        let wire = "sig1=(foo)";
344        let err = parse_signature_input_dict(wire).expect_err("non-string component");
345        assert!(matches!(err, Error::MalformedSignatureHeader(_)));
346    }
347}