dwara_core/security/
cert_pinning.rs1use std::sync::Arc;
26
27use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
28use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
29use sha2::{Digest, Sha256};
30
31use crate::config::{CertPinningConfig, Upstream};
32
33#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct CertPin {
40 pub spki_sha256: String,
42}
43
44impl CertPin {
45 fn parse_hash(&self) -> Option<[u8; 32]> {
48 let bytes = hex_decode(&self.spki_sha256)?;
49 if bytes.len() == 32 {
50 let mut arr = [0u8; 32];
51 arr.copy_from_slice(&bytes);
52 Some(arr)
53 } else {
54 None
55 }
56 }
57}
58
59fn hex_decode(s: &str) -> Option<Vec<u8>> {
62 if !s.len().is_multiple_of(2) {
63 return None;
64 }
65 let mut out = Vec::with_capacity(s.len() / 2);
66 let bytes = s.as_bytes();
67 for chunk in bytes.chunks_exact(2) {
68 let hi = hex_nibble(chunk[0])?;
69 let lo = hex_nibble(chunk[1])?;
70 out.push((hi << 4) | lo);
71 }
72 Some(out)
73}
74
75fn hex_nibble(b: u8) -> Option<u8> {
76 match b {
77 b'0'..=b'9' => Some(b - b'0'),
78 b'a'..=b'f' => Some(b - b'a' + 10),
79 _ => None,
80 }
81}
82
83#[derive(Debug, Clone)]
89pub struct CertPinVerifier {
90 pins: Vec<[u8; 32]>,
92}
93
94impl CertPinVerifier {
95 pub fn from_upstream(upstream: &Upstream) -> Option<Self> {
99 let cfg: &CertPinningConfig = upstream.cert_pinning.as_ref()?;
100 if cfg.pins.is_empty() {
101 return None;
102 }
103 let mut pins = Vec::with_capacity(cfg.pins.len());
104 for p in &cfg.pins {
105 let cert_pin = CertPin {
107 spki_sha256: p.spki_sha256.clone(),
108 };
109 match cert_pin.parse_hash() {
110 Some(hash) => pins.push(hash),
111 None => {
112 tracing::warn!(
113 upstream = %upstream.name,
114 pin = %p.spki_sha256,
115 "cert_pinning: ignoring pin with invalid hex (expected 64 lowercase hex \
116 chars for SHA-256)"
117 );
118 }
119 }
120 }
121 if pins.is_empty() {
122 None
123 } else {
124 Some(Self { pins })
125 }
126 }
127
128 pub fn from_hashes(pins: Vec<[u8; 32]>) -> Self {
130 Self { pins }
131 }
132
133 pub fn verify_cert(&self, cert: &CertificateDer<'_>) -> Result<(), CertPinError> {
137 let spki =
138 crate::security::tls::spki_of_leaf(cert).ok_or(CertPinError::SpkiExtractionFailed)?;
139 let mut hasher = Sha256::new();
140 hasher.update(&spki);
141 let hash = hasher.finalize();
142 for pin in &self.pins {
143 if hash.as_slice() == pin.as_slice() {
144 return Ok(());
145 }
146 }
147 Err(CertPinError::NoMatch)
148 }
149
150 pub fn pin_count(&self) -> usize {
152 self.pins.len()
153 }
154}
155
156impl ServerCertVerifier for CertPinVerifier {
157 fn verify_server_cert(
158 &self,
159 end_entity: &CertificateDer<'_>,
160 _intermediates: &[CertificateDer<'_>],
161 _server_name: &ServerName<'_>,
162 _ocsp_response: &[u8],
163 _now: UnixTime,
164 ) -> Result<ServerCertVerified, rustls::Error> {
165 self.verify_cert(end_entity)
166 .map(|_| ServerCertVerified::assertion())
167 .map_err(|e| rustls::Error::General(e.to_string()))
168 }
169
170 fn verify_tls12_signature(
171 &self,
172 _message: &[u8],
173 _cert: &CertificateDer<'_>,
174 _dss: &rustls::DigitallySignedStruct,
175 ) -> Result<HandshakeSignatureValid, rustls::Error> {
176 Ok(HandshakeSignatureValid::assertion())
182 }
183
184 fn verify_tls13_signature(
185 &self,
186 _message: &[u8],
187 _cert: &CertificateDer<'_>,
188 _dss: &rustls::DigitallySignedStruct,
189 ) -> Result<HandshakeSignatureValid, rustls::Error> {
190 Ok(HandshakeSignatureValid::assertion())
191 }
192
193 fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
194 vec![
196 rustls::SignatureScheme::RSA_PKCS1_SHA256,
197 rustls::SignatureScheme::RSA_PKCS1_SHA384,
198 rustls::SignatureScheme::RSA_PKCS1_SHA512,
199 rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
200 rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
201 rustls::SignatureScheme::ECDSA_NISTP521_SHA512,
202 rustls::SignatureScheme::RSA_PSS_SHA256,
203 rustls::SignatureScheme::RSA_PSS_SHA384,
204 rustls::SignatureScheme::RSA_PSS_SHA512,
205 rustls::SignatureScheme::ED25519,
206 rustls::SignatureScheme::ED448,
207 ]
208 }
209}
210
211pub fn client_config_with_pinning(
218 verifier: Arc<CertPinVerifier>,
219 alpn: &[u8],
220) -> rustls::ClientConfig {
221 let mut cfg = rustls::ClientConfig::builder()
222 .dangerous()
223 .with_custom_certificate_verifier(verifier)
224 .with_no_client_auth();
225 cfg.alpn_protocols = vec![alpn.to_vec()];
226 cfg
227}
228
229#[derive(Debug, Clone, PartialEq, Eq)]
231pub enum CertPinError {
232 NoMatch,
234 InvalidCertificate,
236 SpkiExtractionFailed,
238}
239
240impl std::fmt::Display for CertPinError {
241 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242 match self {
243 CertPinError::NoMatch => {
244 write!(f, "certificate SPKI hash does not match any configured pin")
245 }
246 CertPinError::InvalidCertificate => write!(f, "peer certificate is malformed"),
247 CertPinError::SpkiExtractionFailed => {
248 write!(f, "failed to extract SPKI from peer certificate")
249 }
250 }
251 }
252}
253
254impl std::error::Error for CertPinError {}
255
256#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn hex_decode_valid() {
264 assert_eq!(hex_decode("deadbeef"), Some(vec![0xde, 0xad, 0xbe, 0xef]));
265 }
266
267 #[test]
268 fn hex_decode_uppercase_rejected() {
269 assert_eq!(hex_decode("DeadBeef"), None);
270 }
271
272 #[test]
273 fn hex_decode_odd_length() {
274 assert_eq!(hex_decode("abc"), None);
275 }
276
277 #[test]
278 fn cert_pin_parse_hash_valid() {
279 let pin = CertPin {
280 spki_sha256: "ab".repeat(32),
281 };
282 assert!(pin.parse_hash().is_some());
283 }
284
285 #[test]
286 fn cert_pin_parse_hash_wrong_length() {
287 let pin = CertPin {
288 spki_sha256: "ab".repeat(31),
289 };
290 assert!(pin.parse_hash().is_none());
291 }
292
293 #[test]
294 fn verifier_pin_count() {
295 let v = CertPinVerifier::from_hashes(vec![[0; 32], [1; 32]]);
296 assert_eq!(v.pin_count(), 2);
297 }
298
299 #[test]
300 fn verifier_rejects_empty_pins() {
301 let v = CertPinVerifier::from_hashes(vec![]);
302 assert_eq!(v.pin_count(), 0);
303 }
304}