Skip to main content

actpub_httpsig/rfc9421/
signature.rs

1//! Parsing and emitting the RFC 9421 `Signature:` header.
2//!
3//! The header is a Structured Field dictionary whose values are byte
4//! sequences carrying the raw signature bytes:
5//!
6//! ```text
7//! Signature: sig1=:<base64-signature>:, sig2=:<base64>:
8//! ```
9//!
10//! Each label corresponds one-to-one with a label in the paired
11//! `Signature-Input:` header. Callers must look up both when verifying.
12
13use sfv::{BareItem, Dictionary, FieldType, Item, Key, ListEntry, Parser};
14
15use crate::error::Error;
16
17/// Name of the `Signature:` HTTP header, matching the Cavage spelling so
18/// that `crate::verify` can dispatch between the two flavours on a single
19/// header lookup.
20pub const SIGNATURE_HEADER: &str = "signature";
21
22/// Parses the raw `Signature:` header into an ordered list of
23/// `(label, signature-bytes)` pairs.
24///
25/// # Errors
26///
27/// Returns [`Error::InvalidHeader`] on sf-dictionary parse failure and
28/// [`Error::MalformedSignatureHeader`] if any entry is not a byte-seq
29/// item.
30pub fn parse_signature_dict(raw: &str) -> Result<Vec<(String, Vec<u8>)>, Error> {
31    let dict: Dictionary =
32        Parser::new(raw)
33            .parse()
34            .map_err(|e: sfv::Error| Error::InvalidHeader {
35                name: SIGNATURE_HEADER,
36                reason: e.to_string(),
37            })?;
38
39    let mut out = Vec::with_capacity(dict.len());
40    for (label, entry) in dict {
41        let item = match entry {
42            ListEntry::Item(item) => item,
43            ListEntry::InnerList(_) => {
44                return Err(Error::MalformedSignatureHeader(format!(
45                    "entry `{label}` must be a byte-sequence item, not an inner list"
46                )));
47            }
48        };
49        let BareItem::ByteSequence(bytes) = item.bare_item else {
50            return Err(Error::MalformedSignatureHeader(format!(
51                "entry `{label}` must be a byte sequence"
52            )));
53        };
54        out.push((label.into(), bytes));
55    }
56
57    Ok(out)
58}
59
60/// Serialises a list of `(label, bytes)` pairs into a
61/// `Signature:`-compatible value.
62///
63/// # Panics
64///
65/// Panics only if a label fails sf-key validation, or if `sfv` fails to
66/// serialise an all-byte-sequence dictionary. Both are unreachable for
67/// the inputs this crate constructs.
68#[must_use]
69#[allow(
70    clippy::expect_used,
71    reason = "serialising an all-byte-sequence dictionary under validated keys cannot fail"
72)]
73pub fn serialise_signature_dict(entries: &[(String, Vec<u8>)]) -> String {
74    let mut dict = Dictionary::new();
75    for (label, bytes) in entries {
76        let key = Key::try_from(label.clone()).expect("signature label must be a valid sf-key");
77        dict.insert(
78            key,
79            ListEntry::Item(Item::new(BareItem::ByteSequence(bytes.clone()))),
80        );
81    }
82    FieldType::serialize(&dict).expect("byte-sequence dictionary is always serialisable")
83}
84
85#[cfg(test)]
86mod tests {
87    use pretty_assertions::assert_eq;
88
89    use super::*;
90
91    #[test]
92    fn roundtrip_single_entry() {
93        let sig_bytes = vec![0x01, 0x02, 0x03, 0x04];
94        let wire = serialise_signature_dict(&[("sig1".into(), sig_bytes.clone())]);
95        assert_eq!(wire, "sig1=:AQIDBA==:");
96        let parsed = parse_signature_dict(&wire).expect("parse");
97        assert_eq!(parsed, vec![("sig1".into(), sig_bytes)]);
98    }
99
100    #[test]
101    fn roundtrip_multiple_entries_preserves_order() {
102        let entries = vec![
103            ("sig1".to_owned(), vec![0u8; 32]),
104            ("sig2".to_owned(), vec![0xFFu8; 16]),
105        ];
106        let wire = serialise_signature_dict(&entries);
107        let parsed = parse_signature_dict(&wire).expect("parse");
108        assert_eq!(parsed, entries);
109    }
110
111    #[test]
112    fn inner_list_entry_is_rejected() {
113        let err = parse_signature_dict("sig1=(\"@method\")").expect_err("inner list");
114        assert!(matches!(err, Error::MalformedSignatureHeader(_)));
115    }
116
117    #[test]
118    fn non_byte_sequence_is_rejected() {
119        let err = parse_signature_dict("sig1=123").expect_err("integer");
120        assert!(matches!(err, Error::MalformedSignatureHeader(_)));
121    }
122}