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.emit_algorithm.then(|| algorithm_name(self.key)),
129 headers: self.headers.clone(),
130 signature: sig_b64,
131 created: self.created,
132 expires: self.expires,
133 };
134
135 let value =
136 HeaderValue::from_str(¶ms.to_header_value()).map_err(|e| Error::InvalidHeader {
137 name: "signature",
138 reason: e.to_string(),
139 })?;
140 req.headers_mut().insert(SIGNATURE_HEADER, value);
141 Ok(())
142 }
143}
144
145fn algorithm_name(key: &SigningKey) -> String {
146 match key.algorithm() {
147 Algorithm::RsaSha256 => "rsa-sha256".to_owned(),
148 Algorithm::Ed25519 => "ed25519".to_owned(),
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use http::{Method, Request};
155 use pretty_assertions::assert_eq;
156
157 use super::*;
158 use crate::cavage::header::CavageHeaderParams;
159 use crate::digest::sha256_digest_header;
160 use crate::key::RsaBits;
161
162 fn sample_post(body: &[u8]) -> Request<Vec<u8>> {
163 Request::builder()
164 .method(Method::POST)
165 .uri("https://example.com/inbox")
166 .header("host", "example.com")
167 .header("date", "Sun, 05 Jan 2014 21:31:40 GMT")
168 .header("digest", sha256_digest_header(body))
169 .header("content-type", "application/activity+json")
170 .body(body.to_vec())
171 .expect("valid request")
172 }
173
174 #[test]
175 fn ed25519_sign_inserts_signature_header_with_correct_shape() {
176 let key = SigningKey::generate_ed25519();
177 let mut req = sample_post(b"{}");
178 let signer = CavageSigner::new(&key, "https://example.com/actors/alice#main-key");
179 signer.sign(&mut req).expect("sign must succeed");
180
181 let raw = req
182 .headers()
183 .get(SIGNATURE_HEADER)
184 .expect("Signature header was inserted")
185 .to_str()
186 .expect("ASCII");
187
188 let params = CavageHeaderParams::parse(raw).expect("parseable");
189 assert_eq!(params.key_id, "https://example.com/actors/alice#main-key");
190 assert_eq!(params.algorithm.as_deref(), Some("ed25519"));
191 assert_eq!(params.headers.len(), DEFAULT_HEADER_SET.len());
192 assert!(!params.signature.is_empty());
193 }
194
195 #[test]
196 fn rsa_sha256_sign_emits_rsa_sha256_algorithm_name() {
197 let key = SigningKey::generate_rsa(RsaBits::Rsa2048).expect("rng");
198 let mut req = sample_post(b"{}");
199 let signer = CavageSigner::new(&key, "kid");
200 signer.sign(&mut req).expect("sign");
201 let params = CavageHeaderParams::parse(
202 req.headers()
203 .get(SIGNATURE_HEADER)
204 .unwrap()
205 .to_str()
206 .unwrap(),
207 )
208 .unwrap();
209 assert_eq!(params.algorithm.as_deref(), Some("rsa-sha256"));
210 }
211
212 #[test]
213 fn emit_algorithm_false_suppresses_algorithm_parameter() {
214 let key = SigningKey::generate_ed25519();
215 let mut req = sample_post(b"{}");
216 let signer = CavageSigner::new(&key, "kid").emit_algorithm(false);
217 signer.sign(&mut req).expect("sign");
218 let params = CavageHeaderParams::parse(
219 req.headers()
220 .get(SIGNATURE_HEADER)
221 .unwrap()
222 .to_str()
223 .unwrap(),
224 )
225 .unwrap();
226 assert_eq!(params.algorithm, None);
227 }
228
229 #[test]
230 fn missing_required_header_returns_required_header_absent() {
231 let key = SigningKey::generate_ed25519();
232 let mut req = Request::builder()
233 .method(Method::POST)
234 .uri("https://example.com/inbox")
235 .body(Vec::<u8>::new())
236 .unwrap();
237 let signer = CavageSigner::new(&key, "kid");
238 let err = signer.sign(&mut req).expect_err("missing host/date/digest");
239 assert!(matches!(err, Error::RequiredHeaderAbsent(_)));
240 }
241}