atlas_cli/signing/
mod.rs

1use crate::error::{Error, Result};
2use atlas_c2pa_lib::cose::HashAlgorithm;
3use openssl::hash::MessageDigest;
4use openssl::pkey::{PKey, Private, Public};
5use openssl::sign::Signer;
6use std::fs::read;
7use std::path::Path;
8use zeroize::{ZeroizeOnDrop, Zeroizing};
9
10pub mod signable;
11
12/// Secure wrapper for private key data that zeroizes on drop
13#[derive(ZeroizeOnDrop)]
14pub struct SecurePrivateKey {
15    #[zeroize(skip)]
16    pkey: PKey<Private>,
17    // Store the original key bytes in case we need them
18    _key_data: Zeroizing<Vec<u8>>,
19}
20
21impl SecurePrivateKey {
22    /// Create a new SecurePrivateKey from raw PEM data
23    pub fn from_pem(pem_data: Vec<u8>) -> Result<Self> {
24        // Wrap the PEM data in Zeroizing to ensure it's cleared when dropped
25        let zeroizing_pem = Zeroizing::new(pem_data);
26
27        // Parse the private key
28        let pkey = PKey::private_key_from_pem(&zeroizing_pem)
29            .map_err(|e| Error::Signing(format!("Failed to load private key: {e}")))?;
30
31        Ok(Self {
32            pkey,
33            _key_data: zeroizing_pem,
34        })
35    }
36
37    /// Get a reference to the inner PKey
38    pub fn as_pkey(&self) -> &PKey<Private> {
39        &self.pkey
40    }
41}
42
43/// Load a private key from a file path with automatic zeroization
44pub fn load_private_key(key_path: &Path) -> Result<SecurePrivateKey> {
45    // Read the key data - will be automatically zeroized when dropped
46    let key_data = read(key_path)?;
47    SecurePrivateKey::from_pem(key_data)
48}
49
50/// Sign data with a specific hash algorithm and automatic key zeroization
51pub fn sign_data_with_algorithm(
52    data: &[u8],
53    private_key: &SecurePrivateKey,
54    algorithm: &HashAlgorithm,
55) -> Result<Vec<u8>> {
56    let message_digest = match algorithm {
57        HashAlgorithm::Sha256 => MessageDigest::sha256(),
58        HashAlgorithm::Sha384 => MessageDigest::sha384(),
59        HashAlgorithm::Sha512 => MessageDigest::sha512(),
60    };
61
62    let mut signer = Signer::new(message_digest, private_key.as_pkey())
63        .map_err(|e| Error::Signing(format!("Failed to create signer: {e}")))?;
64
65    signer
66        .update(data)
67        .map_err(|e| Error::Signing(format!("Failed to update signer: {e}")))?;
68
69    // Sign to a zeroizing vector first to ensure cleanup
70    let sig_len = signer
71        .len()
72        .map_err(|e| Error::Signing(format!("Failed to get signature length: {e}")))?;
73    let mut signature = Zeroizing::new(vec![0u8; sig_len]);
74    let len = signer
75        .sign(&mut signature)
76        .map_err(|e| Error::Signing(format!("Failed to sign data: {e}")))?;
77
78    // Return only the used portion of the signature
79    Ok(signature[..len].to_vec())
80}
81
82/// Sign data with default SHA-384 algorithm
83pub fn sign_data(data: &[u8], private_key: &SecurePrivateKey) -> Result<Vec<u8>> {
84    sign_data_with_algorithm(data, private_key, &HashAlgorithm::Sha384)
85}
86
87// Verify signature with a public key using default SHA-384 algorithm
88pub fn verify_signature(data: &[u8], signature: &[u8], public_key: &PKey<Public>) -> Result<bool> {
89    verify_signature_with_algorithm(data, signature, public_key, &HashAlgorithm::Sha384)
90}
91
92/// Verify signature with a public key using the specified algorithm
93pub fn verify_signature_with_algorithm(
94    data: &[u8],
95    signature: &[u8],
96    public_key: &PKey<Public>,
97    algorithm: &HashAlgorithm,
98) -> Result<bool> {
99    let message_digest = match algorithm {
100        HashAlgorithm::Sha256 => MessageDigest::sha256(),
101        HashAlgorithm::Sha384 => MessageDigest::sha384(),
102        HashAlgorithm::Sha512 => MessageDigest::sha512(),
103    };
104
105    let mut verifier = openssl::sign::Verifier::new(message_digest, public_key)
106        .map_err(|e| Error::Signing(e.to_string()))?;
107
108    verifier
109        .update(data)
110        .map_err(|e| Error::Signing(e.to_string()))?;
111
112    verifier
113        .verify(signature)
114        .map_err(|e| Error::Signing(e.to_string()))
115}
116
117pub fn pkey_to_secure(pkey: PKey<Private>) -> Result<SecurePrivateKey> {
118    // Export to PEM format then re-import as SecurePrivateKey
119    let pem_data = pkey
120        .private_key_to_pem_pkcs8()
121        .map_err(|e| Error::Signing(format!("Failed to export key to PEM: {e}")))?;
122
123    SecurePrivateKey::from_pem(pem_data)
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use crate::error::Result;
130    use crate::signing::test_utils::generate_temp_key;
131
132    #[test]
133    fn test_load_private_key() -> Result<()> {
134        // Generate a test key and save it to a temporary file
135        let (secure_key, _dir) = generate_temp_key()?;
136
137        // Test that we can use the key for signing
138        let test_data = b"test data for signing";
139        let signature = sign_data(test_data, &secure_key)?;
140
141        // Signature should not be empty
142        assert!(!signature.is_empty());
143
144        Ok(())
145    }
146
147    #[test]
148    fn test_sign_data() -> Result<()> {
149        // Generate a temporary key
150        let (secure_key, _) = generate_temp_key()?;
151
152        // Test data
153        let data1 = b"test data for signing";
154        let data2 = b"different test data";
155
156        // Sign the data
157        let signature1 = sign_data(data1, &secure_key)?;
158        let signature2 = sign_data(data1, &secure_key)?; // Same data again
159        let signature3 = sign_data(data2, &secure_key)?; // Different data
160
161        // Verify signatures have expected properties
162        assert!(!signature1.is_empty(), "Signature should not be empty");
163
164        // Same data should produce the same signature with the same key
165        assert_eq!(
166            signature1, signature2,
167            "Signatures for the same data should match"
168        );
169
170        // Different data should produce different signatures
171        assert_ne!(
172            signature1, signature3,
173            "Signatures for different data should not match"
174        );
175
176        Ok(())
177    }
178
179    #[test]
180    fn test_signature_different_keys() -> Result<()> {
181        // Generate two different keys
182        let (secure_key1, _) = generate_temp_key()?;
183        let (secure_key2, _) = generate_temp_key()?;
184
185        // Test data
186        let data = b"test data for signature comparison";
187
188        // Sign with both keys
189        let signature1 = sign_data(data, &secure_key1)?;
190        let signature2 = sign_data(data, &secure_key2)?;
191
192        // Different keys should produce different signatures for the same data
193        assert_ne!(
194            signature1, signature2,
195            "Signatures from different keys should not match"
196        );
197
198        Ok(())
199    }
200
201    #[test]
202    fn test_load_private_key_error() {
203        // Attempt to load a non-existent key file
204        let result = load_private_key(std::path::Path::new("/nonexistent/path/to/key.pem"));
205
206        // Should return an error
207        assert!(result.is_err(), "Loading non-existent key should fail");
208
209        // The error should be an IO error
210        if let Err(e) = result {
211            match e {
212                crate::error::Error::Io(_) => {} // Expected error type
213                _ => panic!("Unexpected error type: {e:?}"),
214            }
215        }
216    }
217
218    #[test]
219    fn test_sign_data_with_empty_data() -> Result<()> {
220        // Generate a temporary key
221        let (secure_key, _) = generate_temp_key()?;
222
223        // Sign empty data
224        let signature = sign_data(&[], &secure_key)?;
225
226        // Even empty data should produce a valid signature
227        assert!(
228            !signature.is_empty(),
229            "Signature of empty data should not be empty"
230        );
231
232        Ok(())
233    }
234
235    #[test]
236    fn test_sign_large_data() -> Result<()> {
237        // Generate a temporary key
238        let (secure_key, _) = generate_temp_key()?;
239
240        // Generate larger test data (e.g., 100KB for test speed)
241        let large_data = vec![0x55; 100 * 1024]; // 100KB of the byte 0x55
242
243        // Sign the large data
244        let signature = sign_data(&large_data, &secure_key)?;
245
246        // Should produce a valid signature
247        assert!(
248            !signature.is_empty(),
249            "Signature of large data should not be empty"
250        );
251
252        Ok(())
253    }
254
255    #[test]
256    fn test_secure_key_zeroization() -> Result<()> {
257        // Create a key with known content
258        let pem_data = b"-----BEGIN PRIVATE KEY-----
259MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7W8pGqWu2VZtD
260TEST_KEY_DATA_THAT_SHOULD_BE_ZEROIZED
261-----END PRIVATE KEY-----"
262            .to_vec();
263
264        // Create a SecurePrivateKey in a scope
265        {
266            let _secure_key = SecurePrivateKey::from_pem(pem_data.clone());
267            // Key exists here
268        }
269        // Key should be zeroized after this point
270
271        // Note: In a real scenario, we would need more sophisticated testing
272        // to verify memory has been zeroized, but Rust's ownership system
273        // and the zeroize crate ensure this happens.
274
275        Ok(())
276    }
277
278    #[test]
279    fn test_sign_with_different_algorithms() -> Result<()> {
280        // Generate a temporary key
281        let (secure_key, _) = generate_temp_key()?;
282
283        let data = b"test data for different algorithms";
284
285        // Test different algorithms
286        let sig_sha256 = sign_data_with_algorithm(data, &secure_key, &HashAlgorithm::Sha256)?;
287        let sig_sha384 = sign_data_with_algorithm(data, &secure_key, &HashAlgorithm::Sha384)?;
288        let sig_sha512 = sign_data_with_algorithm(data, &secure_key, &HashAlgorithm::Sha512)?;
289
290        // All signatures should be non-empty
291        assert!(!sig_sha256.is_empty());
292        assert!(!sig_sha384.is_empty());
293        assert!(!sig_sha512.is_empty());
294
295        // Different algorithms produce different signatures
296        assert_ne!(sig_sha256, sig_sha384);
297        assert_ne!(sig_sha384, sig_sha512);
298        assert_ne!(sig_sha256, sig_sha512);
299
300        Ok(())
301    }
302    #[test]
303    fn test_zeroization_with_multiple_references() -> Result<()> {
304        // Test that zeroization works correctly with multiple references
305        use std::sync::Arc;
306
307        let (secure_key, _dir) = generate_temp_key()?;
308
309        // Create multiple references to the same key
310        let key_arc = Arc::new(secure_key);
311        let key_ref1 = Arc::clone(&key_arc);
312        let key_ref2 = Arc::clone(&key_arc);
313
314        // Use the key through different references
315        let data = b"test data";
316
317        let sig1 = sign_data(data, &key_ref1)?;
318        let sig2 = sign_data(data, &key_ref2)?;
319
320        // Signatures should be the same
321        assert_eq!(sig1, sig2);
322
323        // Drop references one by one
324        drop(key_ref1);
325        assert_eq!(Arc::strong_count(&key_arc), 2);
326
327        drop(key_ref2);
328        assert_eq!(Arc::strong_count(&key_arc), 1);
329
330        // The key is still usable through the last reference
331        let sig3 = sign_data(data, &key_arc)?;
332        assert_eq!(sig1, sig3);
333
334        // When the last Arc is dropped, the key will be zeroized
335        drop(key_arc);
336
337        Ok(())
338    }
339    #[test]
340    fn test_sign_and_verify_with_algorithms() -> Result<()> {
341        // Test that signing and verification work correctly with matching algorithms
342        let (secure_key, _dir) = generate_temp_key()?;
343
344        // Get the public key for verification
345        let public_key = secure_key
346            .as_pkey()
347            .public_key_to_pem()
348            .map_err(|e| Error::Signing(e.to_string()))?;
349        let public_key =
350            PKey::public_key_from_pem(&public_key).map_err(|e| Error::Signing(e.to_string()))?;
351
352        let data = b"test data for sign and verify";
353
354        // Test each algorithm
355        for algo in &[
356            HashAlgorithm::Sha256,
357            HashAlgorithm::Sha384,
358            HashAlgorithm::Sha512,
359        ] {
360            // Sign with the algorithm
361            let signature = sign_data_with_algorithm(data, &secure_key, algo)?;
362
363            // Verify with the same algorithm - should succeed
364            let valid = verify_signature_with_algorithm(data, &signature, &public_key, algo)?;
365            assert!(
366                valid,
367                "Verification should succeed with matching algorithm {:?}",
368                algo
369            );
370
371            // Verify with different algorithms - should fail
372            for wrong_algo in &[
373                HashAlgorithm::Sha256,
374                HashAlgorithm::Sha384,
375                HashAlgorithm::Sha512,
376            ] {
377                if wrong_algo != algo {
378                    let invalid =
379                        verify_signature_with_algorithm(data, &signature, &public_key, wrong_algo)?;
380                    assert!(
381                        !invalid,
382                        "Verification should fail with mismatched algorithms {:?} != {:?}",
383                        algo, wrong_algo
384                    );
385                }
386            }
387        }
388
389        // Test default functions (should use SHA-384)
390        let signature = sign_data(data, &secure_key)?;
391        let valid = verify_signature(data, &signature, &public_key)?;
392        assert!(valid, "Default sign/verify should work together");
393
394        // Verify that default uses SHA-384
395        let valid_384 =
396            verify_signature_with_algorithm(data, &signature, &public_key, &HashAlgorithm::Sha384)?;
397        assert!(valid_384, "Default should be SHA-384");
398
399        let invalid_256 =
400            verify_signature_with_algorithm(data, &signature, &public_key, &HashAlgorithm::Sha256)?;
401        assert!(!invalid_256, "Default should not be SHA-256");
402
403        Ok(())
404    }
405}
406
407#[cfg(test)]
408pub(crate) mod test_utils {
409    use crate::error::Result;
410    use crate::signing::SecurePrivateKey;
411    use crate::signing::load_private_key;
412    use openssl::pkey::PKey;
413    use openssl::rsa::Rsa;
414    use std::fs::File;
415    use std::io::Write;
416    use tempfile::tempdir;
417
418    // Helper function to generate a temporary private key for testing
419    pub fn generate_temp_key() -> Result<(SecurePrivateKey, tempfile::TempDir)> {
420        // Create a temporary directory
421        let dir = tempdir()?;
422        let key_path = dir.path().join("test_key.pem");
423
424        // Generate a new RSA key pair (using 2048 bits for speed in tests)
425        let rsa = Rsa::generate(2048).map_err(|e| crate::error::Error::Signing(e.to_string()))?;
426
427        // Convert to PKey
428        let private_key =
429            PKey::from_rsa(rsa).map_err(|e| crate::error::Error::Signing(e.to_string()))?;
430
431        // Write private key to file
432        let pem = private_key
433            .private_key_to_pem_pkcs8()
434            .map_err(|e| crate::error::Error::Signing(e.to_string()))?;
435
436        let mut key_file = File::create(&key_path)?;
437        key_file.write_all(&pem)?;
438
439        // Now load it as SecurePrivateKey
440        let secure_key = load_private_key(&key_path)?;
441
442        Ok((secure_key, dir))
443    }
444}