Skip to main content

herolib_crypt/httpsig/
verifier.rs

1//! HTTP request verification implementation.
2
3use crate::httpsig::components::{extract_authority, extract_method, extract_path};
4use crate::httpsig::digest::verify_content_digest;
5use crate::httpsig::error::HttpSigError;
6use crate::httpsig::parser::{parse_signature, parse_signature_input};
7use crate::httpsig::signature_base::build_signature_base;
8use crate::keys::{Ed25519PublicKey, Signature};
9use std::time::{SystemTime, UNIX_EPOCH};
10
11/// Result of successful signature verification.
12#[derive(Debug, Clone)]
13pub struct VerificationResult {
14    /// The key ID that was used for verification
15    pub key_id: String,
16    /// The signature algorithm
17    pub algorithm: String,
18    /// The signature creation timestamp
19    pub created: u64,
20    /// The components that were signed
21    pub signed_components: Vec<String>,
22}
23
24/// Function type for dynamic key lookup (internal use only).
25#[cfg(not(feature = "rhai"))]
26pub(crate) type KeyGetter =
27    Box<dyn Fn(&str) -> Result<Ed25519PublicKey, HttpSigError> + Send + Sync>;
28
29/// HTTP request signature verifier.
30///
31/// # Example
32///
33/// ```
34/// use herolib_crypt::httpsig::HttpVerifier;
35/// use herolib_crypt::keys::Ed25519Keypair;
36///
37/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
38/// let keypair = Ed25519Keypair::generate()?;
39/// let public_key = keypair.public_key();
40///
41/// let verifier = HttpVerifier::new()
42///     .with_key(public_key);
43///
44/// // Verify would be called with actual request data
45/// # Ok(())
46/// # }
47/// ```
48#[cfg_attr(feature = "rhai", derive(Clone))]
49pub struct HttpVerifier {
50    #[cfg(not(feature = "rhai"))]
51    key_getter: Option<KeyGetter>,
52    default_key: Option<Ed25519PublicKey>,
53    tolerance_secs: u64,
54    required_components: Vec<String>,
55}
56
57impl HttpVerifier {
58    /// Create a new verifier.
59    ///
60    /// You must configure either a default key with `with_key()` or
61    /// a key getter function with `with_key_getter()`.
62    pub fn new() -> Self {
63        Self {
64            #[cfg(not(feature = "rhai"))]
65            key_getter: None,
66            default_key: None,
67            tolerance_secs: 60, // 1 minute default (tight security, handles clock skew)
68            required_components: vec![
69                "@method".to_string(),
70                "@path".to_string(),
71                "@authority".to_string(),
72                "content-digest".to_string(),
73            ],
74        }
75    }
76
77    /// Set a default public key for single-key scenarios.
78    pub fn with_key(mut self, public_key: Ed25519PublicKey) -> Self {
79        self.default_key = Some(public_key);
80        self
81    }
82
83    /// Set a dynamic key lookup function.
84    ///
85    /// The function receives the key ID from the signature and should
86    /// return the corresponding public key.
87    ///
88    /// Note: This method is not available when the `rhai` feature is enabled,
89    /// as function pointers are not cloneable. Use `with_key()` instead.
90    ///
91    /// # Example
92    ///
93    /// ```
94    /// # use herolib_crypt::httpsig::HttpVerifier;
95    /// # use herolib_crypt::keys::Ed25519PublicKey;
96    /// let verifier = HttpVerifier::new()
97    ///     .with_key_getter(Box::new(|key_id| {
98    ///         // Look up key from database, cache, etc.
99    ///         // For this example, just return an error
100    ///         Err(herolib_crypt::httpsig::HttpSigError::KeyNotFound(key_id.to_string()))
101    ///     }));
102    /// ```
103    #[cfg(not(feature = "rhai"))]
104    pub fn with_key_getter(mut self, getter: KeyGetter) -> Self {
105        self.key_getter = Some(getter);
106        self
107    }
108
109    /// Set the timestamp tolerance in seconds (default: 60).
110    ///
111    /// Signatures with timestamps outside the window of
112    /// `now - tolerance` to `now + tolerance` will be rejected.
113    pub fn with_tolerance(mut self, seconds: u64) -> Self {
114        self.tolerance_secs = seconds;
115        self
116    }
117
118    /// Require additional signed components beyond the defaults.
119    pub fn with_required_components(mut self, components: Vec<String>) -> Self {
120        self.required_components.extend(components);
121        self
122    }
123
124    /// Verify an HTTP request.
125    ///
126    /// This method works with any HTTP library that uses `http::Request`.
127    ///
128    /// # Arguments
129    ///
130    /// * `request` - Reference to the HTTP request
131    /// * `body` - Request body bytes
132    pub fn verify_request<B>(
133        &self,
134        request: &http::Request<B>,
135        body: &[u8],
136    ) -> Result<VerificationResult, HttpSigError> {
137        // Extract components from request
138        let method = request.method().as_str();
139        let uri = request.uri();
140        let path = uri.path();
141        let authority = uri
142            .authority()
143            .ok_or(HttpSigError::MissingAuthority)?
144            .as_str();
145
146        // Extract headers
147        let headers: Vec<(String, String)> = request
148            .headers()
149            .iter()
150            .map(|(k, v)| {
151                (
152                    k.as_str().to_string(),
153                    v.to_str().unwrap_or("").to_string(),
154                )
155            })
156            .collect();
157
158        // Verify
159        self.verify_components(method, path, authority, &headers, body)
160    }
161
162    /// Verify an HTTP response.
163    ///
164    /// This method works with any HTTP library that uses `http::Response`.
165    ///
166    /// # Arguments
167    ///
168    /// * `response` - Reference to the HTTP response
169    /// * `body` - Response body bytes
170    pub fn verify_response<B>(
171        &self,
172        response: &http::Response<B>,
173        body: &[u8],
174    ) -> Result<VerificationResult, HttpSigError> {
175        // For responses, we use fixed method and path
176        let method = "POST";
177        let path = "/";
178        let authority = "response.local";
179
180        // Extract headers
181        let headers: Vec<(String, String)> = response
182            .headers()
183            .iter()
184            .map(|(k, v)| {
185                (
186                    k.as_str().to_string(),
187                    v.to_str().unwrap_or("").to_string(),
188                )
189            })
190            .collect();
191
192        // Verify
193        self.verify_components(method, path, authority, &headers, body)
194    }
195
196    /// Verify HTTP message components (internal helper).
197    ///
198    /// This is the low-level verification method used internally.
199    /// For most use cases, use `verify_request` or `verify_response` instead.
200    pub(crate) fn verify_components(
201        &self,
202        method: &str,
203        path: &str,
204        authority: &str,
205        headers: &[(String, String)],
206        body: &[u8],
207    ) -> Result<VerificationResult, HttpSigError> {
208        // Extract signature headers
209        let signature_input = self.find_header(headers, "signature-input")?;
210        let signature_header = self.find_header(headers, "signature")?;
211        let content_digest = self.find_header(headers, "content-digest")?;
212
213        // Parse Signature-Input
214        let params = parse_signature_input(signature_input)?;
215
216        // Verify algorithm
217        if params.alg != "ed25519" {
218            return Err(HttpSigError::UnsupportedAlgorithm(params.alg.clone()));
219        }
220
221        // Verify required components are present
222        for required in &self.required_components {
223            if !params.components.contains(required) {
224                return Err(HttpSigError::MissingComponent(required.clone()));
225            }
226        }
227
228        // Verify timestamp tolerance
229        self.verify_timestamp(params.created)?;
230
231        // Verify content digest
232        verify_content_digest(body, content_digest)?;
233
234        // Get public key
235        let public_key = self.get_public_key(&params.keyid)?;
236
237        // Normalize components
238        let method = extract_method(method);
239        let path = extract_path(path)?;
240        let authority = extract_authority(authority);
241
242        // Filter extra headers that were signed
243        let extra_headers: Vec<(String, String)> = params
244            .components
245            .iter()
246            .filter(|c| !c.starts_with('@') && *c != "content-digest")
247            .filter_map(|name| {
248                headers
249                    .iter()
250                    .find(|(h, _)| h.eq_ignore_ascii_case(name))
251                    .map(|(_, v)| (name.clone(), v.clone()))
252            })
253            .collect();
254
255        // Rebuild signature base
256        let sig_base = build_signature_base(
257            &method,
258            &path,
259            &authority,
260            content_digest,
261            &extra_headers,
262            &params.keyid,
263            params.created,
264        )?;
265
266        // Get canonical string
267        let canonical = sig_base.to_canonical_string();
268
269        // Parse signature bytes
270        let sig_bytes = parse_signature(signature_header, &params.label)?;
271        let signature = Signature::from_bytes(&sig_bytes)?;
272
273        // Verify signature
274        let valid = public_key.verify(canonical.as_bytes(), &signature)?;
275
276        if !valid {
277            return Err(HttpSigError::VerificationFailed);
278        }
279
280        Ok(VerificationResult {
281            key_id: params.keyid,
282            algorithm: params.alg,
283            created: params.created,
284            signed_components: params.components,
285        })
286    }
287
288    fn find_header<'a>(
289        &self,
290        headers: &'a [(String, String)],
291        name: &str,
292    ) -> Result<&'a str, HttpSigError> {
293        headers
294            .iter()
295            .find(|(k, _)| k.eq_ignore_ascii_case(name))
296            .map(|(_, v)| v.as_str())
297            .ok_or_else(|| HttpSigError::MissingHeader(name.to_string()))
298    }
299
300    fn verify_timestamp(&self, created: u64) -> Result<(), HttpSigError> {
301        let now = SystemTime::now()
302            .duration_since(UNIX_EPOCH)
303            .map_err(|_| HttpSigError::ParseError("System time error".to_string()))?
304            .as_secs();
305
306        let diff = if now > created {
307            now - created
308        } else {
309            created - now
310        };
311
312        if diff > self.tolerance_secs {
313            return Err(HttpSigError::TimestampOutOfBounds {
314                created,
315                now,
316                tolerance: self.tolerance_secs,
317            });
318        }
319
320        Ok(())
321    }
322
323    fn get_public_key(&self, _key_id: &str) -> Result<Ed25519PublicKey, HttpSigError> {
324        #[cfg(not(feature = "rhai"))]
325        if let Some(getter) = &self.key_getter {
326            return getter(_key_id);
327        }
328
329        if let Some(key) = &self.default_key {
330            return Ok(key.clone());
331        }
332
333        Err(HttpSigError::NoKeyConfigured)
334    }
335}
336
337impl Default for HttpVerifier {
338    fn default() -> Self {
339        Self::new()
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346    use crate::httpsig::signer::HttpSigner;
347    use crate::keys::Ed25519Keypair;
348    use http::Request;
349
350    #[test]
351    fn test_sign_and_verify_roundtrip() {
352        let keypair = Ed25519Keypair::generate().unwrap();
353        let public_key = keypair.public_key();
354
355        let signer = HttpSigner::new(keypair, "test-key");
356        let verifier = HttpVerifier::new().with_key(public_key).with_tolerance(60);
357
358        let body = b"test body";
359        let mut request = Request::post("https://example.com/api/test")
360            .body(body.to_vec())
361            .unwrap();
362
363        signer.sign_request(&mut request, body).unwrap();
364
365        let result = verifier.verify_request(&request, body).unwrap();
366
367        assert_eq!(result.key_id, "test-key");
368        assert_eq!(result.algorithm, "ed25519");
369    }
370
371    #[test]
372    fn test_verify_wrong_body() {
373        let keypair = Ed25519Keypair::generate().unwrap();
374        let public_key = keypair.public_key();
375
376        let signer = HttpSigner::new(keypair, "test-key");
377        let verifier = HttpVerifier::new().with_key(public_key);
378
379        let original_body = b"original";
380        let mut request = Request::post("https://example.com/api/test")
381            .body(original_body.to_vec())
382            .unwrap();
383
384        signer.sign_request(&mut request, original_body).unwrap();
385
386        // Try to verify with tampered body
387        let result = verifier.verify_request(&request, b"tampered");
388
389        assert!(matches!(result, Err(HttpSigError::DigestMismatch)));
390    }
391
392    #[test]
393    fn test_verify_wrong_signature() {
394        let keypair1 = Ed25519Keypair::generate().unwrap();
395        let keypair2 = Ed25519Keypair::generate().unwrap();
396        let wrong_key = keypair2.public_key();
397
398        let signer = HttpSigner::new(keypair1, "test-key");
399        let verifier = HttpVerifier::new().with_key(wrong_key);
400
401        let body = b"test";
402        let mut request = Request::post("https://example.com/api/test")
403            .body(body.to_vec())
404            .unwrap();
405
406        signer.sign_request(&mut request, body).unwrap();
407
408        // Verify with wrong key
409        let result = verifier.verify_request(&request, body);
410
411        assert!(matches!(result, Err(HttpSigError::VerificationFailed)));
412    }
413}