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}