actpub_httpsig/cavage/
sign.rs1use base64ct::{Base64, Encoding};
4use http::Request;
5use http::header::HeaderValue;
6
7use crate::cavage::canonical::{CavageHeaderSet, Timestamps, build_signature_base};
8use crate::cavage::header::{CavageHeaderParams, SIGNATURE_HEADER};
9use crate::error::Error;
10use crate::key::{Algorithm, SigningKey};
11
12pub const DEFAULT_HEADER_SET: &[&str] =
29 &["(request-target)", "host", "date", "digest", "content-type"];
30
31#[derive(Debug)]
37pub struct CavageSigner<'a> {
38 key: &'a SigningKey,
39 key_id: &'a str,
40 headers: CavageHeaderSet,
41 created: Option<i64>,
42 expires: Option<i64>,
43 emit_algorithm: bool,
44}
45
46impl<'a> CavageSigner<'a> {
47 #[must_use]
51 pub fn new(key: &'a SigningKey, key_id: &'a str) -> Self {
52 Self {
53 key,
54 key_id,
55 headers: CavageHeaderSet::new(DEFAULT_HEADER_SET.iter().copied()),
56 created: None,
57 expires: None,
58 emit_algorithm: true,
59 }
60 }
61
62 #[must_use]
64 pub fn with_headers<I, S>(mut self, headers: I) -> Self
65 where
66 I: IntoIterator<Item = S>,
67 S: Into<String>,
68 {
69 self.headers = CavageHeaderSet::new(headers);
70 self
71 }
72
73 #[must_use]
75 pub fn with_header_set(mut self, headers: CavageHeaderSet) -> Self {
76 self.headers = headers;
77 self
78 }
79
80 #[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]
101 pub const fn emit_algorithm(mut self, emit: bool) -> Self {
102 self.emit_algorithm = emit;
103 self
104 }
105
106 pub fn sign<B>(&self, req: &mut Request<B>) -> Result<(), Error> {
115 let base = build_signature_base(
116 req,
117 &self.headers,
118 Timestamps {
119 created: self.created,
120 expires: self.expires,
121 },
122 )?;
123 let sig_bytes = self.key.sign(base.as_bytes())?;
124 let sig_b64 = Base64::encode_string(&sig_bytes);
125
126 let params = CavageHeaderParams {
127 key_id: self.key_id.to_owned(),
128 algorithm: self
129 .emit_algorithm
130 .then(|| algorithm_name(self.key).to_owned()),
131 headers: self.headers.clone(),
132 signature: sig_b64,
133 created: self.created,
134 expires: self.expires,
135 };
136
137 let value =
138 HeaderValue::from_str(¶ms.to_header_value()).map_err(|e| Error::InvalidHeader {
139 name: "signature",
140 reason: e.to_string(),
141 })?;
142 req.headers_mut().insert(SIGNATURE_HEADER, value);
143 Ok(())
144 }
145}
146
147const fn algorithm_name(key: &SigningKey) -> &'static str {
148 match key.algorithm() {
149 Algorithm::RsaSha256 => "rsa-sha256",
150 Algorithm::Ed25519 => "ed25519",
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use http::{Method, Request};
157 use pretty_assertions::assert_eq;
158
159 use super::*;
160 use crate::cavage::header::CavageHeaderParams;
161 use crate::digest::sha256_digest_header;
162 use crate::key::RsaBits;
163
164 fn sample_post(body: &[u8]) -> Request<Vec<u8>> {
165 Request::builder()
166 .method(Method::POST)
167 .uri("https://example.com/inbox")
168 .header("host", "example.com")
169 .header("date", "Sun, 05 Jan 2014 21:31:40 GMT")
170 .header("digest", sha256_digest_header(body))
171 .header("content-type", "application/activity+json")
172 .body(body.to_vec())
173 .expect("valid request")
174 }
175
176 #[test]
177 fn ed25519_sign_inserts_signature_header_with_correct_shape() {
178 let key = SigningKey::generate_ed25519();
179 let mut req = sample_post(b"{}");
180 let signer = CavageSigner::new(&key, "https://example.com/actors/alice#main-key");
181 signer.sign(&mut req).expect("sign must succeed");
182
183 let raw = req
184 .headers()
185 .get(SIGNATURE_HEADER)
186 .expect("Signature header was inserted")
187 .to_str()
188 .expect("ASCII");
189
190 let params = CavageHeaderParams::parse(raw).expect("parseable");
191 assert_eq!(params.key_id, "https://example.com/actors/alice#main-key");
192 assert_eq!(params.algorithm.as_deref(), Some("ed25519"));
193 assert_eq!(params.headers.len(), DEFAULT_HEADER_SET.len());
194 assert!(!params.signature.is_empty());
195 }
196
197 #[test]
198 fn rsa_sha256_sign_emits_rsa_sha256_algorithm_name() {
199 let key = SigningKey::generate_rsa(RsaBits::Rsa2048).expect("rng");
200 let mut req = sample_post(b"{}");
201 let signer = CavageSigner::new(&key, "kid");
202 signer.sign(&mut req).expect("sign");
203 let params = CavageHeaderParams::parse(
204 req.headers()
205 .get(SIGNATURE_HEADER)
206 .unwrap()
207 .to_str()
208 .unwrap(),
209 )
210 .unwrap();
211 assert_eq!(params.algorithm.as_deref(), Some("rsa-sha256"));
212 }
213
214 #[test]
215 fn emit_algorithm_false_suppresses_algorithm_parameter() {
216 let key = SigningKey::generate_ed25519();
217 let mut req = sample_post(b"{}");
218 let signer = CavageSigner::new(&key, "kid").emit_algorithm(false);
219 signer.sign(&mut req).expect("sign");
220 let params = CavageHeaderParams::parse(
221 req.headers()
222 .get(SIGNATURE_HEADER)
223 .unwrap()
224 .to_str()
225 .unwrap(),
226 )
227 .unwrap();
228 assert_eq!(params.algorithm, None);
229 }
230
231 #[test]
232 fn missing_required_header_returns_required_header_absent() {
233 let key = SigningKey::generate_ed25519();
234 let mut req = Request::builder()
235 .method(Method::POST)
236 .uri("https://example.com/inbox")
237 .body(Vec::<u8>::new())
238 .unwrap();
239 let signer = CavageSigner::new(&key, "kid");
240 let err = signer.sign(&mut req).expect_err("missing host/date/digest");
241 assert!(matches!(err, Error::RequiredHeaderAbsent(_)));
242 }
243}