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, pins its `peer_id`, and checks the BLS binding.
167///
168/// Two modes, selected at construction:
169///
170/// - [`Self::new`] (default) additionally requires the leaf to chain to the shipped DigNetwork CA —
171///   the trust-DOMAIN marker for a fully-migrated DIG network.
172/// - [`Self::new_spki_pinned`] DROPS that CA-chain requirement (see the type-level note on
173///   `require_ca_chain`) while keeping every real authentication check. Used because live DIG peers
174///   present self-signed / chia-ssl leaves today (#1378 CA-everywhere migration deferred), so the
175///   CA-requiring path rejects every legit peer with `UnknownIssuer` (#1422).
176#[derive(Debug)]
177pub struct DigServerCertVerifier {
178    expected: Option<PeerId>,
179    captured: CapturedPeerId,
180    binding_policy: BindingPolicy,
181    captured_bls: CapturedBlsPub,
182    schemes: Vec<SignatureScheme>,
183    /// When `true`, the presented leaf MUST chain to the shipped DigNetwork CA (the classic mode).
184    /// When `false` (SPKI-pinned mode), the CA-chain step is SKIPPED — but the REAL authentication is
185    /// unchanged: `peer_id = SHA-256(SPKI DER)` pinning + rustls proof-of-possession (the handshake
186    /// signature, which rustls verifies regardless) + the #1204 BLS binding still run. Dropping the
187    /// CA chain only removes the trust-domain marker, not identity; it exists so a self-signed live
188    /// peer (#1378 deferred) is accepted (#1422, mirrors dig-gossip #1371's `CaptureAnyClientCert`).
189    require_ca_chain: bool,
190}
191
192impl DigServerCertVerifier {
193    /// Build a verifier that REQUIRES the server leaf to chain to the DigNetwork CA, pins `expected`
194    /// (or accepts any such peer when `None`), captures the derived id + BLS pubkey, and applies
195    /// `binding_policy`.
196    pub fn new(
197        expected: Option<PeerId>,
198        captured: CapturedPeerId,
199        binding_policy: BindingPolicy,
200        captured_bls: CapturedBlsPub,
201    ) -> Self {
202        Self {
203            expected,
204            captured,
205            binding_policy,
206            captured_bls,
207            schemes: default_signature_schemes(),
208            require_ca_chain: true,
209        }
210    }
211
212    /// Build a SPKI-PINNED verifier: identical to [`Self::new`] except it does NOT require the server
213    /// leaf to chain to the DigNetwork CA. Authentication still rests on the `peer_id` pin + rustls
214    /// proof-of-possession + the #1204 BLS binding (see `require_ca_chain`). Use this to dial the
215    /// self-signed peers on the live network (#1422); the CA-requiring [`Self::new`] stays for the
216    /// deferred #1378 DIG-CA-everywhere migration.
217    ///
218    /// **SAFETY / USAGE CONTRACT:** Unlike CA mode (where accept-any at least enforces the DIG trust
219    /// domain), SPKI-pinned mode drops the CA check, so passing `expected: None` together with a
220    /// non-`Required` `BindingPolicy` authenticates NOTHING about which peer answered — any peer
221    /// presenting any self-signed leaf is accepted, and an active MITM is undetectable. A dialer MUST
222    /// pass `expected: Some(peer_id)` (or use `BindingPolicy::Required`) to authenticate the specific
223    /// peer. See #1422 / #1371.
224    pub fn new_spki_pinned(
225        expected: Option<PeerId>,
226        captured: CapturedPeerId,
227        binding_policy: BindingPolicy,
228        captured_bls: CapturedBlsPub,
229    ) -> Self {
230        Self {
231            expected,
232            captured,
233            binding_policy,
234            captured_bls,
235            schemes: default_signature_schemes(),
236            require_ca_chain: false,
237        }
238    }
239}
240
241impl ServerCertVerifier for DigServerCertVerifier {
242    fn verify_server_cert(
243        &self,
244        end_entity: &CertificateDer<'_>,
245        intermediates: &[CertificateDer<'_>],
246        _server_name: &ServerName<'_>,
247        _ocsp_response: &[u8],
248        now: UnixTime,
249    ) -> std::result::Result<ServerCertVerified, TlsError> {
250        if self.require_ca_chain {
251            verify_chain_to_dig_ca(end_entity, intermediates, now, KeyUsage::server_auth())?;
252        }
253        pin_and_bind(
254            end_entity,
255            self.expected,
256            &self.captured,
257            self.binding_policy,
258            &self.captured_bls,
259        )?;
260        Ok(ServerCertVerified::assertion())
261    }
262
263    fn verify_tls12_signature(
264        &self,
265        message: &[u8],
266        cert: &CertificateDer<'_>,
267        dss: &DigitallySignedStruct,
268    ) -> std::result::Result<HandshakeSignatureValid, TlsError> {
269        verify_tls12(message, cert, dss)
270    }
271
272    fn verify_tls13_signature(
273        &self,
274        message: &[u8],
275        cert: &CertificateDer<'_>,
276        dss: &DigitallySignedStruct,
277    ) -> std::result::Result<HandshakeSignatureValid, TlsError> {
278        verify_tls13(message, cert, dss)
279    }
280
281    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
282        self.schemes.clone()
283    }
284}
285
286/// Server-side verifier: verifies the CLIENT's leaf, pins its `peer_id`, and checks the BLS binding.
287/// Client auth is MANDATORY — this is mutual TLS.
288///
289/// Two modes, selected at construction — same distinction as [`DigServerCertVerifier`]:
290///
291/// - [`Self::new`] (default) additionally requires the client leaf to chain to the DigNetwork CA.
292/// - [`Self::new_spki_pinned`] drops that requirement (see `require_ca_chain`) so a self-signed live
293///   peer is accepted (#1422; mirrors dig-gossip #1371).
294#[derive(Debug)]
295pub struct DigClientCertVerifier {
296    expected: Option<PeerId>,
297    captured: CapturedPeerId,
298    binding_policy: BindingPolicy,
299    captured_bls: CapturedBlsPub,
300    schemes: Vec<SignatureScheme>,
301    root_hints: Vec<DistinguishedName>,
302    /// When `true`, the client leaf MUST chain to the shipped DigNetwork CA. When `false`
303    /// (SPKI-pinned mode) the CA-chain step is SKIPPED, while the `peer_id` pin + rustls
304    /// proof-of-possession + #1204 BLS binding still authenticate the peer — see the matching field
305    /// on [`DigServerCertVerifier`] for the full rationale (#1422 / #1378-deferred / #1371).
306    require_ca_chain: bool,
307}
308
309impl DigClientCertVerifier {
310    /// Build a verifier that REQUIRES a client leaf chaining to the DigNetwork CA, pins `expected`
311    /// (or accepts any such peer when `None`), captures the derived id + BLS pubkey, and applies
312    /// `binding_policy`.
313    pub fn new(
314        expected: Option<PeerId>,
315        captured: CapturedPeerId,
316        binding_policy: BindingPolicy,
317        captured_bls: CapturedBlsPub,
318    ) -> Self {
319        Self {
320            expected,
321            captured,
322            binding_policy,
323            captured_bls,
324            schemes: default_signature_schemes(),
325            root_hints: Vec::new(),
326            require_ca_chain: true,
327        }
328    }
329
330    /// Build a SPKI-PINNED verifier: identical to [`Self::new`] except it does NOT require the client
331    /// leaf to chain to the DigNetwork CA. Authentication still rests on the `peer_id` pin + rustls
332    /// proof-of-possession + the #1204 BLS binding (see `require_ca_chain`). Use this to accept the
333    /// self-signed peers on the live network (#1422).
334    pub fn new_spki_pinned(
335        expected: Option<PeerId>,
336        captured: CapturedPeerId,
337        binding_policy: BindingPolicy,
338        captured_bls: CapturedBlsPub,
339    ) -> Self {
340        Self {
341            expected,
342            captured,
343            binding_policy,
344            captured_bls,
345            schemes: default_signature_schemes(),
346            root_hints: Vec::new(),
347            require_ca_chain: false,
348        }
349    }
350}
351
352impl ClientCertVerifier for DigClientCertVerifier {
353    fn offer_client_auth(&self) -> bool {
354        true
355    }
356
357    fn client_auth_mandatory(&self) -> bool {
358        true
359    }
360
361    fn root_hint_subjects(&self) -> &[DistinguishedName] {
362        &self.root_hints
363    }
364
365    fn verify_client_cert(
366        &self,
367        end_entity: &CertificateDer<'_>,
368        intermediates: &[CertificateDer<'_>],
369        now: UnixTime,
370    ) -> std::result::Result<ClientCertVerified, TlsError> {
371        if self.require_ca_chain {
372            verify_chain_to_dig_ca(end_entity, intermediates, now, KeyUsage::client_auth())?;
373        }
374        pin_and_bind(
375            end_entity,
376            self.expected,
377            &self.captured,
378            self.binding_policy,
379            &self.captured_bls,
380        )?;
381        Ok(ClientCertVerified::assertion())
382    }
383
384    fn verify_tls12_signature(
385        &self,
386        message: &[u8],
387        cert: &CertificateDer<'_>,
388        dss: &DigitallySignedStruct,
389    ) -> std::result::Result<HandshakeSignatureValid, TlsError> {
390        verify_tls12(message, cert, dss)
391    }
392
393    fn verify_tls13_signature(
394        &self,
395        message: &[u8],
396        cert: &CertificateDer<'_>,
397        dss: &DigitallySignedStruct,
398    ) -> std::result::Result<HandshakeSignatureValid, TlsError> {
399        verify_tls13(message, cert, dss)
400    }
401
402    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
403        self.schemes.clone()
404    }
405}
406
407/// A `webpki` trust anchor over the shipped DigNetwork CA — exposed so a consumer that builds its own
408/// rustls config (rather than using [`crate::config`]) can reuse the same trust root.
409pub fn dig_ca_trust_anchor_der() -> Result<CertificateDer<'static>> {
410    embedded_ca_cert_der()
411}
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416    use crate::bls::{public_key_bytes, SecretKey};
417    use crate::ca::DigCa;
418    use crate::node_cert::NodeCert;
419    use rcgen::{
420        CertificateParams, DistinguishedName, DnType, ExtendedKeyUsagePurpose, Ia5String, KeyPair,
421        KeyUsagePurpose, SanType, PKCS_ECDSA_P256_SHA256,
422    };
423    use sha2::{Digest, Sha256};
424    use time::OffsetDateTime;
425
426    fn bls_sk(label: &str) -> SecretKey {
427        let seed: [u8; 32] = Sha256::digest(label.as_bytes()).into();
428        SecretKey::from_seed(&seed)
429    }
430
431    /// Mint a leaf signed by the SHIPPED DigNetwork CA but WITHOUT the #1204 binding extension.
432    fn unbound_dig_ca_leaf() -> CertificateDer<'static> {
433        let ca = DigCa::embedded().expect("embedded CA");
434        let leaf_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap();
435        let mut params = CertificateParams::new(Vec::<String>::new()).unwrap();
436        let mut dn = DistinguishedName::new();
437        dn.push(DnType::CommonName, "peer.dig");
438        params.distinguished_name = dn;
439        params.subject_alt_names = vec![SanType::DnsName(
440            Ia5String::try_from("peer.dig".to_string()).unwrap(),
441        )];
442        params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
443        params.extended_key_usages = vec![
444            ExtendedKeyUsagePurpose::ServerAuth,
445            ExtendedKeyUsagePurpose::ClientAuth,
446        ];
447        let cert = params.signed_by(&leaf_key, &ca.cert, &ca.key).unwrap();
448        CertificateDer::from(cert.der().to_vec())
449    }
450
451    /// Under `Required`, a DigNetwork-CA-signed leaf that carries NO binding is rejected by the
452    /// client verifier (anti-downgrade), even though its chain-to-CA is valid.
453    #[test]
454    fn required_rejects_ca_signed_unbound_leaf() {
455        let leaf = unbound_dig_ca_leaf();
456        let v = DigClientCertVerifier::new(
457            None,
458            CapturedPeerId::default(),
459            BindingPolicy::Required,
460            CapturedBlsPub::default(),
461        );
462        let err = v
463            .verify_client_cert(&leaf, &[], UnixTime::now())
464            .expect_err("Required rejects an unbound leaf");
465        assert!(
466            format!("{err}").contains("BLS binding"),
467            "rejected on the binding, not the chain"
468        );
469    }
470
471    /// A bound, DigNetwork-CA-signed leaf verifies AND captures the peer_id + BLS pubkey under
472    /// `Required` (the verifier's accept path, exercised without a socket).
473    #[test]
474    fn required_accepts_bound_leaf_and_captures_identity() {
475        let sk = bls_sk("verify/bound");
476        let node = NodeCert::generate_signed(&sk).expect("node cert");
477        let leaf = CertificateDer::from(node.cert_der().to_vec());
478
479        let captured = CapturedPeerId::default();
480        let captured_bls = CapturedBlsPub::default();
481        let v = DigClientCertVerifier::new(
482            None,
483            captured.clone(),
484            BindingPolicy::Required,
485            captured_bls.clone(),
486        );
487        v.verify_client_cert(&leaf, &[], UnixTime::now())
488            .expect("a bound DIG-CA leaf verifies");
489        assert_eq!(captured.get(), Some(node.peer_id()));
490        assert_eq!(captured_bls.get(), Some(public_key_bytes(&sk)));
491    }
492
493    /// Mint a TRULY self-signed leaf (no CA — it signs itself) carrying the #1204 binding to
494    /// `label`'s BLS key. This is the shape a live DIG peer presents today (#1378 DIG-CA-everywhere
495    /// deferred): it does NOT chain to the shipped DigNetwork CA. Returns the leaf, its derived
496    /// peer_id, and the BLS pubkey it is bound to.
497    fn self_signed_bound_leaf(label: &str) -> (CertificateDer<'static>, PeerId, [u8; 48]) {
498        let sk = bls_sk(label);
499        let leaf_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap();
500        let mut params = CertificateParams::new(Vec::<String>::new()).unwrap();
501        let mut dn = DistinguishedName::new();
502        dn.push(DnType::CommonName, "peer.dig");
503        params.distinguished_name = dn;
504        params.subject_alt_names = vec![SanType::DnsName(
505            Ia5String::try_from("peer.dig".to_string()).unwrap(),
506        )];
507        params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
508        params.extended_key_usages = vec![
509            ExtendedKeyUsagePurpose::ServerAuth,
510            ExtendedKeyUsagePurpose::ClientAuth,
511        ];
512        crate::binding::attach_binding(&mut params, &leaf_key, &sk);
513        let cert = params.self_signed(&leaf_key).unwrap();
514        let der = CertificateDer::from(cert.der().to_vec());
515        let peer_id = peer_id_from_leaf_cert_der(der.as_ref()).unwrap();
516        (der, peer_id, public_key_bytes(&sk))
517    }
518
519    /// Mint a TRULY self-signed leaf WITHOUT any #1204 binding — the real chia-ssl / self-signed live
520    /// case. Returns the leaf and its derived peer_id.
521    fn self_signed_unbound_leaf() -> (CertificateDer<'static>, PeerId) {
522        let leaf_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap();
523        let mut params = CertificateParams::new(Vec::<String>::new()).unwrap();
524        let mut dn = DistinguishedName::new();
525        dn.push(DnType::CommonName, "peer.dig");
526        params.distinguished_name = dn;
527        params.subject_alt_names = vec![SanType::DnsName(
528            Ia5String::try_from("peer.dig".to_string()).unwrap(),
529        )];
530        params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
531        params.extended_key_usages = vec![
532            ExtendedKeyUsagePurpose::ServerAuth,
533            ExtendedKeyUsagePurpose::ClientAuth,
534        ];
535        let cert = params.self_signed(&leaf_key).unwrap();
536        let der = CertificateDer::from(cert.der().to_vec());
537        let peer_id = peer_id_from_leaf_cert_der(der.as_ref()).unwrap();
538        (der, peer_id)
539    }
540
541    /// The SPKI-pinned verifier ACCEPTS a self-signed leaf that does NOT chain to the DigNetwork CA
542    /// and captures `peer_id == SHA-256(SPKI DER)` — while the CA-requiring verifier REJECTS the SAME
543    /// leaf with a "DigNetwork CA" error. Proves the two modes genuinely differ and the CA path is
544    /// intact (#1422 / mirrors dig-gossip #1371).
545    #[test]
546    fn spki_pinned_accepts_self_signed_leaf() {
547        let (leaf, peer_id, _bls) = self_signed_bound_leaf("verify/spki-self-signed");
548
549        // SPKI-pinned mode: accepted, identity captured.
550        let captured = CapturedPeerId::default();
551        let v = DigServerCertVerifier::new_spki_pinned(
552            None,
553            captured.clone(),
554            BindingPolicy::Opportunistic,
555            CapturedBlsPub::default(),
556        );
557        let name = ServerName::try_from("peer.dig").unwrap();
558        v.verify_server_cert(&leaf, &[], &name, &[], UnixTime::now())
559            .expect("SPKI-pinned mode accepts a self-signed leaf");
560        assert_eq!(captured.get(), Some(peer_id));
561
562        // CA-requiring mode: the SAME leaf is rejected at the chain check.
563        let ca_v = DigServerCertVerifier::new(
564            None,
565            CapturedPeerId::default(),
566            BindingPolicy::Opportunistic,
567            CapturedBlsPub::default(),
568        );
569        let err = ca_v
570            .verify_server_cert(&leaf, &[], &name, &[], UnixTime::now())
571            .expect_err("CA-requiring mode rejects a self-signed leaf");
572        assert!(
573            format!("{err}").contains("DigNetwork CA"),
574            "rejected on the chain, not something else: {err}"
575        );
576    }
577
578    /// The identity-equality guard still holds in SPKI-pinned mode: a leaf whose derived peer_id does
579    /// not equal the pinned `expected` is rejected with "peer_id mismatch".
580    #[test]
581    fn spki_pinned_rejects_wrong_peer_id() {
582        let (leaf, _peer_id, _bls) = self_signed_bound_leaf("verify/spki-wrong-pin");
583        let wrong = PeerId::from_bytes([0x22u8; 32]);
584        let v = DigServerCertVerifier::new_spki_pinned(
585            Some(wrong),
586            CapturedPeerId::default(),
587            BindingPolicy::Opportunistic,
588            CapturedBlsPub::default(),
589        );
590        let name = ServerName::try_from("peer.dig").unwrap();
591        let err = v
592            .verify_server_cert(&leaf, &[], &name, &[], UnixTime::now())
593            .expect_err("a wrong-peer_id pin is rejected even in SPKI-pinned mode");
594        assert!(
595            format!("{err}").contains("peer_id mismatch"),
596            "rejected on the pin: {err}"
597        );
598    }
599
600    /// The live case: a self-signed leaf carrying NO #1204 binding (the real chia-ssl peer) is
601    /// ACCEPTED under `Opportunistic` (dig-nat's default) yet REJECTED under `Required` on the binding
602    /// (anti-downgrade preserved) — exercised on the server-side (client-auth) verifier.
603    #[test]
604    fn spki_pinned_live_case_unbound_self_signed_under_opportunistic() {
605        let (leaf, peer_id) = self_signed_unbound_leaf();
606
607        // Opportunistic: accepted, identity captured, no BLS pubkey captured (none present).
608        let captured = CapturedPeerId::default();
609        let captured_bls = CapturedBlsPub::default();
610        let opp = DigClientCertVerifier::new_spki_pinned(
611            None,
612            captured.clone(),
613            BindingPolicy::Opportunistic,
614            captured_bls.clone(),
615        );
616        opp.verify_client_cert(&leaf, &[], UnixTime::now())
617            .expect("Opportunistic accepts an unbound self-signed leaf");
618        assert_eq!(captured.get(), Some(peer_id));
619        assert_eq!(captured_bls.get(), None);
620
621        // Required: rejected on the absent binding (anti-downgrade).
622        let req = DigClientCertVerifier::new_spki_pinned(
623            None,
624            CapturedPeerId::default(),
625            BindingPolicy::Required,
626            CapturedBlsPub::default(),
627        );
628        let err = req
629            .verify_client_cert(&leaf, &[], UnixTime::now())
630            .expect_err("Required rejects an unbound leaf");
631        assert!(
632            format!("{err}").contains("BLS binding"),
633            "rejected on the binding, not the chain: {err}"
634        );
635    }
636
637    /// A foreign-CA leaf fails the chain check regardless of policy.
638    #[test]
639    fn foreign_ca_leaf_fails_chain() {
640        let foreign = crate::ca::generate_dig_ca(OffsetDateTime::now_utc()).unwrap();
641        let foreign_ca = DigCa::from_pem(&foreign.cert_pem, &foreign.key_pem).unwrap();
642        let node = NodeCert::generate_signed_by(
643            &foreign_ca,
644            &bls_sk("verify/foreign"),
645            OffsetDateTime::now_utc(),
646        )
647        .unwrap();
648        let leaf = CertificateDer::from(node.cert_der().to_vec());
649
650        let v = DigServerCertVerifier::new(
651            None,
652            CapturedPeerId::default(),
653            BindingPolicy::Off,
654            CapturedBlsPub::default(),
655        );
656        let name = ServerName::try_from("peer.dig").unwrap();
657        let err = v
658            .verify_server_cert(&leaf, &[], &name, &[], UnixTime::now())
659            .expect_err("foreign CA leaf is rejected");
660        assert!(format!("{err}").contains("DigNetwork CA"));
661    }
662}