1use std::path::{Path, PathBuf};
8
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11use tracing::{debug, info, warn};
12
13#[derive(Debug, Error)]
15pub enum CaInitError {
16 #[error("failed to read CA certificate from {path}: {source}")]
18 CertRead {
19 path: String,
20 source: std::io::Error,
21 },
22
23 #[error("no certificates found in {path}")]
25 NoCertificates { path: String },
26
27 #[error("CA certificate is missing Basic Constraints CA:TRUE")]
29 NotCaCertificate,
30
31 #[error("CA certificate Key Usage does not include keyCertSign")]
33 MissingKeyCertSign,
34
35 #[error("failed to load CA private key from {path}: {reason}")]
37 KeyLoad { path: String, reason: String },
38
39 #[error("PKCS#11 URI detected for CA key: {uri}")]
41 Pkcs11Uri { uri: String },
42
43 #[error("CA init error: {0}")]
45 Config(String),
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct CaConfig {
51 pub id: String,
53 pub cert_chain_path: PathBuf,
55 pub key_path: PathBuf,
57 pub hsm: bool,
59 pub priority: u32,
61 pub weight: u32,
63 pub endpoint: Option<String>,
65}
66
67pub struct CaInstance {
69 pub config: CaConfig,
71 pub cert_chain: Vec<Vec<u8>>,
73 pub hsm_backed: bool,
75 }
79
80impl CaInstance {
81 pub fn from_config(config: &CaConfig) -> Result<Self, CaInitError> {
86 let cert_chain = load_cert_chain(&config.cert_chain_path)?;
87 validate_ca_certificate(&cert_chain)?;
88
89 if config.hsm || is_pkcs11_uri(&config.key_path)? {
91 info!(
92 ca_id = %config.id,
93 "CA key is HSM-backed (PKCS#11); deferring to kipuka-hsm"
94 );
95 return Ok(Self {
96 config: config.clone(),
97 cert_chain,
98 hsm_backed: true,
99 });
100 }
101
102 if !config.key_path.exists() {
104 return Err(CaInitError::KeyLoad {
105 path: config.key_path.display().to_string(),
106 reason: "file does not exist".into(),
107 });
108 }
109
110 info!(
111 ca_id = %config.id,
112 cert_count = cert_chain.len(),
113 hsm = false,
114 "CA instance initialized"
115 );
116
117 Ok(Self {
118 config: config.clone(),
119 cert_chain,
120 hsm_backed: false,
121 })
122 }
123
124 pub fn id(&self) -> &str {
126 &self.config.id
127 }
128}
129
130fn load_cert_chain(path: &Path) -> Result<Vec<Vec<u8>>, CaInitError> {
132 let pem_data = std::fs::read(path).map_err(|e| CaInitError::CertRead {
133 path: path.display().to_string(),
134 source: e,
135 })?;
136
137 let certs: Vec<Vec<u8>> = rustls_pemfile::certs(&mut &pem_data[..])
138 .filter_map(|r| r.ok())
139 .map(|c| c.to_vec())
140 .collect();
141
142 if certs.is_empty() {
143 return Err(CaInitError::NoCertificates {
144 path: path.display().to_string(),
145 });
146 }
147
148 debug!(
149 path = %path.display(),
150 count = certs.len(),
151 "loaded CA certificate chain"
152 );
153
154 Ok(certs)
155}
156
157fn validate_ca_certificate(chain: &[Vec<u8>]) -> Result<(), CaInitError> {
167 let ca_cert_der = chain.first().ok_or(CaInitError::NotCaCertificate)?;
168
169 let bc_oid = [0x55, 0x1D, 0x13];
171 if !contains_subsequence(ca_cert_der, &bc_oid) {
172 warn!("CA certificate may be missing Basic Constraints extension");
173 }
176
177 let ku_oid = [0x55, 0x1D, 0x0F];
179 if !contains_subsequence(ca_cert_der, &ku_oid) {
180 warn!("CA certificate may be missing Key Usage extension");
181 }
182
183 debug!("CA certificate validation passed (basic OID check)");
184 Ok(())
185}
186
187fn is_pkcs11_uri(path: &Path) -> Result<bool, CaInitError> {
189 if let Some(s) = path.to_str()
190 && s.starts_with("pkcs11:")
191 {
192 return Ok(true);
193 }
194
195 if path.exists()
197 && let Ok(contents) = std::fs::read_to_string(path)
198 && contents.trim().starts_with("pkcs11:")
199 {
200 return Ok(true);
201 }
202
203 Ok(false)
204}
205
206fn contains_subsequence(haystack: &[u8], needle: &[u8]) -> bool {
208 haystack
209 .windows(needle.len())
210 .any(|window| window == needle)
211}