1use std::fs;
13use std::path::Path;
14
15use rcgen::{
16 CertificateParams, DistinguishedName, DnType, ExtendedKeyUsagePurpose, Ia5String, KeyPair,
17 KeyUsagePurpose, SanType, PKCS_ECDSA_P256_SHA256,
18};
19use rustls_pki_types::{CertificateDer, PrivateKeyDer};
20use time::{Duration, OffsetDateTime};
21use zeroize::Zeroizing;
22
23use crate::binding::attach_binding;
24use crate::bls::SecretKey;
25use crate::ca::{DigCa, CLOCK_SKEW_BACKDATE};
26use crate::error::{DigTlsError, Result};
27use crate::identity::{peer_id_from_tls_spki_der, PeerId};
28
29pub const LEAF_LIFETIME: Duration = Duration::days(365 * 10);
33
34const LEAF_SAN: &str = "peer.dig";
38
39const CERT_FILE: &str = "node.crt";
41const KEY_FILE: &str = "node.key";
42
43pub struct NodeCert {
49 cert_pem: String,
50 key_pem: Zeroizing<String>,
51 cert_der: Vec<u8>,
52 key_der: Zeroizing<Vec<u8>>,
53 spki_der: Vec<u8>,
54 peer_id: PeerId,
55}
56
57impl std::fmt::Debug for NodeCert {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 f.debug_struct("NodeCert")
61 .field("peer_id", &self.peer_id)
62 .field("key_pem", &"<redacted>")
63 .finish()
64 }
65}
66
67impl NodeCert {
68 pub fn generate_signed(bls_sk: &SecretKey) -> Result<Self> {
70 Self::generate_signed_by(&DigCa::embedded()?, bls_sk, OffsetDateTime::now_utc())
71 }
72
73 pub fn generate_signed_by(ca: &DigCa, bls_sk: &SecretKey, now: OffsetDateTime) -> Result<Self> {
76 let leaf_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)
77 .map_err(|e| DigTlsError::CertGen(format!("generate leaf key: {e}")))?;
78
79 let mut params = CertificateParams::new(Vec::<String>::new())
80 .map_err(|e| DigTlsError::CertGen(format!("leaf params: {e}")))?;
81 params.not_before = now - CLOCK_SKEW_BACKDATE;
82 params.not_after = now + LEAF_LIFETIME;
83
84 let mut dn = DistinguishedName::new();
85 dn.push(DnType::CommonName, LEAF_SAN);
86 params.distinguished_name = dn;
87
88 let san = Ia5String::try_from(LEAF_SAN.to_string())
89 .map_err(|e| DigTlsError::CertGen(format!("leaf SAN: {e}")))?;
90 params.subject_alt_names = vec![SanType::DnsName(san)];
91 params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
92 params.extended_key_usages = vec![
94 ExtendedKeyUsagePurpose::ServerAuth,
95 ExtendedKeyUsagePurpose::ClientAuth,
96 ];
97
98 attach_binding(&mut params, &leaf_key, bls_sk);
100
101 let cert = params
102 .signed_by(&leaf_key, &ca.cert, &ca.key)
103 .map_err(|e| DigTlsError::CertGen(format!("sign leaf: {e}")))?;
104
105 Self::from_parts(cert.pem(), leaf_key.serialize_pem(), &leaf_key)
106 }
107
108 pub fn load_or_generate(dir: impl AsRef<Path>, bls_sk: &SecretKey) -> Result<Self> {
111 let dir = dir.as_ref();
112 let cert_path = dir.join(CERT_FILE);
113 let key_path = dir.join(KEY_FILE);
114 if cert_path.exists() && key_path.exists() {
115 let cert_pem = fs::read_to_string(&cert_path)?;
116 let key_pem = fs::read_to_string(&key_path)?;
117 return Self::from_pem(&cert_pem, &key_pem);
118 }
119 let node = Self::generate_signed(bls_sk)?;
120 fs::create_dir_all(dir)?;
121 harden_dir_permissions(dir)?;
122 fs::write(&cert_path, node.cert_pem.as_bytes())?;
123 write_key_file(&key_path, node.key_pem.as_bytes())?;
124 Ok(node)
125 }
126
127 pub fn from_pem(cert_pem: &str, key_pem: &str) -> Result<Self> {
129 let key = KeyPair::from_pem(key_pem)
130 .map_err(|e| DigTlsError::Parse(format!("parse leaf key: {e}")))?;
131 Self::from_parts(cert_pem.to_string(), key_pem.to_string(), &key)
132 }
133
134 fn from_parts(cert_pem: String, key_pem: String, key: &KeyPair) -> Result<Self> {
136 let cert_der = rustls_pemfile::certs(&mut cert_pem.as_bytes())
137 .next()
138 .and_then(|r| r.ok())
139 .ok_or_else(|| DigTlsError::Parse("leaf PEM has no certificate".into()))?
140 .to_vec();
141 let key_der = key.serialize_der();
142 let spki_der = key.public_key_der();
143 let peer_id = peer_id_from_tls_spki_der(&spki_der);
144 Ok(Self {
145 cert_pem,
146 key_pem: Zeroizing::new(key_pem),
147 cert_der,
148 key_der: Zeroizing::new(key_der),
149 spki_der,
150 peer_id,
151 })
152 }
153
154 pub fn peer_id(&self) -> PeerId {
156 self.peer_id
157 }
158
159 pub fn spki_der(&self) -> &[u8] {
161 &self.spki_der
162 }
163
164 pub fn cert_der(&self) -> &[u8] {
166 &self.cert_der
167 }
168
169 pub fn cert_pem(&self) -> &str {
171 &self.cert_pem
172 }
173
174 pub fn key_pem(&self) -> &str {
177 &self.key_pem
178 }
179
180 pub fn rustls_cert_chain(&self) -> Vec<CertificateDer<'static>> {
183 vec![CertificateDer::from(self.cert_der.clone())]
184 }
185
186 pub fn rustls_private_key(&self) -> PrivateKeyDer<'static> {
188 PrivateKeyDer::try_from(self.key_der.to_vec())
189 .expect("a freshly serialized PKCS#8 key is always a valid PrivateKeyDer")
190 }
191}
192
193#[cfg(unix)]
201fn harden_dir_permissions(dir: &Path) -> Result<()> {
202 use std::os::unix::fs::PermissionsExt;
203 fs::set_permissions(dir, fs::Permissions::from_mode(0o700))?;
204 Ok(())
205}
206
207#[cfg(windows)]
210fn harden_dir_permissions(_dir: &Path) -> Result<()> {
211 Ok(())
212}
213
214#[cfg(unix)]
217fn write_key_file(path: &Path, key_pem: &[u8]) -> Result<()> {
218 use std::io::Write;
219 use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
220
221 let mut file = fs::OpenOptions::new()
224 .write(true)
225 .create(true)
226 .truncate(true)
227 .mode(0o600)
228 .open(path)?;
229 file.write_all(key_pem)?;
230 fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
233 Ok(())
234}
235
236#[cfg(windows)]
243fn write_key_file(path: &Path, key_pem: &[u8]) -> Result<()> {
244 fs::write(path, key_pem)?;
245 if let (Some(path_str), Ok(user)) = (path.to_str(), std::env::var("USERNAME")) {
246 let _ = std::process::Command::new("icacls")
248 .args([path_str, "/inheritance:r", "/grant:r", &format!("{user}:F")])
249 .output();
250 }
251 Ok(())
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use crate::binding::{verify_binding_from_leaf_cert, BindingOutcome};
258 use crate::bls::public_key_bytes;
259 use crate::ca::generate_dig_ca;
260 use sha2::{Digest, Sha256};
261
262 fn test_ca() -> DigCa {
263 let m = generate_dig_ca(OffsetDateTime::now_utc()).unwrap();
264 DigCa::from_pem(&m.cert_pem, &m.key_pem).unwrap()
265 }
266
267 fn bls_sk(label: &str) -> SecretKey {
268 let seed: [u8; 32] = Sha256::digest(label.as_bytes()).into();
269 SecretKey::from_seed(&seed)
270 }
271
272 #[test]
273 fn generated_cert_binds_peer_id_to_the_bls_key() {
274 let ca = test_ca();
275 let sk = bls_sk("node-cert/bind");
276 let node = NodeCert::generate_signed_by(&ca, &sk, OffsetDateTime::now_utc()).unwrap();
277
278 let expected: [u8; 32] = Sha256::digest(node.spki_der()).into();
280 assert_eq!(node.peer_id().as_bytes(), &expected);
281
282 match verify_binding_from_leaf_cert(node.cert_der()) {
284 BindingOutcome::Bound { bls_pub } => assert_eq!(bls_pub, public_key_bytes(&sk)),
285 other => panic!("expected Bound, got {other:?}"),
286 }
287 }
288
289 #[test]
290 fn distinct_peers_get_distinct_ids() {
291 let ca = test_ca();
292 let a = NodeCert::generate_signed_by(&ca, &bls_sk("a"), OffsetDateTime::now_utc()).unwrap();
293 let b = NodeCert::generate_signed_by(&ca, &bls_sk("b"), OffsetDateTime::now_utc()).unwrap();
294 assert_ne!(a.peer_id(), b.peer_id());
295 }
296
297 #[test]
298 fn pem_round_trips_preserving_peer_id() {
299 let ca = test_ca();
300 let node =
301 NodeCert::generate_signed_by(&ca, &bls_sk("rt"), OffsetDateTime::now_utc()).unwrap();
302 let restored = NodeCert::from_pem(node.cert_pem(), node.key_pem()).unwrap();
303 assert_eq!(node.peer_id(), restored.peer_id());
304 }
305
306 #[test]
307 fn load_or_generate_is_stable_across_calls() {
308 let dir = tempfile::tempdir().unwrap();
309 let sk = bls_sk("persist");
310 let first = NodeCert::load_or_generate(dir.path(), &sk).unwrap();
311 let second = NodeCert::load_or_generate(dir.path(), &sk).unwrap();
312 assert_eq!(
313 first.peer_id(),
314 second.peer_id(),
315 "a persisted cert is reloaded, not regenerated"
316 );
317 }
318
319 #[test]
323 #[cfg(unix)]
324 fn load_or_generate_persists_the_key_owner_only() {
325 use std::os::unix::fs::PermissionsExt;
326
327 let dir = tempfile::tempdir().unwrap();
328 let sk = bls_sk("perms");
329 NodeCert::load_or_generate(dir.path(), &sk).unwrap();
330
331 let dir_mode = fs::metadata(dir.path()).unwrap().permissions().mode() & 0o777;
332 assert_eq!(dir_mode, 0o700, "cert directory must be owner-only");
333
334 let key_mode = fs::metadata(dir.path().join(KEY_FILE))
335 .unwrap()
336 .permissions()
337 .mode()
338 & 0o777;
339 assert_eq!(key_mode, 0o600, "private key file must be owner-only");
340 }
341
342 #[test]
343 fn debug_never_leaks_the_key() {
344 let ca = test_ca();
345 let node =
346 NodeCert::generate_signed_by(&ca, &bls_sk("dbg"), OffsetDateTime::now_utc()).unwrap();
347 assert!(format!("{node:?}").contains("<redacted>"));
348 }
349}