nonce_auth/nonce/
signature.rs

1//! Pluggable signature algorithm system for nonce-auth.
2//!
3//! This module provides a trait-based system for supporting different
4//! cryptographic signature algorithms. The library ships with HMAC-SHA256
5//! as the default algorithm, but users can implement custom algorithms
6//! or use alternative implementations.
7
8use crate::NonceError;
9
10/// A trait for signature algorithms used in nonce authentication.
11///
12/// This trait abstracts the cryptographic operations needed for generating
13/// and verifying signatures in the nonce authentication system.
14///
15/// # Implementation Notes
16///
17/// - Implementations should be secure against timing attacks
18/// - The `sign` method should include timestamp and nonce in the signature
19/// - The `verify` method should perform constant-time comparison
20/// - All implementations should be `Send + Sync` for async usage
21///
22/// # Example
23///
24/// ```rust
25/// # use nonce_auth::signature::SignatureAlgorithm;
26/// # use nonce_auth::NonceError;
27/// # use std::sync::Arc;
28/// #
29/// struct MyCustomAlgorithm {
30///     key: Vec<u8>,
31/// }
32///
33/// impl SignatureAlgorithm for MyCustomAlgorithm {
34///     fn name(&self) -> &'static str {
35///         "custom-hmac"
36///     }
37///
38///     fn sign(&self, timestamp: u64, nonce: &str, data: &[u8]) -> Result<String, NonceError> {
39///         // Custom signature implementation
40///         Ok("custom_signature".to_string())
41///     }
42///
43///     fn verify(&self, timestamp: u64, nonce: &str, data: &[u8], signature: &str) -> Result<(), NonceError> {
44///         // Custom verification implementation
45///         if signature == "custom_signature" {
46///             Ok(())
47///         } else {
48///             Err(NonceError::InvalidSignature)
49///         }
50///     }
51/// }
52/// ```
53pub trait SignatureAlgorithm: Send + Sync {
54    /// Returns the name/identifier of this signature algorithm.
55    ///
56    /// This is used for debugging and algorithm identification.
57    /// Should be a short, unique identifier like "hmac-sha256" or "ed25519".
58    fn name(&self) -> &'static str;
59
60    /// Generate a signature for the given data.
61    ///
62    /// # Arguments
63    ///
64    /// * `timestamp` - The timestamp to include in the signature
65    /// * `nonce` - The nonce value to include in the signature  
66    /// * `data` - The payload data to sign
67    ///
68    /// # Returns
69    ///
70    /// A base64-encoded signature string, or an error if signing fails.
71    ///
72    /// # Implementation Requirements
73    ///
74    /// The signature MUST include the timestamp and nonce to prevent
75    /// replay attacks and ensure uniqueness. The typical pattern is:
76    ///
77    /// ```text
78    /// signature = algorithm(key, timestamp || nonce || data)
79    /// ```
80    fn sign(&self, timestamp: u64, nonce: &str, data: &[u8]) -> Result<String, NonceError>;
81
82    /// Verify a signature against the given data.
83    ///
84    /// # Arguments
85    ///
86    /// * `timestamp` - The timestamp from the credential
87    /// * `nonce` - The nonce from the credential
88    /// * `data` - The payload data that was signed
89    /// * `signature` - The signature to verify (base64-encoded)
90    ///
91    /// # Returns
92    ///
93    /// `Ok(())` if the signature is valid, `Err(NonceError::InvalidSignature)` otherwise.
94    ///
95    /// # Security Requirements
96    ///
97    /// - Must use constant-time comparison to prevent timing attacks
98    /// - Must validate the signature covers timestamp, nonce, and data
99    /// - Should handle malformed signatures gracefully
100    fn verify(
101        &self,
102        timestamp: u64,
103        nonce: &str,
104        data: &[u8],
105        signature: &str,
106    ) -> Result<(), NonceError>;
107
108    /// Create a signature using a custom MAC builder function.
109    ///
110    /// This method provides maximum flexibility for applications that need
111    /// to include additional data in their signatures or use custom signing logic.
112    ///
113    /// # Arguments
114    ///
115    /// * `builder` - A closure that receives a MAC instance and builds the signature data
116    ///
117    /// # Returns
118    ///
119    /// A base64-encoded signature string, or an error if signing fails.
120    ///
121    /// # Default Implementation
122    ///
123    /// The default implementation calls `sign()` with empty data, which may not
124    /// be appropriate for all algorithms. Custom algorithms should override this
125    /// method if they support flexible signing.
126    fn sign_with<F>(&self, timestamp: u64, nonce: &str, builder: F) -> Result<String, NonceError>
127    where
128        F: FnOnce(&mut dyn MacLike),
129    {
130        // Default implementation - algorithms should override if they support custom signing
131        let _ = (timestamp, nonce, builder);
132        Err(NonceError::CryptoError(
133            "Custom signing not supported by this algorithm".to_string(),
134        ))
135    }
136
137    /// Verify a signature using a custom MAC builder function.
138    ///
139    /// This method provides maximum flexibility for applications that need
140    /// to verify signatures with additional data or custom verification logic.
141    ///
142    /// # Arguments
143    ///
144    /// * `signature` - The signature to verify (base64-encoded)
145    /// * `builder` - A closure that receives a MAC instance and builds the expected signature data
146    ///
147    /// # Returns
148    ///
149    /// `Ok(())` if the signature is valid, `Err(NonceError::InvalidSignature)` otherwise.
150    ///
151    /// # Default Implementation
152    ///
153    /// The default implementation returns an error, indicating custom verification
154    /// is not supported. Custom algorithms should override this method if they
155    /// support flexible verification.
156    fn verify_with<F>(
157        &self,
158        timestamp: u64,
159        nonce: &str,
160        signature: &str,
161        builder: F,
162    ) -> Result<(), NonceError>
163    where
164        F: FnOnce(&mut dyn MacLike),
165    {
166        // Default implementation - algorithms should override if they support custom verification
167        let _ = (timestamp, nonce, signature, builder);
168        Err(NonceError::CryptoError(
169            "Custom verification not supported by this algorithm".to_string(),
170        ))
171    }
172}
173
174/// A trait for MAC-like operations that can be used in custom signing.
175///
176/// This trait abstracts over different MAC implementations to allow
177/// flexible signature construction in the `sign_with` and `verify_with` methods.
178pub trait MacLike {
179    /// Update the MAC with the given data.
180    fn update(&mut self, data: &[u8]);
181}
182
183#[cfg(feature = "algo-hmac-sha256")]
184pub mod hmac_sha256 {
185    //! HMAC-SHA256 signature algorithm implementation.
186
187    use super::{MacLike, SignatureAlgorithm};
188    use crate::NonceError;
189    use base64::Engine;
190    use hmac::{Hmac, Mac};
191    use sha2::Sha256;
192
193    /// HMAC-SHA256 signature algorithm.
194    ///
195    /// This is the default signature algorithm used by nonce-auth.
196    /// It provides strong security guarantees and is widely supported.
197    ///
198    /// # Example
199    ///
200    /// ```rust
201    /// use nonce_auth::signature::hmac_sha256::HmacSha256Algorithm;
202    /// use nonce_auth::signature::SignatureAlgorithm;
203    ///
204    /// let algorithm = HmacSha256Algorithm::new(b"my_secret_key");
205    /// let signature = algorithm.sign(1234567890, "unique_nonce", b"payload")?;
206    ///
207    /// // Verify the signature
208    /// algorithm.verify(1234567890, "unique_nonce", b"payload", &signature)?;
209    /// # Ok::<(), nonce_auth::NonceError>(())
210    /// ```
211    pub struct HmacSha256Algorithm {
212        key: Vec<u8>,
213    }
214
215    impl HmacSha256Algorithm {
216        /// Create a new HMAC-SHA256 algorithm with the given key.
217        ///
218        /// # Arguments
219        ///
220        /// * `key` - The secret key to use for HMAC operations
221        ///
222        /// # Example
223        ///
224        /// ```rust
225        /// use nonce_auth::signature::hmac_sha256::HmacSha256Algorithm;
226        ///
227        /// let algorithm = HmacSha256Algorithm::new(b"secret_key");
228        /// ```
229        pub fn new(key: &[u8]) -> Self {
230            Self { key: key.to_vec() }
231        }
232
233        /// Create an HMAC instance for internal use.
234        fn create_hmac(&self) -> Result<Hmac<Sha256>, NonceError> {
235            Hmac::<Sha256>::new_from_slice(&self.key)
236                .map_err(|e| NonceError::CryptoError(format!("Invalid HMAC key: {e}")))
237        }
238    }
239
240    impl SignatureAlgorithm for HmacSha256Algorithm {
241        fn name(&self) -> &'static str {
242            "hmac-sha256"
243        }
244
245        fn sign(&self, timestamp: u64, nonce: &str, data: &[u8]) -> Result<String, NonceError> {
246            let mut mac = self.create_hmac()?;
247
248            // Standard signature format: timestamp || nonce || data
249            mac.update(timestamp.to_string().as_bytes());
250            mac.update(nonce.as_bytes());
251            mac.update(data);
252
253            let signature = mac.finalize().into_bytes();
254            Ok(base64::engine::general_purpose::STANDARD.encode(signature))
255        }
256
257        fn verify(
258            &self,
259            timestamp: u64,
260            nonce: &str,
261            data: &[u8],
262            signature: &str,
263        ) -> Result<(), NonceError> {
264            // Decode the provided signature
265            let expected_signature = base64::engine::general_purpose::STANDARD
266                .decode(signature)
267                .map_err(|e| NonceError::CryptoError(format!("Invalid base64 signature: {e}")))?;
268
269            // Compute the expected signature
270            let mut mac = self.create_hmac()?;
271            mac.update(timestamp.to_string().as_bytes());
272            mac.update(nonce.as_bytes());
273            mac.update(data);
274
275            // Use constant-time comparison
276            mac.verify_slice(&expected_signature)
277                .map_err(|_| NonceError::InvalidSignature)
278        }
279
280        fn sign_with<F>(
281            &self,
282            timestamp: u64,
283            nonce: &str,
284            builder: F,
285        ) -> Result<String, NonceError>
286        where
287            F: FnOnce(&mut dyn MacLike),
288        {
289            let mut mac = self.create_hmac()?;
290            let mut mac_wrapper = HmacWrapper(&mut mac);
291
292            // Always include timestamp and nonce first
293            mac_wrapper.update(timestamp.to_string().as_bytes());
294            mac_wrapper.update(nonce.as_bytes());
295
296            // Let the builder add additional data
297            builder(&mut mac_wrapper);
298
299            let signature = mac.finalize().into_bytes();
300            Ok(base64::engine::general_purpose::STANDARD.encode(signature))
301        }
302
303        fn verify_with<F>(
304            &self,
305            timestamp: u64,
306            nonce: &str,
307            signature: &str,
308            builder: F,
309        ) -> Result<(), NonceError>
310        where
311            F: FnOnce(&mut dyn MacLike),
312        {
313            // Decode the provided signature
314            let expected_signature = base64::engine::general_purpose::STANDARD
315                .decode(signature)
316                .map_err(|e| NonceError::CryptoError(format!("Invalid base64 signature: {e}")))?;
317
318            // Compute the expected signature using the same process as signing
319            let mut mac = self.create_hmac()?;
320            let mut mac_wrapper = HmacWrapper(&mut mac);
321
322            // Always include timestamp and nonce first (matching sign_with)
323            mac_wrapper.update(timestamp.to_string().as_bytes());
324            mac_wrapper.update(nonce.as_bytes());
325
326            // Let the builder add the same additional data as during signing
327            builder(&mut mac_wrapper);
328
329            // Use constant-time comparison
330            mac.verify_slice(&expected_signature)
331                .map_err(|_| NonceError::InvalidSignature)
332        }
333    }
334
335    /// Wrapper to make HMAC implement MacLike.
336    struct HmacWrapper<'a>(&'a mut Hmac<Sha256>);
337
338    impl<'a> MacLike for HmacWrapper<'a> {
339        fn update(&mut self, data: &[u8]) {
340            self.0.update(data);
341        }
342    }
343
344    #[cfg(test)]
345    mod tests {
346        use super::*;
347
348        #[test]
349        fn test_hmac_sha256_basic_sign_verify() {
350            let algorithm = HmacSha256Algorithm::new(b"test_key");
351            let timestamp = 1234567890;
352            let nonce = "test_nonce";
353            let data = b"test_payload";
354
355            // Sign the data
356            let signature = algorithm.sign(timestamp, nonce, data).unwrap();
357            assert!(!signature.is_empty());
358
359            // Verify the signature
360            algorithm
361                .verify(timestamp, nonce, data, &signature)
362                .unwrap();
363        }
364
365        #[test]
366        fn test_hmac_sha256_invalid_signature() {
367            let algorithm = HmacSha256Algorithm::new(b"test_key");
368            let timestamp = 1234567890;
369            let nonce = "test_nonce";
370            let data = b"test_payload";
371
372            // Try to verify with wrong signature
373            let result = algorithm.verify(timestamp, nonce, data, "invalid_signature");
374            assert!(matches!(result, Err(NonceError::CryptoError(_))));
375
376            // Try to verify with wrong data
377            let signature = algorithm.sign(timestamp, nonce, data).unwrap();
378            let result = algorithm.verify(timestamp, nonce, b"wrong_data", &signature);
379            assert!(matches!(result, Err(NonceError::InvalidSignature)));
380        }
381
382        #[test]
383        fn test_hmac_sha256_sign_with() {
384            let algorithm = HmacSha256Algorithm::new(b"test_key");
385            let timestamp = 1234567890;
386            let nonce = "test_nonce";
387
388            // Sign with custom data
389            let signature = algorithm
390                .sign_with(timestamp, nonce, |mac| {
391                    mac.update(b"custom_data");
392                    mac.update(b"more_data");
393                })
394                .unwrap();
395
396            // Verify with the same custom data
397            algorithm
398                .verify_with(timestamp, nonce, &signature, |mac| {
399                    mac.update(b"custom_data");
400                    mac.update(b"more_data");
401                })
402                .unwrap();
403
404            // Verify should fail with different data
405            let result = algorithm.verify_with(timestamp, nonce, &signature, |mac| {
406                mac.update(b"different_data");
407            });
408            assert!(matches!(result, Err(NonceError::InvalidSignature)));
409        }
410
411        #[test]
412        fn test_hmac_sha256_different_keys_different_signatures() {
413            let algorithm1 = HmacSha256Algorithm::new(b"key1");
414            let algorithm2 = HmacSha256Algorithm::new(b"key2");
415            let timestamp = 1234567890;
416            let nonce = "test_nonce";
417            let data = b"test_payload";
418
419            let signature1 = algorithm1.sign(timestamp, nonce, data).unwrap();
420            let signature2 = algorithm2.sign(timestamp, nonce, data).unwrap();
421
422            // Different keys should produce different signatures
423            assert_ne!(signature1, signature2);
424
425            // Cross-verification should fail
426            let result = algorithm1.verify(timestamp, nonce, data, &signature2);
427            assert!(matches!(result, Err(NonceError::InvalidSignature)));
428        }
429    }
430}
431
432/// Type alias for the default signature algorithm.
433#[cfg(feature = "algo-hmac-sha256")]
434pub type DefaultSignatureAlgorithm = hmac_sha256::HmacSha256Algorithm;
435
436/// Create the default signature algorithm with the given key.
437#[cfg(feature = "algo-hmac-sha256")]
438pub fn create_default_algorithm(key: &[u8]) -> DefaultSignatureAlgorithm {
439    hmac_sha256::HmacSha256Algorithm::new(key)
440}
441
442/// Create the default signature algorithm with the given key.
443#[cfg(not(feature = "algo-hmac-sha256"))]
444pub fn create_default_algorithm(_key: &[u8]) -> ! {
445    compile_error!("No signature algorithm available. Enable at least one algorithm feature.");
446}