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] = &["@method", "@target-uri", "host", "date", "digest"];
19
20#[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 #[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 #[must_use]
69 pub fn with_components(mut self, components: Vec<Component>) -> Self {
70 self.components = components;
71 self
72 }
73
74 #[must_use]
76 pub fn with_label(mut self, label: impl Into<String>) -> Self {
77 self.label = label.into();
78 self
79 }
80
81 #[must_use]
83 pub const fn with_created(mut self, seconds: i64) -> Self {
84 self.created = Some(seconds);
85 self
86 }
87
88 #[must_use]
90 pub const fn with_expires(mut self, seconds: i64) -> Self {
91 self.expires = Some(seconds);
92 self
93 }
94
95 #[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 #[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 #[must_use]
113 pub const fn emit_alg(mut self, emit: bool) -> Self {
114 self.emit_alg = emit;
115 self
116 }
117
118 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}