Skip to main content

licenz_core/
keys.rs

1//! Key management for license signing and verification
2//!
3//! This module provides key management functionality that works with the
4//! pluggable cryptographic architecture. It maintains backward compatibility
5//! with RSA keys while supporting new algorithms like Ed25519.
6//!
7//! # Example
8//!
9//! ```rust,no_run
10//! use licenz_core::keys::{CryptoKeyPair, KeyPair, KeySize};
11//! use licenz_core::crypto::algorithm_ids;
12//!
13//! // Legacy RSA key pair (backward compatible)
14//! let rsa_keypair = KeyPair::generate(KeySize::Bits2048).unwrap();
15//!
16//! // New algorithm-agnostic key pair
17//! let ed25519_keypair = CryptoKeyPair::generate(algorithm_ids::ED25519).unwrap();
18//! let rsa_keypair = CryptoKeyPair::generate(algorithm_ids::RSA_SHA256).unwrap();
19//! ```
20
21use crate::crypto::{algorithm_ids, CryptoRegistry, SignatureAlgorithm};
22use crate::error::{LicenseError, Result};
23use pem::{encode, Pem};
24use rand::rngs::OsRng;
25use rsa::pkcs1::{DecodeRsaPrivateKey, DecodeRsaPublicKey};
26use rsa::pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey};
27use rsa::{RsaPrivateKey, RsaPublicKey};
28use std::path::Path;
29use zeroize::Zeroizing;
30
31/// Supported RSA key sizes
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub enum KeySize {
34    Bits2048,
35    #[default]
36    Bits3072,
37    Bits4096,
38}
39
40impl KeySize {
41    pub fn bits(&self) -> usize {
42        match self {
43            KeySize::Bits2048 => 2048,
44            KeySize::Bits3072 => 3072,
45            KeySize::Bits4096 => 4096,
46        }
47    }
48}
49
50/// RSA key pair for license signing
51pub struct KeyPair {
52    private_key: RsaPrivateKey,
53    pub public_key: RsaPublicKey,
54}
55
56impl std::fmt::Debug for KeyPair {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        f.debug_struct("KeyPair")
59            .field("private_key", &"[REDACTED]")
60            .field("public_key", &"<RsaPublicKey>")
61            .finish()
62    }
63}
64
65impl KeyPair {
66    /// Get a reference to the private key
67    pub fn private_key(&self) -> &RsaPrivateKey {
68        &self.private_key
69    }
70
71    /// Consume the key pair and return the private key
72    pub fn into_private_key(self) -> RsaPrivateKey {
73        self.private_key
74    }
75
76    /// Generate a new RSA key pair
77    pub fn generate(size: KeySize) -> Result<Self> {
78        let mut rng = OsRng;
79        let private_key = RsaPrivateKey::new(&mut rng, size.bits())
80            .map_err(|e| LicenseError::KeyGenerationFailed(e.to_string()))?;
81        let public_key = RsaPublicKey::from(&private_key);
82
83        Ok(Self {
84            private_key,
85            public_key,
86        })
87    }
88
89    /// Export the private key as PEM string
90    pub fn export_private_pem(&self) -> Result<String> {
91        let der = self
92            .private_key
93            .to_pkcs8_der()
94            .map_err(|e| LicenseError::InvalidKeyFormat(e.to_string()))?;
95
96        let pem = Pem::new("PRIVATE KEY", der.as_bytes());
97        Ok(encode(&pem))
98    }
99
100    /// Export the public key as PEM string
101    pub fn export_public_pem(&self) -> Result<String> {
102        let der = self
103            .public_key
104            .to_public_key_der()
105            .map_err(|e| LicenseError::InvalidKeyFormat(e.to_string()))?;
106
107        let pem = Pem::new("PUBLIC KEY", der.as_bytes());
108        Ok(encode(&pem))
109    }
110
111    /// Save the key pair to files
112    pub fn save_to_files(&self, private_path: &Path, public_path: &Path) -> Result<()> {
113        std::fs::write(private_path, self.export_private_pem()?)?;
114        std::fs::write(public_path, self.export_public_pem()?)?;
115        Ok(())
116    }
117
118    /// Load a key pair from files
119    ///
120    /// On Unix, this checks that the private key file is not readable by group/other.
121    pub fn load_from_files(private_path: &Path, public_path: &Path) -> Result<Self> {
122        check_private_key_permissions(private_path)?;
123
124        let private_pem = std::fs::read_to_string(private_path)?;
125        let public_pem = std::fs::read_to_string(public_path)?;
126
127        let private_key = parse_private_key(&private_pem)?;
128        let public_key = parse_public_key(&public_pem)?;
129
130        Ok(Self {
131            private_key,
132            public_key,
133        })
134    }
135}
136
137/// Parse a private key from PEM format
138pub fn parse_private_key(pem_str: &str) -> Result<RsaPrivateKey> {
139    // Handle escaped newlines (from LDFLAGS injection)
140    let pem_str = pem_str.replace("\\n", "\n");
141
142    // Try PKCS#8 format first
143    if let Ok(key) = RsaPrivateKey::from_pkcs8_pem(&pem_str) {
144        return Ok(key);
145    }
146
147    // Try PKCS#1 format
148    if let Ok(key) = RsaPrivateKey::from_pkcs1_pem(&pem_str) {
149        return Ok(key);
150    }
151
152    Err(LicenseError::InvalidKeyFormat(
153        "Could not parse private key (tried PKCS#8 and PKCS#1 formats)".into(),
154    ))
155}
156
157/// Parse a public key from PEM format
158pub fn parse_public_key(pem_str: &str) -> Result<RsaPublicKey> {
159    // Handle escaped newlines (from LDFLAGS injection or env vars)
160    let pem_str = pem_str.replace("\\n", "\n");
161
162    // Try SPKI format first (most common)
163    if let Ok(key) = RsaPublicKey::from_public_key_pem(&pem_str) {
164        return Ok(key);
165    }
166
167    // Try PKCS#1 format
168    if let Ok(key) = RsaPublicKey::from_pkcs1_pem(&pem_str) {
169        return Ok(key);
170    }
171
172    Err(LicenseError::InvalidKeyFormat(
173        "Could not parse public key (tried SPKI and PKCS#1 formats)".into(),
174    ))
175}
176
177/// Extract the public key from a private key
178pub fn extract_public_key(private_key: &RsaPrivateKey) -> RsaPublicKey {
179    RsaPublicKey::from(private_key)
180}
181
182/// Algorithm-agnostic key pair that works with any supported signature algorithm
183///
184/// This struct provides a unified interface for key management across different
185/// cryptographic algorithms (RSA, Ed25519, etc.).
186///
187/// The private key is stored in a `Zeroizing<String>` wrapper that automatically
188/// clears memory when dropped, preventing key material from lingering in memory.
189pub struct CryptoKeyPair {
190    /// The private key in PEM format (zeroized on drop)
191    private_key_pem: Zeroizing<String>,
192    /// The public key in PEM format
193    pub public_key_pem: String,
194    /// The algorithm identifier (e.g., "RSA-SHA256", "Ed25519")
195    pub algorithm_id: String,
196}
197
198impl std::fmt::Debug for CryptoKeyPair {
199    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200        f.debug_struct("CryptoKeyPair")
201            .field("private_key_pem", &"[REDACTED]")
202            .field(
203                "public_key_pem",
204                &format!(
205                    "{}...",
206                    &self.public_key_pem[..40.min(self.public_key_pem.len())]
207                ),
208            )
209            .field("algorithm_id", &self.algorithm_id)
210            .finish()
211    }
212}
213
214impl CryptoKeyPair {
215    /// Get a reference to the private key PEM
216    pub fn private_key_pem(&self) -> &str {
217        &self.private_key_pem
218    }
219
220    /// Generate a new key pair using the specified algorithm
221    ///
222    /// # Arguments
223    /// * `algorithm_id` - The algorithm to use (e.g., "RSA-SHA256", "Ed25519")
224    ///
225    /// # Example
226    /// ```rust,no_run
227    /// use licenz_core::keys::CryptoKeyPair;
228    /// use licenz_core::crypto::algorithm_ids;
229    ///
230    /// let keypair = CryptoKeyPair::generate(algorithm_ids::ED25519).unwrap();
231    /// ```
232    pub fn generate(algorithm_id: &str) -> Result<Self> {
233        let algorithm = CryptoRegistry::get_signature_algorithm(algorithm_id)?;
234        let (private_key_pem, public_key_pem) = algorithm.generate_keypair()?;
235        Ok(Self {
236            private_key_pem: Zeroizing::new(private_key_pem),
237            public_key_pem,
238            algorithm_id: algorithm_id.to_string(),
239        })
240    }
241
242    /// Create from existing PEM keys
243    ///
244    /// # Arguments
245    /// * `private_key_pem` - The private key in PEM format
246    /// * `public_key_pem` - The public key in PEM format
247    /// * `algorithm_id` - The algorithm identifier
248    pub fn from_pem(private_key_pem: String, public_key_pem: String, algorithm_id: &str) -> Self {
249        Self {
250            private_key_pem: Zeroizing::new(private_key_pem),
251            public_key_pem,
252            algorithm_id: algorithm_id.to_string(),
253        }
254    }
255
256    /// Load a key pair from files
257    ///
258    /// On Unix, this checks that the private key file is not readable by group/other.
259    ///
260    /// # Arguments
261    /// * `private_path` - Path to the private key PEM file
262    /// * `public_path` - Path to the public key PEM file
263    /// * `algorithm_id` - The algorithm identifier
264    pub fn load_from_files(
265        private_path: &Path,
266        public_path: &Path,
267        algorithm_id: &str,
268    ) -> Result<Self> {
269        check_private_key_permissions(private_path)?;
270
271        let private_key_pem = std::fs::read_to_string(private_path)?;
272        let public_key_pem = std::fs::read_to_string(public_path)?;
273        Ok(Self::from_pem(
274            private_key_pem,
275            public_key_pem,
276            algorithm_id,
277        ))
278    }
279
280    /// Save the key pair to files
281    pub fn save_to_files(&self, private_path: &Path, public_path: &Path) -> Result<()> {
282        std::fs::write(private_path, self.private_key_pem.as_str())?;
283        std::fs::write(public_path, &self.public_key_pem)?;
284
285        // Set restrictive permissions on Unix
286        #[cfg(unix)]
287        {
288            use std::os::unix::fs::PermissionsExt;
289            let perms = std::fs::Permissions::from_mode(0o600);
290            std::fs::set_permissions(private_path, perms)?;
291        }
292
293        Ok(())
294    }
295
296    /// Sign data using this key pair's private key
297    pub fn sign(&self, data: &[u8]) -> Result<Vec<u8>> {
298        let algorithm = CryptoRegistry::get_signature_algorithm(&self.algorithm_id)?;
299        algorithm.sign(data, &self.private_key_pem)
300    }
301
302    /// Verify a signature using this key pair's public key
303    pub fn verify(&self, data: &[u8], signature: &[u8]) -> Result<()> {
304        let algorithm = CryptoRegistry::get_signature_algorithm(&self.algorithm_id)?;
305        algorithm.verify(data, signature, &self.public_key_pem)
306    }
307
308    /// Get the signature algorithm for this key pair
309    pub fn get_algorithm(&self) -> Result<&'static dyn SignatureAlgorithm> {
310        CryptoRegistry::get_signature_algorithm(&self.algorithm_id)
311    }
312
313    /// Convert a legacy RSA KeyPair to a CryptoKeyPair
314    pub fn from_rsa_keypair(keypair: &KeyPair) -> Result<Self> {
315        Ok(Self {
316            private_key_pem: Zeroizing::new(keypair.export_private_pem()?),
317            public_key_pem: keypair.export_public_pem()?,
318            algorithm_id: algorithm_ids::RSA_SHA256.to_string(),
319        })
320    }
321}
322
323/// Check that a private key file has restrictive permissions (Unix only)
324///
325/// Returns an error if the file is readable by group or other users.
326/// On non-Unix platforms, this is a no-op.
327fn check_private_key_permissions(path: &Path) -> Result<()> {
328    #[cfg(unix)]
329    {
330        use std::os::unix::fs::PermissionsExt;
331
332        if !path.exists() {
333            return Ok(()); // File doesn't exist yet; let the caller handle it
334        }
335
336        let metadata = std::fs::metadata(path)?;
337        let mode = metadata.permissions().mode();
338
339        // Check if group or other have any permissions
340        if mode & 0o077 != 0 {
341            return Err(LicenseError::InsecureKeyPermissions {
342                path: path.to_path_buf(),
343                mode: format!("{:04o}", mode & 0o7777),
344                suggestion: "Run: chmod 600 <file>".to_string(),
345            });
346        }
347    }
348    #[cfg(not(unix))]
349    {
350        let _ = path; // suppress unused warning
351    }
352    Ok(())
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    #[test]
360    fn test_key_generation() {
361        let keypair = KeyPair::generate(KeySize::Bits2048).unwrap();
362
363        let private_pem = keypair.export_private_pem().unwrap();
364        let public_pem = keypair.export_public_pem().unwrap();
365
366        assert!(private_pem.contains("PRIVATE KEY"));
367        assert!(public_pem.contains("PUBLIC KEY"));
368    }
369
370    #[test]
371    fn test_key_round_trip() {
372        let keypair = KeyPair::generate(KeySize::Bits2048).unwrap();
373
374        let private_pem = keypair.export_private_pem().unwrap();
375        let public_pem = keypair.export_public_pem().unwrap();
376
377        let parsed_private = parse_private_key(&private_pem).unwrap();
378        let parsed_public = parse_public_key(&public_pem).unwrap();
379
380        assert_eq!(*keypair.private_key(), parsed_private);
381        assert_eq!(keypair.public_key, parsed_public);
382    }
383
384    #[test]
385    fn test_crypto_keypair_rsa() {
386        let keypair = CryptoKeyPair::generate(algorithm_ids::RSA_SHA256).unwrap();
387        assert_eq!(keypair.algorithm_id, algorithm_ids::RSA_SHA256);
388        assert!(keypair.private_key_pem().contains("PRIVATE KEY"));
389        assert!(keypair.public_key_pem.contains("PUBLIC KEY"));
390
391        // Test sign and verify
392        let data = b"test message for RSA";
393        let signature = keypair.sign(data).unwrap();
394        assert!(keypair.verify(data, &signature).is_ok());
395    }
396
397    #[test]
398    fn test_crypto_keypair_ed25519() {
399        let keypair = CryptoKeyPair::generate(algorithm_ids::ED25519).unwrap();
400        assert_eq!(keypair.algorithm_id, algorithm_ids::ED25519);
401        assert!(keypair.private_key_pem().contains("PRIVATE KEY"));
402        assert!(keypair.public_key_pem.contains("PUBLIC KEY"));
403
404        // Test sign and verify
405        let data = b"test message for Ed25519";
406        let signature = keypair.sign(data).unwrap();
407        assert!(keypair.verify(data, &signature).is_ok());
408
409        // Ed25519 signatures are always 64 bytes
410        assert_eq!(signature.len(), 64);
411    }
412
413    #[test]
414    fn test_crypto_keypair_from_rsa_keypair() {
415        let rsa_keypair = KeyPair::generate(KeySize::Bits2048).unwrap();
416        let crypto_keypair = CryptoKeyPair::from_rsa_keypair(&rsa_keypair).unwrap();
417
418        assert_eq!(crypto_keypair.algorithm_id, algorithm_ids::RSA_SHA256);
419
420        // Test that signing works
421        let data = b"conversion test";
422        let signature = crypto_keypair.sign(data).unwrap();
423        assert!(crypto_keypair.verify(data, &signature).is_ok());
424    }
425}