qualia_client_core/
webizen_x509.rs1use sha2::{Digest, Sha256};
7use x509_parser::prelude::*;
8
9use crate::webizen_trust::TrustStore;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct PathVerifyResult {
14 pub accepted: bool,
15 pub reason_code: &'static str,
16 pub detail: String,
17}
18
19impl PathVerifyResult {
20 pub fn accept(reason: &'static str, detail: impl Into<String>) -> Self {
21 Self {
22 accepted: true,
23 reason_code: reason,
24 detail: detail.into(),
25 }
26 }
27 pub fn reject(reason: &'static str, detail: impl Into<String>) -> Self {
28 Self {
29 accepted: false,
30 reason_code: reason,
31 detail: detail.into(),
32 }
33 }
34}
35
36pub fn pem_to_ders(pem: &str) -> Result<Vec<Vec<u8>>, String> {
38 let mut out = Vec::new();
39 let mut rest = pem.as_bytes();
40 loop {
41 match rustls_pemfile::read_one_from_slice(rest) {
42 Ok(Some((item, rem))) => {
43 rest = rem;
44 match item {
45 rustls_pemfile::Item::X509Certificate(der) => out.push(der.to_vec()),
46 _ => {}
47 }
48 }
49 Ok(None) => break,
50 Err(e) => {
51 if out.is_empty() {
52 return Err(format!("PEM parse: {e:?}"));
53 }
54 break;
55 }
56 }
57 }
58 if out.is_empty() {
59 return Err("no CERTIFICATE blocks in PEM".into());
60 }
61 Ok(out)
62}
63
64pub fn spki_sha256_hex(cert_der: &[u8]) -> Result<String, String> {
66 let (_, cert) = X509Certificate::from_der(cert_der).map_err(|e| format!("x509 parse: {e}"))?;
67 let spki = cert.public_key().raw;
68 let mut h = Sha256::new();
69 h.update(spki);
70 Ok(hex::encode(h.finalize()))
71}
72
73pub fn spki_pin_matches(leaf_pem_or_der: &str, expected_hex: &str) -> Result<bool, String> {
75 let ders = if leaf_pem_or_der.contains("BEGIN CERTIFICATE") {
76 pem_to_ders(leaf_pem_or_der)?
77 } else {
78 return Err("leaf must be PEM CERTIFICATE".into());
80 };
81 let leaf = ders.first().ok_or("empty leaf")?;
82 let got = spki_sha256_hex(leaf)?;
83 Ok(got.eq_ignore_ascii_case(expected_hex.trim()))
84}
85
86pub fn verify_chain_against_enabled_roots(
94 leaf_pem: &str,
95 intermediate_pems: &[&str],
96 store: &TrustStore,
97) -> PathVerifyResult {
98 let leaf_ders = match pem_to_ders(leaf_pem) {
99 Ok(d) => d,
100 Err(e) => return PathVerifyResult::reject("leaf_pem_invalid", e),
101 };
102 let leaf_der = match leaf_ders.first() {
103 Some(d) => d.as_slice(),
104 None => return PathVerifyResult::reject("leaf_empty", "no leaf certificate"),
105 };
106 let leaf = match X509Certificate::from_der(leaf_der) {
107 Ok((_, c)) => c,
108 Err(e) => return PathVerifyResult::reject("leaf_parse", format!("{e}")),
109 };
110
111 let mut intermediate_ders: Vec<Vec<u8>> = Vec::new();
112 for p in intermediate_pems {
113 if p.trim().is_empty() {
114 continue;
115 }
116 match pem_to_ders(p) {
117 Ok(ders) => intermediate_ders.extend(ders),
118 Err(e) => {
119 return PathVerifyResult::reject("intermediate_pem_invalid", e);
120 }
121 }
122 }
123
124 let mut roots: Vec<(String, Vec<u8>)> = Vec::new();
125 for a in store
126 .anchors
127 .iter()
128 .filter(|a| a.enabled && a.kind == crate::webizen_trust::AnchorKind::PemRoot)
129 {
130 if let Ok(ders) = pem_to_ders(&a.material) {
131 for d in ders {
132 if X509Certificate::from_der(&d).is_ok() {
133 roots.push((a.id.clone(), d));
134 }
135 }
136 }
137 }
138 if roots.is_empty() {
139 return PathVerifyResult::reject(
140 "no_enabled_roots",
141 "no enabled PEM roots in trust store — chain verify fails closed",
142 );
143 }
144
145 for (id, root_der) in &roots {
147 let Ok((_, root)) = X509Certificate::from_der(root_der) else {
148 continue;
149 };
150 if leaf.verify_signature(Some(root.public_key())).is_ok() {
151 return PathVerifyResult::accept(
152 "leaf_signed_by_enabled_root",
153 format!("verified against root {id}"),
154 );
155 }
156 if leaf.tbs_certificate.subject == root.tbs_certificate.subject
157 && leaf.tbs_certificate.subject == leaf.tbs_certificate.issuer
158 && leaf.verify_signature(None).is_ok()
159 {
160 return PathVerifyResult::accept(
161 "self_signed_matches_enabled_root",
162 format!("self-signed leaf matches enabled root {id}"),
163 );
164 }
165 }
166
167 for inter_der in &intermediate_ders {
169 let Ok((_, inter)) = X509Certificate::from_der(inter_der) else {
170 continue;
171 };
172 if leaf.verify_signature(Some(inter.public_key())).is_err() {
173 continue;
174 }
175 for (id_r, root_der) in &roots {
176 let Ok((_, root)) = X509Certificate::from_der(root_der) else {
177 continue;
178 };
179 if inter.verify_signature(Some(root.public_key())).is_ok() {
180 return PathVerifyResult::accept(
181 "leaf_via_intermediate_to_enabled_root",
182 format!("chain to root {id_r}"),
183 );
184 }
185 }
186 }
187
188 PathVerifyResult::reject(
189 "chain_not_anchored",
190 "leaf/intermediates did not verify to any enabled PEM root",
191 )
192}
193
194pub fn root_cert_store_from_trust(store: &TrustStore) -> Result<rustls::RootCertStore, String> {
196 let mut roots = rustls::RootCertStore::empty();
197 let mut n = 0usize;
198 for a in store
199 .anchors
200 .iter()
201 .filter(|a| a.enabled && a.kind == crate::webizen_trust::AnchorKind::PemRoot)
202 {
203 let ders = pem_to_ders(&a.material)?;
204 for der in ders {
205 let cert = rustls::pki_types::CertificateDer::from(der);
206 roots
207 .add(cert)
208 .map_err(|e| format!("add root to RootCertStore: {e}"))?;
209 n += 1;
210 }
211 }
212 if n == 0 {
213 return Err("no enabled PEM roots to build RootCertStore".into());
214 }
215 Ok(roots)
216}
217
218#[derive(Debug, Clone, PartialEq, Eq)]
220pub enum AgentTlsMode {
221 CustomRootsOnly { n_roots: usize },
223 SystemDefault,
225}
226
227pub fn agent_tls_mode(store: &TrustStore) -> AgentTlsMode {
228 let n = store
229 .anchors
230 .iter()
231 .filter(|a| a.enabled && a.kind == crate::webizen_trust::AnchorKind::PemRoot)
232 .count();
233 if n > 0 {
234 AgentTlsMode::CustomRootsOnly { n_roots: n }
235 } else {
236 AgentTlsMode::SystemDefault
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use crate::webizen_trust::{AnchorKind, TrustAnchor, TrustStore};
244
245 #[test]
247 fn empty_roots_fail_closed() {
248 let store = TrustStore::new();
249 let r = verify_chain_against_enabled_roots(
250 "-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n",
251 &[],
252 &store,
253 );
254 assert!(!r.accepted);
255 assert!(
257 r.reason_code == "no_enabled_roots"
258 || r.reason_code == "leaf_parse"
259 || r.reason_code == "leaf_pem_invalid"
260 );
261 }
262
263 #[test]
264 fn agent_mode_system_when_no_pem() {
265 assert_eq!(
266 agent_tls_mode(&TrustStore::new()),
267 AgentTlsMode::SystemDefault
268 );
269 }
270
271 #[test]
272 fn agent_mode_custom_when_pem_enabled() {
273 let mut s = TrustStore::new();
274 s.anchors.push(TrustAnchor {
275 id: "pem:t".into(),
276 label: "t".into(),
277 kind: AnchorKind::PemRoot,
278 material: "-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n".into(),
279 enabled: true,
280 notes: "".into(),
281 added_unix: 1,
282 });
283 match agent_tls_mode(&s) {
284 AgentTlsMode::CustomRootsOnly { n_roots } => assert_eq!(n_roots, 1),
285 _ => panic!("expected custom"),
286 }
287 }
288}