1use crate::httpsig::components::{extract_authority, extract_method, extract_path};
4use crate::httpsig::digest::compute_content_digest;
5use crate::httpsig::error::HttpSigError;
6use crate::httpsig::signature_base::build_signature_base;
7use crate::keys::Ed25519Keypair;
8use std::time::{SystemTime, UNIX_EPOCH};
9
10#[derive(Debug, Clone)]
12pub struct SignatureOutput {
13 pub signature_input: String,
15 pub signature: String,
17 pub content_digest: String,
19}
20
21#[cfg_attr(feature = "rhai", derive(Clone))]
45pub struct HttpSigner {
46 keypair: Ed25519Keypair,
47 key_id: String,
48 extra_headers: Vec<String>,
49 signature_label: String,
50}
51
52impl HttpSigner {
53 pub fn new(keypair: Ed25519Keypair, key_id: impl Into<String>) -> Self {
55 Self {
56 keypair,
57 key_id: key_id.into(),
58 extra_headers: Vec::new(),
59 signature_label: "sig1".to_string(),
60 }
61 }
62
63 pub fn with_headers(mut self, headers: Vec<String>) -> Self {
75 self.extra_headers = headers;
76 self
77 }
78
79 pub fn with_label(mut self, label: impl Into<String>) -> Self {
81 self.signature_label = label.into();
82 self
83 }
84
85 pub fn sign_request<B>(
117 &self,
118 request: &mut http::Request<B>,
119 body: &[u8],
120 ) -> Result<(), HttpSigError> {
121 let method = request.method().as_str();
123 let uri = request.uri();
124 let path = uri.path();
125 let authority = uri
126 .authority()
127 .ok_or(HttpSigError::MissingAuthority)?
128 .as_str();
129
130 let headers: Vec<(String, String)> = request
132 .headers()
133 .iter()
134 .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
135 .collect();
136
137 let result = self.sign_components(method, path, authority, &headers, body)?;
139
140 request.headers_mut().insert(
142 "signature-input",
143 result
144 .signature_input
145 .parse()
146 .map_err(|_| HttpSigError::InvalidHeader("signature-input".to_string()))?,
147 );
148 request.headers_mut().insert(
149 "signature",
150 result
151 .signature
152 .parse()
153 .map_err(|_| HttpSigError::InvalidHeader("signature".to_string()))?,
154 );
155 request.headers_mut().insert(
156 "content-digest",
157 result
158 .content_digest
159 .parse()
160 .map_err(|_| HttpSigError::InvalidHeader("content-digest".to_string()))?,
161 );
162
163 Ok(())
164 }
165
166 pub fn sign_response<B>(
176 &self,
177 response: &mut http::Response<B>,
178 body: &[u8],
179 ) -> Result<(), HttpSigError> {
180 let method = "POST"; let path = "/";
183 let authority = "response.local";
184
185 let headers: Vec<(String, String)> = response
187 .headers()
188 .iter()
189 .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
190 .collect();
191
192 let result = self.sign_components(method, path, authority, &headers, body)?;
194
195 response.headers_mut().insert(
197 "signature-input",
198 result
199 .signature_input
200 .parse()
201 .map_err(|_| HttpSigError::InvalidHeader("signature-input".to_string()))?,
202 );
203 response.headers_mut().insert(
204 "signature",
205 result
206 .signature
207 .parse()
208 .map_err(|_| HttpSigError::InvalidHeader("signature".to_string()))?,
209 );
210 response.headers_mut().insert(
211 "content-digest",
212 result
213 .content_digest
214 .parse()
215 .map_err(|_| HttpSigError::InvalidHeader("content-digest".to_string()))?,
216 );
217
218 Ok(())
219 }
220
221 pub(crate) fn sign_components(
226 &self,
227 method: &str,
228 path: &str,
229 authority: &str,
230 headers: &[(String, String)],
231 body: &[u8],
232 ) -> Result<SignatureOutput, HttpSigError> {
233 let method = extract_method(method);
235 let path = extract_path(path)?;
236 let authority = extract_authority(authority);
237
238 let content_digest = compute_content_digest(body);
240
241 let created = SystemTime::now()
243 .duration_since(UNIX_EPOCH)
244 .map_err(|_| HttpSigError::ParseError("System time error".to_string()))?
245 .as_secs();
246
247 let extra_headers: Vec<(String, String)> = self
249 .extra_headers
250 .iter()
251 .filter_map(|name| {
252 headers
253 .iter()
254 .find(|(h, _)| h.eq_ignore_ascii_case(name))
255 .map(|(_, v)| (name.clone(), v.clone()))
256 })
257 .collect();
258
259 let sig_base = build_signature_base(
261 &method,
262 &path,
263 &authority,
264 &content_digest,
265 &extra_headers,
266 &self.key_id,
267 created,
268 )?;
269
270 let canonical = sig_base.to_canonical_string();
272 let signature = self.keypair.sign(canonical.as_bytes());
273
274 let sig_b64 = base64::Engine::encode(
276 &base64::engine::general_purpose::STANDARD,
277 signature.to_bytes(),
278 );
279
280 let component_names = sig_base.component_names();
282 let components_str = component_names
283 .iter()
284 .map(|name| format!("\"{}\"", name))
285 .collect::<Vec<_>>()
286 .join(" ");
287
288 let signature_input = format!(
290 "{}=({});keyid=\"{}\";alg=\"ed25519\";created={}",
291 self.signature_label, components_str, self.key_id, created
292 );
293
294 let signature_header = format!("{}=:{}:", self.signature_label, sig_b64);
296
297 Ok(SignatureOutput {
298 signature_input,
299 signature: signature_header,
300 content_digest,
301 })
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308 use http::Request;
309
310 #[test]
311 fn test_sign_request() {
312 let keypair = Ed25519Keypair::generate().unwrap();
313 let signer = HttpSigner::new(keypair, "test-key");
314
315 let body = b"test body";
316 let mut request = Request::post("https://example.com/api/test")
317 .body(body.to_vec())
318 .unwrap();
319
320 signer.sign_request(&mut request, body).unwrap();
321
322 let sig_input = request
323 .headers()
324 .get("signature-input")
325 .unwrap()
326 .to_str()
327 .unwrap();
328 let signature = request
329 .headers()
330 .get("signature")
331 .unwrap()
332 .to_str()
333 .unwrap();
334 let digest = request
335 .headers()
336 .get("content-digest")
337 .unwrap()
338 .to_str()
339 .unwrap();
340
341 assert!(sig_input.contains("sig1="));
342 assert!(sig_input.contains("keyid=\"test-key\""));
343 assert!(sig_input.contains("alg=\"ed25519\""));
344 assert!(sig_input.contains("created="));
345
346 assert!(signature.starts_with("sig1=:"));
347 assert!(signature.ends_with(":"));
348
349 assert!(digest.starts_with("sha-256=:"));
350 }
351
352 #[test]
353 fn test_sign_with_extra_headers() {
354 let keypair = Ed25519Keypair::generate().unwrap();
355 let signer =
356 HttpSigner::new(keypair, "test-key").with_headers(vec!["content-type".to_string()]);
357
358 let body = b"{}";
359 let mut request = Request::post("https://example.com/api/test")
360 .header("content-type", "application/json")
361 .body(body.to_vec())
362 .unwrap();
363
364 signer.sign_request(&mut request, body).unwrap();
365
366 let sig_input = request
367 .headers()
368 .get("signature-input")
369 .unwrap()
370 .to_str()
371 .unwrap();
372 assert!(sig_input.contains("content-type"));
373 }
374
375 #[test]
376 fn test_custom_label() {
377 let keypair = Ed25519Keypair::generate().unwrap();
378 let signer = HttpSigner::new(keypair, "test-key").with_label("custom");
379
380 let body = b"";
381 let mut request = Request::get("https://example.com/")
382 .body(body.to_vec())
383 .unwrap();
384
385 signer.sign_request(&mut request, body).unwrap();
386
387 let sig_input = request
388 .headers()
389 .get("signature-input")
390 .unwrap()
391 .to_str()
392 .unwrap();
393 let signature = request
394 .headers()
395 .get("signature")
396 .unwrap()
397 .to_str()
398 .unwrap();
399
400 assert!(sig_input.starts_with("custom="));
401 assert!(signature.starts_with("custom=:"));
402 }
403
404 #[test]
405 fn test_empty_body() {
406 let keypair = Ed25519Keypair::generate().unwrap();
407 let signer = HttpSigner::new(keypair, "test-key");
408
409 let body = b"";
410 let mut request = Request::get("https://example.com/api/test")
411 .body(body.to_vec())
412 .unwrap();
413
414 signer.sign_request(&mut request, body).unwrap();
415
416 let digest = request
417 .headers()
418 .get("content-digest")
419 .unwrap()
420 .to_str()
421 .unwrap();
422 assert_eq!(
423 digest,
424 "sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:"
425 );
426 }
427}