truthlinked_sdk/
signing.rs

1use hmac::{Hmac, Mac};
2use sha2::Sha256;
3use std::time::{SystemTime, UNIX_EPOCH};
4use base64::Engine;
5
6type HmacSha256 = Hmac<Sha256>;
7
8/// Request signing for replay attack prevention
9pub struct RequestSigner {
10    signing_key: [u8; 32],
11}
12
13impl RequestSigner {
14    /// Create new request signer from license key
15    pub fn new(license_key: &str) -> Self {
16        // Derive signing key from license key using HMAC
17        let mut mac = HmacSha256::new_from_slice(b"truthlinked-request-signing-v1")
18            .expect("HMAC can take key of any size");
19        mac.update(license_key.as_bytes());
20        let result = mac.finalize();
21        
22        let mut signing_key = [0u8; 32];
23        signing_key.copy_from_slice(&result.into_bytes());
24        
25        Self { signing_key }
26    }
27    
28    /// Sign a request with timestamp and body
29    pub fn sign_request(
30        &self,
31        method: &str,
32        path: &str,
33        body: &[u8],
34        timestamp: u64,
35    ) -> String {
36        let mut mac = HmacSha256::new_from_slice(&self.signing_key)
37            .expect("Valid key length");
38        
39        // Sign: METHOD\nPATH\nTIMESTAMP\nBODY
40        mac.update(method.as_bytes());
41        mac.update(b"\n");
42        mac.update(path.as_bytes());
43        mac.update(b"\n");
44        mac.update(timestamp.to_string().as_bytes());
45        mac.update(b"\n");
46        mac.update(body);
47        
48        let signature = mac.finalize();
49        base64::engine::general_purpose::STANDARD.encode(signature.into_bytes())
50    }
51    
52    /// Get current timestamp
53    pub fn current_timestamp() -> u64 {
54        SystemTime::now()
55            .duration_since(UNIX_EPOCH)
56            .unwrap()
57            .as_secs()
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    
65    #[test]
66    fn test_request_signing() {
67        let signer = RequestSigner::new("test_key");
68        let signature = signer.sign_request("GET", "/health", b"", 1234567890);
69        
70        // Should be deterministic
71        let signature2 = signer.sign_request("GET", "/health", b"", 1234567890);
72        assert_eq!(signature, signature2);
73        
74        // Different timestamp should produce different signature
75        let signature3 = signer.sign_request("GET", "/health", b"", 1234567891);
76        assert_ne!(signature, signature3);
77    }
78}