actpub_httpsig/rfc9421/
sign.rs1use 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
14pub const DEFAULT_COMPONENTS: &[&str] =
24 &["@method", "@target-uri", "host", "date", "content-digest"];
25
26#[derive(Debug)]
29pub struct Rfc9421Signer<'a> {
30 key: &'a SigningKey,
31 key_id: &'a str,
32 label: String,
33 components: Vec<Component>,
34 created: Option<i64>,
35 expires: Option<i64>,
36 emit_alg: bool,
37 nonce: Option<String>,
38 tag: Option<String>,
39}
40
41impl<'a> Rfc9421Signer<'a> {
42 #[must_use]
51 pub fn new(key: &'a SigningKey, key_id: &'a str) -> Self {
52 #[allow(
53 clippy::expect_used,
54 reason = "the DEFAULT_COMPONENTS constant contains only valid identifiers"
55 )]
56 let components = DEFAULT_COMPONENTS
57 .iter()
58 .map(|ident| Component::parse(ident).expect("valid default component"))
59 .collect();
60 Self {
61 key,
62 key_id,
63 label: "sig1".into(),
64 components,
65 created: None,
66 expires: None,
67 emit_alg: true,
68 nonce: None,
69 tag: None,
70 }
71 }
72
73 #[must_use]
75 pub fn with_components(mut self, components: Vec<Component>) -> Self {
76 self.components = components;
77 self
78 }
79
80 #[must_use]
82 pub fn with_label(mut self, label: impl Into<String>) -> Self {
83 self.label = label.into();
84 self
85 }
86
87 #[must_use]
89 pub const fn with_created(mut self, seconds: i64) -> Self {
90 self.created = Some(seconds);
91 self
92 }
93
94 #[must_use]
96 pub const fn with_expires(mut self, seconds: i64) -> Self {
97 self.expires = Some(seconds);
98 self
99 }
100
101 #[must_use]
103 pub fn with_nonce(mut self, nonce: impl Into<String>) -> Self {
104 self.nonce = Some(nonce.into());
105 self
106 }
107
108 #[must_use]
110 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
111 self.tag = Some(tag.into());
112 self
113 }
114
115 #[must_use]
119 pub const fn emit_alg(mut self, emit: bool) -> Self {
120 self.emit_alg = emit;
121 self
122 }
123
124 pub fn sign<B>(&self, req: &mut Request<B>) -> Result<(), Error> {
134 let input = SignatureInput {
135 components: self.components.clone(),
136 keyid: Some(self.key_id.to_owned()),
137 algorithm: self.emit_alg.then(|| algorithm_name(self.key).to_owned()),
138 created: self.created,
139 expires: self.expires,
140 nonce: self.nonce.clone(),
141 tag: self.tag.clone(),
142 };
143 let inner_list = input.serialise_inner_list();
144 let base = build_signature_base(req, &self.components, &inner_list)?;
145 let sig_bytes = self.key.sign(base.as_bytes())?;
146
147 let input_value = serialise_signature_input_dict(&[(self.label.clone(), input)]);
148 let sig_value = serialise_signature_dict(&[(self.label.clone(), sig_bytes)]);
149
150 insert_header(req, SIGNATURE_INPUT_HEADER, &input_value)?;
151 insert_header(req, SIGNATURE_HEADER, &sig_value)?;
152 Ok(())
153 }
154}
155
156const fn algorithm_name(key: &SigningKey) -> &'static str {
157 match key.algorithm() {
158 Algorithm::RsaSha256 => "rsa-v1_5-sha256",
159 Algorithm::Ed25519 => "ed25519",
160 }
161}
162
163fn insert_header<B>(req: &mut Request<B>, name: &'static str, value: &str) -> Result<(), Error> {
164 let value = HeaderValue::from_str(value).map_err(|e| Error::InvalidHeader {
165 name,
166 reason: e.to_string(),
167 })?;
168 req.headers_mut().insert(name, value);
169 Ok(())
170}
171
172#[cfg(test)]
173mod tests {
174 use http::{Method, Request};
175 use pretty_assertions::assert_eq;
176
177 use super::*;
178 use crate::content_digest::content_digest_header;
179 use crate::rfc9421::signature::parse_signature_dict;
180 use crate::rfc9421::signature_input::parse_signature_input_dict;
181
182 fn sample_request() -> Request<Vec<u8>> {
183 let body = b"{}";
184 Request::builder()
185 .method(Method::POST)
186 .uri("https://example.com/inbox")
187 .header("host", "example.com")
188 .header("date", "Sun, 05 Jan 2014 21:31:40 GMT")
189 .header("content-digest", content_digest_header(body))
190 .body(body.to_vec())
191 .expect("valid")
192 }
193
194 #[test]
195 fn sign_inserts_both_headers_with_matching_label() {
196 let key = SigningKey::generate_ed25519();
197 let mut req = sample_request();
198 Rfc9421Signer::new(&key, "https://example.com/actor#sig")
199 .with_label("sig1")
200 .with_created(1_700_000_000)
201 .sign(&mut req)
202 .expect("sign");
203
204 let input_raw = req
205 .headers()
206 .get(SIGNATURE_INPUT_HEADER)
207 .expect("Signature-Input present")
208 .to_str()
209 .expect("ASCII");
210 let sig_raw = req
211 .headers()
212 .get(SIGNATURE_HEADER)
213 .expect("Signature present")
214 .to_str()
215 .expect("ASCII");
216
217 let input = parse_signature_input_dict(input_raw).expect("parse input");
218 let sig = parse_signature_dict(sig_raw).expect("parse sig");
219 assert_eq!(input[0].0, "sig1");
220 assert_eq!(sig[0].0, "sig1");
221 assert_eq!(
222 input[0].1.keyid.as_deref(),
223 Some("https://example.com/actor#sig")
224 );
225 assert_eq!(input[0].1.algorithm.as_deref(), Some("ed25519"));
226 assert_eq!(input[0].1.created, Some(1_700_000_000));
227 }
228
229 #[test]
230 fn rsa_signer_uses_rfc9421_algorithm_name() {
231 let key = SigningKey::generate_rsa(crate::key::RsaBits::Rsa2048).expect("rng");
232 let mut req = sample_request();
233 Rfc9421Signer::new(&key, "kid")
234 .sign(&mut req)
235 .expect("sign");
236 let input_raw = req
237 .headers()
238 .get(SIGNATURE_INPUT_HEADER)
239 .unwrap()
240 .to_str()
241 .unwrap();
242 let input = parse_signature_input_dict(input_raw).unwrap();
243 assert_eq!(input[0].1.algorithm.as_deref(), Some("rsa-v1_5-sha256"));
244 }
245
246 #[test]
247 fn emit_alg_false_suppresses_alg_parameter() {
248 let key = SigningKey::generate_ed25519();
249 let mut req = sample_request();
250 Rfc9421Signer::new(&key, "kid")
251 .emit_alg(false)
252 .sign(&mut req)
253 .expect("sign");
254 let input_raw = req
255 .headers()
256 .get(SIGNATURE_INPUT_HEADER)
257 .unwrap()
258 .to_str()
259 .unwrap();
260 let input = parse_signature_input_dict(input_raw).unwrap();
261 assert_eq!(input[0].1.algorithm, None);
262 }
263}