dig_tls/ca.rs
1//! The shipped, PUBLIC DigNetwork Certificate Authority.
2//!
3//! dig-tls mirrors Chia's TLS model: it ships a single, well-known CA whose certificate AND private
4//! key are BOTH public and compiled into the crate (exactly as `chia-blockchain` ships the
5//! `chia_ca.crt` and `chia_ca.key` pair). **The CA private key is intentionally NOT a secret** — it
6//! is a shared
7//! trust-domain marker, not a secret gate. Anyone can mint a leaf that chains to the DigNetwork CA;
8//! that is by design. Real authentication of a peer comes from the application layer — the
9//! `peer_id = SHA-256(SPKI)` pin ([`crate::identity`]) and the #1204 BLS-G1 cert binding
10//! ([`crate::binding`]) — never from CA-key secrecy. Because the CA key is public there is no user
11//! step, no custody gate, and no runtime PKI service.
12//!
13//! Every DIG peer generates its own leaf certificate signed by this CA at first run
14//! ([`crate::node_cert`]); every DIG peer trusts leaves that chain to this CA. The CA material is
15//! byte-identical across the whole ecosystem (recorded in the `canonical` skill), so any two DIG
16//! peers share the same trust anchor.
17
18use std::sync::OnceLock;
19
20use rcgen::{
21 BasicConstraints, CertificateParams, DistinguishedName, DnType, IsCa, KeyPair, KeyUsagePurpose,
22 PKCS_ECDSA_P256_SHA256,
23};
24use rustls_pki_types::CertificateDer;
25use time::{Duration, OffsetDateTime};
26
27use crate::error::{DigTlsError, Result};
28
29/// The shipped DigNetwork CA certificate, PEM-encoded (public — the trust anchor every peer uses).
30pub const DIG_CA_CERT_PEM: &str = include_str!("ca/dig_ca.crt");
31
32/// The shipped DigNetwork CA private key, PEM-encoded. **Intentionally public** (Chia precedent):
33/// the CA key is a shared trust-domain marker, not a secret. A peer signs its own leaf with it.
34pub const DIG_CA_KEY_PEM: &str = include_str!("ca/dig_ca.key");
35
36/// The organization name on the DigNetwork CA, so a trust-store listing is self-identifying.
37pub const CA_ORGANIZATION: &str = "DIG Network";
38
39/// The DigNetwork CA CommonName.
40pub const CA_COMMON_NAME: &str = "DIG Network CA";
41
42/// CA validity window: ~100 years. The public CA is a fixed, long-lived trust anchor; it is rotated
43/// only by an explicit, ecosystem-wide protocol event, never on a timer.
44pub const CA_LIFETIME: Duration = Duration::days(365 * 100);
45
46/// Backdate `not_before` by an hour so a peer with a slightly slow clock never rejects the anchor as
47/// "not yet valid".
48pub(crate) const CLOCK_SKEW_BACKDATE: Duration = Duration::hours(1);
49
50/// A generated CA: the self-signed certificate and its private key, both PEM-encoded.
51#[derive(Clone)]
52pub struct CaMaterial {
53 /// The self-signed CA certificate, PEM-encoded.
54 pub cert_pem: String,
55 /// The CA private key, PKCS#8 PEM-encoded (public by design — see the module docs).
56 pub key_pem: String,
57}
58
59/// Mint a fresh DigNetwork CA (used ONCE to produce the shipped [`DIG_CA_CERT_PEM`]/[`DIG_CA_KEY_PEM`]
60/// via `examples/generate_ca.rs`, and by tests). ECDSA P-256; 100-year validity; `CA:TRUE`
61/// path-length 0 (it signs only end-entity leaves); `keyCertSign` + `cRLSign`. No name constraints:
62/// DIG peer leaves carry no meaningful hostname (peers dial by IP and authenticate by peer_id + BLS
63/// binding), so the CA's namespace is intentionally unconstrained.
64pub fn generate_dig_ca(now: OffsetDateTime) -> Result<CaMaterial> {
65 let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)
66 .map_err(|e| DigTlsError::CertGen(format!("generate CA key: {e}")))?;
67
68 let mut params = CertificateParams::new(Vec::<String>::new())
69 .map_err(|e| DigTlsError::CertGen(format!("CA params: {e}")))?;
70 params.not_before = now - CLOCK_SKEW_BACKDATE;
71 params.not_after = now + CA_LIFETIME;
72
73 let mut dn = DistinguishedName::new();
74 dn.push(DnType::CommonName, CA_COMMON_NAME);
75 dn.push(DnType::OrganizationName, CA_ORGANIZATION);
76 params.distinguished_name = dn;
77
78 params.is_ca = IsCa::Ca(BasicConstraints::Constrained(0));
79 params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign];
80 params.use_authority_key_identifier_extension = true;
81
82 let cert = params
83 .self_signed(&key)
84 .map_err(|e| DigTlsError::CertGen(format!("self-sign CA: {e}")))?;
85
86 Ok(CaMaterial {
87 cert_pem: cert.pem(),
88 key_pem: key.serialize_pem(),
89 })
90}
91
92/// The DigNetwork CA loaded from PEM, ready to sign peer leaves.
93///
94/// Reconstructs the issuer handle (distinguished name + key) so leaf issuance needs only the PEM
95/// material. [`DigCa::embedded`] loads the shipped public CA — the common path.
96pub struct DigCa {
97 pub(crate) cert: rcgen::Certificate,
98 pub(crate) key: KeyPair,
99}
100
101impl DigCa {
102 /// Load the shipped, public DigNetwork CA (the trust anchor every DIG peer shares).
103 pub fn embedded() -> Result<Self> {
104 Self::from_pem(DIG_CA_CERT_PEM, DIG_CA_KEY_PEM)
105 }
106
107 /// Load a CA from its PEM certificate + key (used by tests with a throwaway CA).
108 pub fn from_pem(cert_pem: &str, key_pem: &str) -> Result<Self> {
109 let key = KeyPair::from_pem(key_pem)
110 .map_err(|e| DigTlsError::Ca(format!("parse CA key: {e}")))?;
111 // Rematerialize the issuer handle from the stored cert; the persisted PEM is unchanged and
112 // remains the trusted anchor.
113 let params = CertificateParams::from_ca_cert_pem(cert_pem)
114 .map_err(|e| DigTlsError::Ca(format!("parse CA cert: {e}")))?;
115 let cert = params
116 .self_signed(&key)
117 .map_err(|e| DigTlsError::Ca(format!("rematerialize CA issuer: {e}")))?;
118 Ok(Self { cert, key })
119 }
120}
121
122/// The shipped DigNetwork CA certificate in DER form, parsed once and cached — the trust anchor the
123/// rustls verifiers ([`crate::verify`]) chain peer leaves to.
124pub fn embedded_ca_cert_der() -> Result<CertificateDer<'static>> {
125 static DER: OnceLock<Vec<u8>> = OnceLock::new();
126 let der = DER.get_or_init(|| {
127 // The shipped PEM is generated by this crate's own `generate_dig_ca`, so it always parses;
128 // an empty placeholder (before the CA is minted) yields an empty Vec that webpki rejects
129 // cleanly at verify time rather than panicking here.
130 rustls_pemfile::certs(&mut DIG_CA_CERT_PEM.as_bytes())
131 .next()
132 .and_then(|r| r.ok())
133 .map(|c| c.to_vec())
134 .unwrap_or_default()
135 });
136 if der.is_empty() {
137 return Err(DigTlsError::Ca(
138 "embedded DigNetwork CA certificate is missing or unparseable".into(),
139 ));
140 }
141 Ok(CertificateDer::from(der.clone()))
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn generated_ca_round_trips_through_pem() {
150 let ca = generate_dig_ca(OffsetDateTime::now_utc()).expect("mint CA");
151 assert!(ca.cert_pem.contains("BEGIN CERTIFICATE"));
152 assert!(ca.key_pem.contains("PRIVATE KEY"));
153 // Loading it back succeeds — the issuer handle rematerializes.
154 DigCa::from_pem(&ca.cert_pem, &ca.key_pem).expect("load generated CA");
155 }
156
157 #[test]
158 fn embedded_ca_loads_and_parses_to_der() {
159 DigCa::embedded().expect("the shipped CA loads");
160 let der = embedded_ca_cert_der().expect("shipped CA parses to DER");
161 assert!(!der.as_ref().is_empty());
162 }
163}