rusty_common/auth/
pem.rs

1//! PEM format validation utilities
2//!
3//! This module provides shared PEM validation functionality used across
4//! different exchange authentication implementations.
5
6use crate::Result;
7use crate::error::CommonError;
8
9/// Valid PEM header types for private keys
10const VALID_PEM_HEADERS: &[&str] = &[
11    "-----BEGIN PRIVATE KEY-----",
12    "-----BEGIN EC PRIVATE KEY-----",
13    "-----BEGIN RSA PRIVATE KEY-----",
14    "-----BEGIN ENCRYPTED PRIVATE KEY-----",
15];
16
17/// Valid PEM footer types for private keys
18const VALID_PEM_FOOTERS: &[&str] = &[
19    "-----END PRIVATE KEY-----",
20    "-----END EC PRIVATE KEY-----",
21    "-----END RSA PRIVATE KEY-----",
22    "-----END ENCRYPTED PRIVATE KEY-----",
23];
24
25/// Validate PEM format for private keys
26///
27/// # Arguments
28/// * `private_key` - The PEM-formatted private key string to validate
29///
30/// # Returns
31/// * `Ok(())` if the PEM format is valid
32/// * `Err(CommonError::Auth)` if the format is invalid
33///
34/// # Example
35/// ```
36/// use rusty_common::auth::pem::validate_pem_format;
37///
38/// let valid_pem = "-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----";
39/// assert!(validate_pem_format(valid_pem).is_ok());
40///
41/// let invalid_pem = "not a pem key";
42/// assert!(validate_pem_format(invalid_pem).is_err());
43/// ```
44pub fn validate_pem_format(private_key: &str) -> Result<()> {
45    // Check basic PEM structure
46    if !private_key.starts_with("-----BEGIN")
47        || !(private_key.ends_with("-----\n") || private_key.ends_with("-----"))
48    {
49        return Err(CommonError::Auth(
50            "Invalid PEM format: must start with '-----BEGIN' and end with '-----'".into(),
51        ));
52    }
53
54    // Check for valid PEM header
55    let has_valid_header = VALID_PEM_HEADERS
56        .iter()
57        .any(|header| private_key.contains(header));
58    if !has_valid_header {
59        return Err(CommonError::Auth(
60            "Invalid PEM format: must contain valid private key header".into(),
61        ));
62    }
63
64    // Check for corresponding END marker
65    let has_valid_footer = VALID_PEM_FOOTERS
66        .iter()
67        .any(|footer| private_key.contains(footer));
68    if !has_valid_footer {
69        return Err(CommonError::Auth(
70            "Invalid PEM format: must contain valid private key footer".into(),
71        ));
72    }
73
74    // Check header/footer match
75    for (header, footer) in VALID_PEM_HEADERS.iter().zip(VALID_PEM_FOOTERS.iter()) {
76        if private_key.contains(header) && !private_key.contains(footer) {
77            return Err(CommonError::Auth(
78                format!(
79                    "Invalid PEM format: header '{header}' requires matching footer '{footer}'"
80                )
81                .into(),
82            ));
83        }
84    }
85
86    Ok(())
87}
88
89/// Extract the key type from a PEM-formatted string
90///
91/// # Arguments
92/// * `private_key` - The PEM-formatted private key string
93///
94/// # Returns
95/// * `Some(&str)` containing the key type (e.g., "EC", "RSA", or generic "")
96/// * `None` if no valid header is found
97pub fn extract_pem_key_type(private_key: &str) -> Option<&'static str> {
98    if private_key.contains("-----BEGIN EC PRIVATE KEY-----") {
99        Some("EC")
100    } else if private_key.contains("-----BEGIN RSA PRIVATE KEY-----") {
101        Some("RSA")
102    } else if private_key.contains("-----BEGIN PRIVATE KEY-----") {
103        Some("") // Generic PKCS#8
104    } else if private_key.contains("-----BEGIN ENCRYPTED PRIVATE KEY-----") {
105        Some("ENCRYPTED")
106    } else {
107        None
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_valid_pem_formats() {
117        let valid_pems = vec![
118            "-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----",
119            "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEE...\n-----END EC PRIVATE KEY-----",
120            "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIB...\n-----END RSA PRIVATE KEY-----",
121            "-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----\n",
122        ];
123
124        for pem in valid_pems {
125            assert!(
126                validate_pem_format(pem).is_ok(),
127                "Failed to validate: {pem}"
128            );
129        }
130    }
131
132    #[test]
133    fn test_invalid_pem_formats() {
134        let invalid_pems = vec![
135            "not a pem key",
136            "-----BEGIN PRIVATE KEY-----",
137            "-----END PRIVATE KEY-----",
138            "-----BEGIN CERTIFICATE-----\nMIIE...\n-----END CERTIFICATE-----",
139            "-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END RSA PRIVATE KEY-----", // Mismatched
140        ];
141
142        for pem in invalid_pems {
143            assert!(
144                validate_pem_format(pem).is_err(),
145                "Should have failed: {pem}"
146            );
147        }
148    }
149
150    #[test]
151    fn test_extract_key_type() {
152        assert_eq!(
153            extract_pem_key_type(
154                "-----BEGIN EC PRIVATE KEY-----\ntest\n-----END EC PRIVATE KEY-----"
155            ),
156            Some("EC")
157        );
158        assert_eq!(
159            extract_pem_key_type(
160                "-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----"
161            ),
162            Some("RSA")
163        );
164        assert_eq!(
165            extract_pem_key_type("-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----"),
166            Some("")
167        );
168        assert_eq!(extract_pem_key_type("not a pem"), None);
169    }
170}