1use rcgen::{
2 BasicConstraints, CertificateParams, DistinguishedName, DnType, IsCa, KeyPair, KeyUsagePurpose,
3};
4
5use crate::ca_trust;
6use crate::{CaError, MitmError};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct CertificateAuthority {
13 pub cert_pem: Vec<u8>,
14 pub(crate) key_pem: Vec<u8>,
15 pub fingerprint: String,
16}
17
18impl CertificateAuthority {
19 pub fn key_pem(&self) -> &[u8] {
24 &self.key_pem
25 }
26}
27
28pub fn generate_ca() -> Result<CertificateAuthority, CaError> {
30 let key = KeyPair::generate().map_err(|error| CaError::InvalidMaterial(error.to_string()))?;
31 let mut params = CertificateParams::default();
32 params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
33 params.key_usages = vec![
34 KeyUsagePurpose::DigitalSignature,
35 KeyUsagePurpose::KeyCertSign,
36 KeyUsagePurpose::CrlSign,
37 ];
38
39 let mut dn = DistinguishedName::new();
40 dn.push(DnType::CommonName, "soth-mitm Local CA");
41 dn.push(DnType::OrganizationName, "soth-mitm");
42 params.distinguished_name = dn;
43
44 let cert = params
45 .self_signed(&key)
46 .map_err(|error| CaError::InvalidMaterial(error.to_string()))?;
47
48 let cert_pem = cert.pem().into_bytes();
49 let key_pem = key.serialize_pem().into_bytes();
50
51 Ok(CertificateAuthority {
52 fingerprint: fingerprint_from_pem(&cert_pem),
53 cert_pem,
54 key_pem,
55 })
56}
57
58pub fn load_ca(cert: &[u8], key: &[u8]) -> Result<CertificateAuthority, CaError> {
60 if cert.is_empty() {
61 return Err(CaError::InvalidMaterial(
62 "certificate PEM must not be empty".to_string(),
63 ));
64 }
65 if key.is_empty() {
66 return Err(CaError::InvalidMaterial(
67 "private key PEM must not be empty".to_string(),
68 ));
69 }
70
71 Ok(CertificateAuthority {
72 cert_pem: cert.to_vec(),
73 key_pem: key.to_vec(),
74 fingerprint: fingerprint_from_pem(cert),
75 })
76}
77
78pub fn load_ca_from_files(
80 cert_path: impl AsRef<std::path::Path>,
81 key_path: impl AsRef<std::path::Path>,
82) -> Result<CertificateAuthority, CaError> {
83 let cert = std::fs::read(cert_path.as_ref()).map_err(|error| {
84 if error.kind() == std::io::ErrorKind::PermissionDenied {
85 return CaError::PermissionDenied {
86 operation: "read_ca_cert".to_string(),
87 detail: error.to_string(),
88 };
89 }
90 CaError::Io(error)
91 })?;
92
93 let key = std::fs::read(key_path.as_ref()).map_err(|error| {
94 if error.kind() == std::io::ErrorKind::PermissionDenied {
95 return CaError::PermissionDenied {
96 operation: "read_ca_key".to_string(),
97 detail: error.to_string(),
98 };
99 }
100 CaError::Io(error)
101 })?;
102
103 load_ca(&cert, &key)
104}
105
106pub fn install_ca_system_trust(_ca: &CertificateAuthority) -> Result<(), CaError> {
108 ca_trust::install(_ca)
109}
110
111pub fn uninstall_ca_system_trust() -> Result<(), CaError> {
113 ca_trust::uninstall()
114}
115
116pub fn is_ca_trusted(_fingerprint: &str) -> Result<bool, CaError> {
118 ca_trust::is_trusted(_fingerprint)
119}
120
121#[allow(dead_code)]
122pub(crate) fn map_ca_error_to_mitm_error(error: CaError) -> MitmError {
123 match error {
124 CaError::PermissionDenied { operation, detail } => {
125 MitmError::CaLoadFailed(format!("permission denied for {operation}: {detail}"))
126 }
127 CaError::OperationFailed(detail) => MitmError::CaOperationFailed(detail),
128 CaError::InvalidMaterial(detail) => MitmError::CaLoadFailed(detail),
129 CaError::UnsupportedOperation(detail) => MitmError::CaOperationFailed(detail),
130 CaError::Io(error) => MitmError::CaLoadFailed(error.to_string()),
131 }
132}
133
134#[allow(dead_code)]
135pub(crate) fn load_ca_for_startup(
136 cert: &[u8],
137 key: &[u8],
138) -> Result<CertificateAuthority, MitmError> {
139 load_ca(cert, key).map_err(map_ca_error_to_mitm_error)
140}
141
142fn fingerprint_from_pem(cert: &[u8]) -> String {
143 let mut rendered = String::with_capacity(2 * cert.len().min(16) + 4);
144 for byte in cert.iter().take(16) {
145 rendered.push(hex_digit(byte >> 4));
146 rendered.push(hex_digit(byte & 0x0f));
147 }
148 rendered.push(':');
149 rendered.push_str(&cert.len().to_string());
150 rendered
151}
152
153fn hex_digit(value: u8) -> char {
154 match value {
155 0..=9 => (b'0' + value) as char,
156 10..=15 => (b'a' + (value - 10)) as char,
157 _ => '0',
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::{generate_ca, load_ca, load_ca_for_startup, map_ca_error_to_mitm_error};
164 use crate::{CaError, MitmError};
165
166 #[test]
167 fn ca_generate_load_api_contract() {
168 let generated = generate_ca().expect("generate ca");
169 let loaded = load_ca(&generated.cert_pem, &generated.key_pem).expect("load generated ca");
170 assert!(!loaded.fingerprint.is_empty());
171 assert_eq!(loaded.cert_pem, generated.cert_pem);
172 assert_eq!(loaded.key_pem, generated.key_pem);
173 }
174
175 #[test]
176 fn ca_permission_denied_error_mapping() {
177 let mapped = map_ca_error_to_mitm_error(CaError::PermissionDenied {
178 operation: "read_ca_cert".to_string(),
179 detail: "os error 13".to_string(),
180 });
181 match mapped {
182 MitmError::CaLoadFailed(detail) => {
183 assert!(detail.contains("permission denied"));
184 assert!(detail.contains("read_ca_cert"));
185 }
186 other => panic!("unexpected mapped error: {other}"),
187 }
188 }
189
190 #[test]
191 fn startup_fails_with_ca_load_failed_when_ca_invalid() {
192 let error = load_ca_for_startup(b"", b"key").expect_err("invalid ca should fail");
193 match error {
194 MitmError::CaLoadFailed(detail) => {
195 assert!(detail.contains("certificate PEM must not be empty"));
196 }
197 other => panic!("unexpected startup error: {other}"),
198 }
199 }
200}