Skip to main content

dig_tls/
verify.rs

1//! The rustls mutual-auth verifiers — the trust decision every DIG peer connection makes.
2//!
3//! A DIG peer accepts the other side only when THREE checks pass, in order:
4//!
5//! 1. **Chain to the DigNetwork CA.** The presented leaf must be signed by the shipped
6//!    [`crate::ca`] CA (a `webpki` path validation with the correct EKU). This is the trust-domain
7//!    marker — like Chia, the CA is public, so this alone is not authentication.
8//! 2. **peer_id pin.** `peer_id = SHA-256(SPKI DER)` is derived and, when the caller asked to reach a
9//!    specific peer, must equal it. The derived id is always captured so the caller learns who it
10//!    connected to. rustls itself proves the peer holds the leaf's private key (handshake signature),
11//!    so a pinned `peer_id` is a real authentication.
12//! 3. **BLS-G1 binding (#1204).** Under the configured [`BindingPolicy`], the leaf's BLS binding is
13//!    verified and the bound BLS pubkey captured (the seal target). `Required` fails closed on an
14//!    absent or invalid binding (anti-downgrade).
15//!
16//! The chain check deliberately does NOT verify a server name (DIG peers dial by IP and authenticate
17//! by peer_id + binding, not by hostname), so the same verifiers work in both handshake directions.
18
19use std::sync::{Arc, Mutex};
20
21use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
22use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
23use rustls::server::danger::{ClientCertVerified, ClientCertVerifier};
24use rustls::{DigitallySignedStruct, DistinguishedName, Error as TlsError, SignatureScheme};
25use webpki::{anchor_from_trusted_cert, EndEntityCert, KeyUsage};
26
27use crate::binding::{evaluate, verify_binding_from_leaf_cert, BindingPolicy};
28use crate::ca::embedded_ca_cert_der;
29use crate::error::Result;
30use crate::identity::{peer_id_from_leaf_cert_der, PeerId};
31
32/// The `peer_id` a verifier derived from the certificate the peer presented, captured for the caller.
33/// Shared via `Arc<Mutex<_>>` because rustls verifiers are `Sync` and run inside the handshake.
34#[derive(Debug, Default, Clone)]
35pub struct CapturedPeerId(pub Arc<Mutex<Option<PeerId>>>);
36
37impl CapturedPeerId {
38    /// The `peer_id` derived from the peer's certificate, if the handshake reached cert verification.
39    pub fn get(&self) -> Option<PeerId> {
40        *self.0.lock().unwrap()
41    }
42}
43
44/// The peer's verified BLS G1 identity pubkey, captured from the #1204 cert binding when the
45/// handshake carried a valid one. `None` means no valid binding was presented (a legacy peer under
46/// [`BindingPolicy::Opportunistic`], or [`BindingPolicy::Off`]). The sealing layer seals to this key.
47#[derive(Debug, Default, Clone)]
48pub struct CapturedBlsPub(pub Arc<Mutex<Option<[u8; 48]>>>);
49
50impl CapturedBlsPub {
51    /// The verified BLS G1 pubkey the peer's `peer_id` is bound to, if a valid binding was presented.
52    pub fn get(&self) -> Option<[u8; 48]> {
53        *self.0.lock().unwrap()
54    }
55}
56
57/// ECDSA signature algorithms accepted for the CA→leaf chain signature. DIG certs are ECDSA P-256
58/// (both CA and leaf); the P-384 entries are harmless future-proofing.
59const CHAIN_SIG_ALGS: &[&dyn webpki::types::SignatureVerificationAlgorithm] = &[
60    webpki::ring::ECDSA_P256_SHA256,
61    webpki::ring::ECDSA_P256_SHA384,
62    webpki::ring::ECDSA_P384_SHA256,
63    webpki::ring::ECDSA_P384_SHA384,
64];
65
66/// Verify that `end_entity` chains to the shipped DigNetwork CA for `usage`, ignoring any server
67/// name. Returns a rustls [`TlsError`] on any failure so it can be returned straight from a verifier.
68fn verify_chain_to_dig_ca(
69    end_entity: &CertificateDer<'_>,
70    intermediates: &[CertificateDer<'_>],
71    now: UnixTime,
72    usage: KeyUsage,
73) -> std::result::Result<(), TlsError> {
74    let ca_der =
75        embedded_ca_cert_der().map_err(|e| TlsError::General(format!("DigNetwork CA: {e}")))?;
76    let anchor = anchor_from_trusted_cert(&ca_der).map_err(|e| {
77        TlsError::General(format!("DigNetwork CA is not a valid trust anchor: {e}"))
78    })?;
79    let ee = EndEntityCert::try_from(end_entity)
80        .map_err(|e| TlsError::General(format!("peer leaf is not a valid certificate: {e}")))?;
81    ee.verify_for_usage(
82        CHAIN_SIG_ALGS,
83        &[anchor],
84        intermediates,
85        now,
86        usage,
87        None,
88        None,
89    )
90    .map_err(|e| {
91        TlsError::General(format!(
92            "peer leaf does not chain to the DigNetwork CA: {e}"
93        ))
94    })?;
95    Ok(())
96}
97
98/// Derive the peer_id, enforce the pin, and apply the BLS-binding policy — the shared tail of both
99/// the client-side and server-side verifiers. Returns the derived `peer_id` on success.
100fn pin_and_bind(
101    end_entity: &CertificateDer<'_>,
102    expected: Option<PeerId>,
103    captured: &CapturedPeerId,
104    binding_policy: BindingPolicy,
105    captured_bls: &CapturedBlsPub,
106) -> std::result::Result<PeerId, TlsError> {
107    let derived = peer_id_from_leaf_cert_der(end_entity.as_ref()).ok_or_else(|| {
108        TlsError::General("peer leaf certificate could not be parsed as X.509".to_string())
109    })?;
110    // Record who we connected to regardless of the pin outcome.
111    *captured.0.lock().unwrap() = Some(derived);
112    if let Some(expected) = expected {
113        if derived != expected {
114            return Err(TlsError::General(format!(
115                "peer_id mismatch: expected {expected}, got {derived}"
116            )));
117        }
118    }
119    if binding_policy != BindingPolicy::Off {
120        let outcome = verify_binding_from_leaf_cert(end_entity.as_ref());
121        match evaluate(&outcome, binding_policy) {
122            Ok(bls_pub) => *captured_bls.0.lock().unwrap() = bls_pub,
123            Err(reason) => {
124                return Err(TlsError::General(format!(
125                    "peer {derived} rejected by cert BLS binding policy: {reason}"
126                )))
127            }
128        }
129    }
130    Ok(derived)
131}
132
133/// The signature schemes ring's provider supports (for `supported_verify_schemes`).
134fn default_signature_schemes() -> Vec<SignatureScheme> {
135    rustls::crypto::ring::default_provider()
136        .signature_verification_algorithms
137        .supported_schemes()
138}
139
140fn verify_tls12(
141    message: &[u8],
142    cert: &CertificateDer<'_>,
143    dss: &DigitallySignedStruct,
144) -> std::result::Result<HandshakeSignatureValid, TlsError> {
145    rustls::crypto::verify_tls12_signature(
146        message,
147        cert,
148        dss,
149        &rustls::crypto::ring::default_provider().signature_verification_algorithms,
150    )
151}
152
153fn verify_tls13(
154    message: &[u8],
155    cert: &CertificateDer<'_>,
156    dss: &DigitallySignedStruct,
157) -> std::result::Result<HandshakeSignatureValid, TlsError> {
158    rustls::crypto::verify_tls13_signature(
159        message,
160        cert,
161        dss,
162        &rustls::crypto::ring::default_provider().signature_verification_algorithms,
163    )
164}
165
166/// Client-side verifier: verifies the SERVER's leaf chains to the DigNetwork CA (serverAuth), pins
167/// its `peer_id`, and checks the BLS binding.
168#[derive(Debug)]
169pub struct DigServerCertVerifier {
170    expected: Option<PeerId>,
171    captured: CapturedPeerId,
172    binding_policy: BindingPolicy,
173    captured_bls: CapturedBlsPub,
174    schemes: Vec<SignatureScheme>,
175}
176
177impl DigServerCertVerifier {
178    /// Build a verifier that pins `expected` (or accepts any DigNetwork-CA peer when `None`), captures
179    /// the derived id + BLS pubkey, and applies `binding_policy`.
180    pub fn new(
181        expected: Option<PeerId>,
182        captured: CapturedPeerId,
183        binding_policy: BindingPolicy,
184        captured_bls: CapturedBlsPub,
185    ) -> Self {
186        Self {
187            expected,
188            captured,
189            binding_policy,
190            captured_bls,
191            schemes: default_signature_schemes(),
192        }
193    }
194}
195
196impl ServerCertVerifier for DigServerCertVerifier {
197    fn verify_server_cert(
198        &self,
199        end_entity: &CertificateDer<'_>,
200        intermediates: &[CertificateDer<'_>],
201        _server_name: &ServerName<'_>,
202        _ocsp_response: &[u8],
203        now: UnixTime,
204    ) -> std::result::Result<ServerCertVerified, TlsError> {
205        verify_chain_to_dig_ca(end_entity, intermediates, now, KeyUsage::server_auth())?;
206        pin_and_bind(
207            end_entity,
208            self.expected,
209            &self.captured,
210            self.binding_policy,
211            &self.captured_bls,
212        )?;
213        Ok(ServerCertVerified::assertion())
214    }
215
216    fn verify_tls12_signature(
217        &self,
218        message: &[u8],
219        cert: &CertificateDer<'_>,
220        dss: &DigitallySignedStruct,
221    ) -> std::result::Result<HandshakeSignatureValid, TlsError> {
222        verify_tls12(message, cert, dss)
223    }
224
225    fn verify_tls13_signature(
226        &self,
227        message: &[u8],
228        cert: &CertificateDer<'_>,
229        dss: &DigitallySignedStruct,
230    ) -> std::result::Result<HandshakeSignatureValid, TlsError> {
231        verify_tls13(message, cert, dss)
232    }
233
234    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
235        self.schemes.clone()
236    }
237}
238
239/// Server-side verifier: verifies the CLIENT's leaf chains to the DigNetwork CA (clientAuth), pins
240/// its `peer_id`, and checks the BLS binding. Client auth is MANDATORY — this is mutual TLS.
241#[derive(Debug)]
242pub struct DigClientCertVerifier {
243    expected: Option<PeerId>,
244    captured: CapturedPeerId,
245    binding_policy: BindingPolicy,
246    captured_bls: CapturedBlsPub,
247    schemes: Vec<SignatureScheme>,
248    root_hints: Vec<DistinguishedName>,
249}
250
251impl DigClientCertVerifier {
252    /// Build a verifier that requires a client leaf chaining to the DigNetwork CA, pins `expected`
253    /// (or accepts any such peer when `None`), captures the derived id + BLS pubkey, and applies
254    /// `binding_policy`.
255    pub fn new(
256        expected: Option<PeerId>,
257        captured: CapturedPeerId,
258        binding_policy: BindingPolicy,
259        captured_bls: CapturedBlsPub,
260    ) -> Self {
261        Self {
262            expected,
263            captured,
264            binding_policy,
265            captured_bls,
266            schemes: default_signature_schemes(),
267            root_hints: Vec::new(),
268        }
269    }
270}
271
272impl ClientCertVerifier for DigClientCertVerifier {
273    fn offer_client_auth(&self) -> bool {
274        true
275    }
276
277    fn client_auth_mandatory(&self) -> bool {
278        true
279    }
280
281    fn root_hint_subjects(&self) -> &[DistinguishedName] {
282        &self.root_hints
283    }
284
285    fn verify_client_cert(
286        &self,
287        end_entity: &CertificateDer<'_>,
288        intermediates: &[CertificateDer<'_>],
289        now: UnixTime,
290    ) -> std::result::Result<ClientCertVerified, TlsError> {
291        verify_chain_to_dig_ca(end_entity, intermediates, now, KeyUsage::client_auth())?;
292        pin_and_bind(
293            end_entity,
294            self.expected,
295            &self.captured,
296            self.binding_policy,
297            &self.captured_bls,
298        )?;
299        Ok(ClientCertVerified::assertion())
300    }
301
302    fn verify_tls12_signature(
303        &self,
304        message: &[u8],
305        cert: &CertificateDer<'_>,
306        dss: &DigitallySignedStruct,
307    ) -> std::result::Result<HandshakeSignatureValid, TlsError> {
308        verify_tls12(message, cert, dss)
309    }
310
311    fn verify_tls13_signature(
312        &self,
313        message: &[u8],
314        cert: &CertificateDer<'_>,
315        dss: &DigitallySignedStruct,
316    ) -> std::result::Result<HandshakeSignatureValid, TlsError> {
317        verify_tls13(message, cert, dss)
318    }
319
320    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
321        self.schemes.clone()
322    }
323}
324
325/// A `webpki` trust anchor over the shipped DigNetwork CA — exposed so a consumer that builds its own
326/// rustls config (rather than using [`crate::config`]) can reuse the same trust root.
327pub fn dig_ca_trust_anchor_der() -> Result<CertificateDer<'static>> {
328    embedded_ca_cert_der()
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334    use crate::bls::{public_key_bytes, SecretKey};
335    use crate::ca::DigCa;
336    use crate::node_cert::NodeCert;
337    use rcgen::{
338        CertificateParams, DistinguishedName, DnType, ExtendedKeyUsagePurpose, Ia5String, KeyPair,
339        KeyUsagePurpose, SanType, PKCS_ECDSA_P256_SHA256,
340    };
341    use sha2::{Digest, Sha256};
342    use time::OffsetDateTime;
343
344    fn bls_sk(label: &str) -> SecretKey {
345        let seed: [u8; 32] = Sha256::digest(label.as_bytes()).into();
346        SecretKey::from_seed(&seed)
347    }
348
349    /// Mint a leaf signed by the SHIPPED DigNetwork CA but WITHOUT the #1204 binding extension.
350    fn unbound_dig_ca_leaf() -> CertificateDer<'static> {
351        let ca = DigCa::embedded().expect("embedded CA");
352        let leaf_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap();
353        let mut params = CertificateParams::new(Vec::<String>::new()).unwrap();
354        let mut dn = DistinguishedName::new();
355        dn.push(DnType::CommonName, "peer.dig");
356        params.distinguished_name = dn;
357        params.subject_alt_names = vec![SanType::DnsName(
358            Ia5String::try_from("peer.dig".to_string()).unwrap(),
359        )];
360        params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
361        params.extended_key_usages = vec![
362            ExtendedKeyUsagePurpose::ServerAuth,
363            ExtendedKeyUsagePurpose::ClientAuth,
364        ];
365        let cert = params.signed_by(&leaf_key, &ca.cert, &ca.key).unwrap();
366        CertificateDer::from(cert.der().to_vec())
367    }
368
369    /// Under `Required`, a DigNetwork-CA-signed leaf that carries NO binding is rejected by the
370    /// client verifier (anti-downgrade), even though its chain-to-CA is valid.
371    #[test]
372    fn required_rejects_ca_signed_unbound_leaf() {
373        let leaf = unbound_dig_ca_leaf();
374        let v = DigClientCertVerifier::new(
375            None,
376            CapturedPeerId::default(),
377            BindingPolicy::Required,
378            CapturedBlsPub::default(),
379        );
380        let err = v
381            .verify_client_cert(&leaf, &[], UnixTime::now())
382            .expect_err("Required rejects an unbound leaf");
383        assert!(
384            format!("{err}").contains("BLS binding"),
385            "rejected on the binding, not the chain"
386        );
387    }
388
389    /// A bound, DigNetwork-CA-signed leaf verifies AND captures the peer_id + BLS pubkey under
390    /// `Required` (the verifier's accept path, exercised without a socket).
391    #[test]
392    fn required_accepts_bound_leaf_and_captures_identity() {
393        let sk = bls_sk("verify/bound");
394        let node = NodeCert::generate_signed(&sk).expect("node cert");
395        let leaf = CertificateDer::from(node.cert_der().to_vec());
396
397        let captured = CapturedPeerId::default();
398        let captured_bls = CapturedBlsPub::default();
399        let v = DigClientCertVerifier::new(
400            None,
401            captured.clone(),
402            BindingPolicy::Required,
403            captured_bls.clone(),
404        );
405        v.verify_client_cert(&leaf, &[], UnixTime::now())
406            .expect("a bound DIG-CA leaf verifies");
407        assert_eq!(captured.get(), Some(node.peer_id()));
408        assert_eq!(captured_bls.get(), Some(public_key_bytes(&sk)));
409    }
410
411    /// A foreign-CA leaf fails the chain check regardless of policy.
412    #[test]
413    fn foreign_ca_leaf_fails_chain() {
414        let foreign = crate::ca::generate_dig_ca(OffsetDateTime::now_utc()).unwrap();
415        let foreign_ca = DigCa::from_pem(&foreign.cert_pem, &foreign.key_pem).unwrap();
416        let node = NodeCert::generate_signed_by(
417            &foreign_ca,
418            &bls_sk("verify/foreign"),
419            OffsetDateTime::now_utc(),
420        )
421        .unwrap();
422        let leaf = CertificateDer::from(node.cert_der().to_vec());
423
424        let v = DigServerCertVerifier::new(
425            None,
426            CapturedPeerId::default(),
427            BindingPolicy::Off,
428            CapturedBlsPub::default(),
429        );
430        let name = ServerName::try_from("peer.dig").unwrap();
431        let err = v
432            .verify_server_cert(&leaf, &[], &name, &[], UnixTime::now())
433            .expect_err("foreign CA leaf is rejected");
434        assert!(format!("{err}").contains("DigNetwork CA"));
435    }
436}