Skip to main content

actpub_httpsig/rfc9421/
sign.rs

1//! RFC 9421 request signer.
2
3use http::Request;
4use http::header::HeaderValue;
5
6use crate::error::Error;
7use crate::key::{Algorithm, SigningKey};
8use crate::rfc9421::components::{Component, build_signature_base};
9use crate::rfc9421::signature::{SIGNATURE_HEADER, serialise_signature_dict};
10use crate::rfc9421::signature_input::{
11    SIGNATURE_INPUT_HEADER, SignatureInput, serialise_signature_input_dict,
12};
13
14/// Default component sequence emitted by [`Rfc9421Signer::new`].
15///
16/// Chosen to mirror the Cavage default, making side-by-side dual-stack
17/// signing cheap (both signers cover the same semantic ground).
18pub const DEFAULT_COMPONENTS: &[&str] = &["@method", "@target-uri", "host", "date", "digest"];
19
20/// A request signer that produces RFC 9421 `Signature-Input:` and
21/// `Signature:` headers.
22#[derive(Debug)]
23pub struct Rfc9421Signer<'a> {
24    key: &'a SigningKey,
25    key_id: &'a str,
26    label: String,
27    components: Vec<Component>,
28    created: Option<i64>,
29    expires: Option<i64>,
30    emit_alg: bool,
31    nonce: Option<String>,
32    tag: Option<String>,
33}
34
35impl<'a> Rfc9421Signer<'a> {
36    /// Creates a signer with the [`DEFAULT_COMPONENTS`] layout, label
37    /// `"sig1"`, and `alg=` emitted for compatibility.
38    ///
39    /// # Panics
40    ///
41    /// Panics if any entry in [`DEFAULT_COMPONENTS`] fails to parse as
42    /// a valid identifier. The default list is a compile-time constant,
43    /// so this is unreachable at runtime.
44    #[must_use]
45    pub fn new(key: &'a SigningKey, key_id: &'a str) -> Self {
46        #[allow(
47            clippy::expect_used,
48            reason = "the DEFAULT_COMPONENTS constant contains only valid identifiers"
49        )]
50        let components = DEFAULT_COMPONENTS
51            .iter()
52            .map(|ident| Component::parse(ident).expect("valid default component"))
53            .collect();
54        Self {
55            key,
56            key_id,
57            label: "sig1".into(),
58            components,
59            created: None,
60            expires: None,
61            emit_alg: true,
62            nonce: None,
63            tag: None,
64        }
65    }
66
67    /// Replaces the full component list.
68    #[must_use]
69    pub fn with_components(mut self, components: Vec<Component>) -> Self {
70        self.components = components;
71        self
72    }
73
74    /// Replaces the `Signature-Input:` label (default `"sig1"`).
75    #[must_use]
76    pub fn with_label(mut self, label: impl Into<String>) -> Self {
77        self.label = label.into();
78        self
79    }
80
81    /// Sets the `created=` parameter.
82    #[must_use]
83    pub const fn with_created(mut self, seconds: i64) -> Self {
84        self.created = Some(seconds);
85        self
86    }
87
88    /// Sets the `expires=` parameter.
89    #[must_use]
90    pub const fn with_expires(mut self, seconds: i64) -> Self {
91        self.expires = Some(seconds);
92        self
93    }
94
95    /// Sets the `nonce=` parameter.
96    #[must_use]
97    pub fn with_nonce(mut self, nonce: impl Into<String>) -> Self {
98        self.nonce = Some(nonce.into());
99        self
100    }
101
102    /// Sets the `tag=` parameter.
103    #[must_use]
104    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
105        self.tag = Some(tag.into());
106        self
107    }
108
109    /// Controls whether the `alg=` parameter is emitted. Defaults to
110    /// `true`; set to `false` to match RFC 9421 ยง3.3.7's stated
111    /// preference for relying on out-of-band key agreement instead.
112    #[must_use]
113    pub const fn emit_alg(mut self, emit: bool) -> Self {
114        self.emit_alg = emit;
115        self
116    }
117
118    /// Signs `req` and inserts `Signature-Input:` and `Signature:` headers.
119    ///
120    /// # Errors
121    ///
122    /// Returns [`Error::RequiredHeaderAbsent`] if the request is missing
123    /// any referenced header, [`Error::Crypto`] if the signing primitive
124    /// fails, and [`Error::InvalidHeader`] if the resulting header value
125    /// cannot be converted to an [`http::HeaderValue`] (extremely rare,
126    /// only if the key id contains non-ASCII bytes).
127    pub fn sign<B>(&self, req: &mut Request<B>) -> Result<(), Error> {
128        let input = SignatureInput {
129            components: self.components.clone(),
130            keyid: Some(self.key_id.to_owned()),
131            algorithm: self.emit_alg.then(|| algorithm_name(self.key)),
132            created: self.created,
133            expires: self.expires,
134            nonce: self.nonce.clone(),
135            tag: self.tag.clone(),
136        };
137        let inner_list = input.serialise_inner_list();
138        let base = build_signature_base(req, &self.components, &inner_list)?;
139        let sig_bytes = self.key.sign(base.as_bytes())?;
140
141        let input_value = serialise_signature_input_dict(&[(self.label.clone(), input)]);
142        let sig_value = serialise_signature_dict(&[(self.label.clone(), sig_bytes)]);
143
144        insert_header(req, SIGNATURE_INPUT_HEADER, &input_value)?;
145        insert_header(req, SIGNATURE_HEADER, &sig_value)?;
146        Ok(())
147    }
148}
149
150fn algorithm_name(key: &SigningKey) -> String {
151    match key.algorithm() {
152        Algorithm::RsaSha256 => "rsa-v1_5-sha256".to_owned(),
153        Algorithm::Ed25519 => "ed25519".to_owned(),
154    }
155}
156
157fn insert_header<B>(req: &mut Request<B>, name: &'static str, value: &str) -> Result<(), Error> {
158    let value = HeaderValue::from_str(value).map_err(|e| Error::InvalidHeader {
159        name,
160        reason: e.to_string(),
161    })?;
162    req.headers_mut().insert(name, value);
163    Ok(())
164}
165
166#[cfg(test)]
167mod tests {
168    use http::{Method, Request};
169    use pretty_assertions::assert_eq;
170
171    use super::*;
172    use crate::digest::sha256_digest_header;
173    use crate::rfc9421::signature::parse_signature_dict;
174    use crate::rfc9421::signature_input::parse_signature_input_dict;
175
176    fn sample_request() -> Request<Vec<u8>> {
177        let body = b"{}";
178        Request::builder()
179            .method(Method::POST)
180            .uri("https://example.com/inbox")
181            .header("host", "example.com")
182            .header("date", "Sun, 05 Jan 2014 21:31:40 GMT")
183            .header("digest", sha256_digest_header(body))
184            .body(body.to_vec())
185            .expect("valid")
186    }
187
188    #[test]
189    fn sign_inserts_both_headers_with_matching_label() {
190        let key = SigningKey::generate_ed25519();
191        let mut req = sample_request();
192        Rfc9421Signer::new(&key, "https://example.com/actor#sig")
193            .with_label("sig1")
194            .with_created(1_700_000_000)
195            .sign(&mut req)
196            .expect("sign");
197
198        let input_raw = req
199            .headers()
200            .get(SIGNATURE_INPUT_HEADER)
201            .expect("Signature-Input present")
202            .to_str()
203            .expect("ASCII");
204        let sig_raw = req
205            .headers()
206            .get(SIGNATURE_HEADER)
207            .expect("Signature present")
208            .to_str()
209            .expect("ASCII");
210
211        let input = parse_signature_input_dict(input_raw).expect("parse input");
212        let sig = parse_signature_dict(sig_raw).expect("parse sig");
213        assert_eq!(input[0].0, "sig1");
214        assert_eq!(sig[0].0, "sig1");
215        assert_eq!(
216            input[0].1.keyid.as_deref(),
217            Some("https://example.com/actor#sig")
218        );
219        assert_eq!(input[0].1.algorithm.as_deref(), Some("ed25519"));
220        assert_eq!(input[0].1.created, Some(1_700_000_000));
221    }
222
223    #[test]
224    fn rsa_signer_uses_rfc9421_algorithm_name() {
225        let key = SigningKey::generate_rsa(crate::key::RsaBits::Rsa2048).expect("rng");
226        let mut req = sample_request();
227        Rfc9421Signer::new(&key, "kid")
228            .sign(&mut req)
229            .expect("sign");
230        let input_raw = req
231            .headers()
232            .get(SIGNATURE_INPUT_HEADER)
233            .unwrap()
234            .to_str()
235            .unwrap();
236        let input = parse_signature_input_dict(input_raw).unwrap();
237        assert_eq!(input[0].1.algorithm.as_deref(), Some("rsa-v1_5-sha256"));
238    }
239
240    #[test]
241    fn emit_alg_false_suppresses_alg_parameter() {
242        let key = SigningKey::generate_ed25519();
243        let mut req = sample_request();
244        Rfc9421Signer::new(&key, "kid")
245            .emit_alg(false)
246            .sign(&mut req)
247            .expect("sign");
248        let input_raw = req
249            .headers()
250            .get(SIGNATURE_INPUT_HEADER)
251            .unwrap()
252            .to_str()
253            .unwrap();
254        let input = parse_signature_input_dict(input_raw).unwrap();
255        assert_eq!(input[0].1.algorithm, None);
256    }
257}