Skip to main content

macula_rust/
cert.rs

1//! TLS trust for dialing a macula-station: pubkey-pin verification,
2//! ported from `native/macula_quic/src/cert.rs` (`macula-io/macula`).
3//!
4//! A station presents a self-signed X.509 cert wrapping its macula
5//! identity's Ed25519 public key. A client that already knows which
6//! station it's dialing (from DHT records, pre-shared relay identities,
7//! or — for a mobile client — configuration) verifies by comparing the
8//! cert's SubjectPublicKeyInfo to that known pubkey directly. No CA
9//! chain, no DNS-anchored trust: the pubkey **is** the identity.
10//!
11//! Unlike the Erlang SDK's own `native/macula_quic`, this crate never
12//! needs to *generate* a cert — macula uses `with_no_client_auth()`
13//! throughout, so a dialing client presents no TLS certificate of its
14//! own at all. Identity/authentication happens entirely at the
15//! application layer (the CONNECT/HELLO frame's Ed25519 signature — see
16//! `plans/PLAN_WIRE_PROTOCOL.md` §2, §5), not via mutual TLS. This
17//! module is verification-only.
18
19use std::sync::Arc;
20
21use rustls::pki_types::CertificateDer;
22
23/// Extract the 32-byte Ed25519 public key from a DER-encoded X.509
24/// certificate's SubjectPublicKeyInfo. Errors if the SPKI algorithm
25/// isn't Ed25519 (OID `1.3.101.112`) or the key isn't 32 bytes.
26pub fn ed25519_pubkey_from_cert(der: &[u8]) -> Result<[u8; 32], String> {
27    let (_, cert) =
28        x509_parser::parse_x509_certificate(der).map_err(|e| format!("parse cert: {e}"))?;
29    let spki = cert.public_key();
30    let alg_oid = &spki.algorithm.algorithm;
31    if alg_oid.to_id_string() != "1.3.101.112" {
32        return Err(format!(
33            "expected Ed25519 SPKI, got OID {}",
34            alg_oid.to_id_string()
35        ));
36    }
37    let pk = spki.subject_public_key.data.as_ref();
38    pk.try_into()
39        .map_err(|_| format!("Ed25519 pubkey must be 32 bytes, got {}", pk.len()))
40}
41
42/// Custom rustls `ServerCertVerifier` that pins on the leaf cert's
43/// SubjectPublicKeyInfo Ed25519 pubkey rather than walking a CA chain —
44/// the pragmatic equivalent of TLS raw-public-key (RFC 7250) without
45/// changing the wire protocol. No expiry check, no SAN check, no CA
46/// chain: matches the reference verifier exactly, including its
47/// intentional narrowness.
48#[derive(Debug)]
49pub struct PubkeyPinVerifier {
50    pinned: [u8; 32],
51    crypto: Arc<rustls::crypto::CryptoProvider>,
52}
53
54impl PubkeyPinVerifier {
55    pub fn new(pinned_pubkey: [u8; 32]) -> Self {
56        Self {
57            pinned: pinned_pubkey,
58            crypto: Arc::new(rustls::crypto::ring::default_provider()),
59        }
60    }
61}
62
63impl rustls::client::danger::ServerCertVerifier for PubkeyPinVerifier {
64    fn verify_server_cert(
65        &self,
66        end_entity: &CertificateDer<'_>,
67        _intermediates: &[CertificateDer<'_>],
68        _server_name: &rustls::pki_types::ServerName<'_>,
69        _ocsp_response: &[u8],
70        _now: rustls::pki_types::UnixTime,
71    ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
72        let presented = ed25519_pubkey_from_cert(end_entity.as_ref())
73            .map_err(|e| rustls::Error::General(format!("pubkey extract: {e}")))?;
74
75        if presented == self.pinned {
76            Ok(rustls::client::danger::ServerCertVerified::assertion())
77        } else {
78            Err(rustls::Error::General(format!(
79                "pubkey mismatch: pinned={} presented={}",
80                hex(&self.pinned),
81                hex(&presented)
82            )))
83        }
84    }
85
86    fn verify_tls12_signature(
87        &self,
88        _message: &[u8],
89        _cert: &CertificateDer<'_>,
90        _dss: &rustls::DigitallySignedStruct,
91    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
92        // Ed25519 leaves are TLS 1.3 only; if a 1.2 path somehow
93        // triggers this, accept — the cert match above is what anchors
94        // trust, matching the reference verifier's own reasoning.
95        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
96    }
97
98    fn verify_tls13_signature(
99        &self,
100        message: &[u8],
101        cert: &CertificateDer<'_>,
102        dss: &rustls::DigitallySignedStruct,
103    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
104        // Defer to the crypto provider: this verifies `dss` was really
105        // produced by the leaf cert's own private key, i.e. that the
106        // peer we pinned by pubkey is the one actually completing this
107        // handshake, not just quoting someone else's cert.
108        rustls::crypto::verify_tls13_signature(
109            message,
110            cert,
111            dss,
112            &self.crypto.signature_verification_algorithms,
113        )
114    }
115
116    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
117        self.crypto
118            .signature_verification_algorithms
119            .supported_schemes()
120    }
121}
122
123/// Skips all server certificate verification. **Development/diagnostic
124/// use only** — establishes transport connectivity with no identity
125/// guarantee whatsoever. Never use this to dial a station you intend to
126/// trust; use [`PubkeyPinVerifier`] once the station's identity is
127/// known, matching the reference's own `verify => none` mode.
128#[derive(Debug)]
129pub struct SkipServerVerification(Arc<rustls::crypto::CryptoProvider>);
130
131impl SkipServerVerification {
132    pub fn new() -> Self {
133        Self(Arc::new(rustls::crypto::ring::default_provider()))
134    }
135}
136
137impl Default for SkipServerVerification {
138    fn default() -> Self {
139        Self::new()
140    }
141}
142
143impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
144    fn verify_server_cert(
145        &self,
146        _end_entity: &CertificateDer<'_>,
147        _intermediates: &[CertificateDer<'_>],
148        _server_name: &rustls::pki_types::ServerName<'_>,
149        _ocsp_response: &[u8],
150        _now: rustls::pki_types::UnixTime,
151    ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
152        Ok(rustls::client::danger::ServerCertVerified::assertion())
153    }
154
155    fn verify_tls12_signature(
156        &self,
157        _message: &[u8],
158        _cert: &CertificateDer<'_>,
159        _dss: &rustls::DigitallySignedStruct,
160    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
161        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
162    }
163
164    fn verify_tls13_signature(
165        &self,
166        _message: &[u8],
167        _cert: &CertificateDer<'_>,
168        _dss: &rustls::DigitallySignedStruct,
169    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
170        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
171    }
172
173    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
174        self.0.signature_verification_algorithms.supported_schemes()
175    }
176}
177
178fn hex(bytes: &[u8]) -> String {
179    bytes.iter().map(|b| format!("{b:02x}")).collect()
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use rustls::client::danger::ServerCertVerifier;
186
187    #[test]
188    fn ed25519_pubkey_extraction_rejects_garbage_der() {
189        assert!(ed25519_pubkey_from_cert(b"not a certificate").is_err());
190    }
191
192    #[test]
193    fn pinned_verifier_stores_the_exact_bytes_given() {
194        let pin = [0x42u8; 32];
195        let verifier = PubkeyPinVerifier::new(pin);
196        assert_eq!(verifier.pinned, pin);
197    }
198
199    /// A synthetic self-signed Ed25519 cert, shaped like what a station
200    /// running pubkey-pinned trust actually presents — generated with
201    /// `rcgen` purely for this test (not a runtime dependency, see the
202    /// module doc). No live station reachable from this crate's test
203    /// suite happens to be configured this way (the reachable demo
204    /// fleet all uses `Trust::WebPki` — see `tests/live_station.rs`), so
205    /// this is the only way to exercise `PubkeyPinVerifier`'s actual
206    /// matching logic end-to-end without one.
207    fn synthetic_ed25519_cert() -> (rustls::pki_types::CertificateDer<'static>, [u8; 32]) {
208        let key_pair = rcgen::KeyPair::generate_for(&rcgen::PKCS_ED25519).expect("keygen");
209        let params = rcgen::CertificateParams::new(Vec::<String>::new()).expect("params");
210        let cert = params.self_signed(&key_pair).expect("self-sign");
211        let der = rustls::pki_types::CertificateDer::from(cert.der().to_vec());
212        let pubkey =
213            ed25519_pubkey_from_cert(der.as_ref()).expect("our own synthetic cert must parse");
214        (der, pubkey)
215    }
216
217    fn fake_server_name() -> rustls::pki_types::ServerName<'static> {
218        rustls::pki_types::ServerName::try_from("station.example").expect("valid server name")
219    }
220
221    #[test]
222    fn extracts_the_real_pubkey_from_a_synthetic_cert() {
223        // Not asserting a specific value — proves the extraction path
224        // works end-to-end against a real (if synthetic) cert, distinct
225        // from `ed25519_pubkey_extraction_rejects_garbage_der`'s
226        // negative case.
227        let (_der, pubkey) = synthetic_ed25519_cert();
228        assert_ne!(
229            pubkey, [0u8; 32],
230            "a real generated key should not be all-zero"
231        );
232    }
233
234    #[test]
235    fn verify_server_cert_accepts_the_pinned_key() {
236        let (der, pubkey) = synthetic_ed25519_cert();
237        let verifier = PubkeyPinVerifier::new(pubkey);
238        let result = verifier.verify_server_cert(
239            &der,
240            &[],
241            &fake_server_name(),
242            &[],
243            rustls::pki_types::UnixTime::now(),
244        );
245        assert!(
246            result.is_ok(),
247            "pinning the cert's real key must succeed: {result:?}"
248        );
249    }
250
251    #[test]
252    fn verify_server_cert_rejects_a_mismatched_key() {
253        let (der, pubkey) = synthetic_ed25519_cert();
254        let mut wrong = pubkey;
255        wrong[0] ^= 0xFF;
256        let verifier = PubkeyPinVerifier::new(wrong);
257        let result = verifier.verify_server_cert(
258            &der,
259            &[],
260            &fake_server_name(),
261            &[],
262            rustls::pki_types::UnixTime::now(),
263        );
264        assert!(result.is_err(), "pinning the WRONG key must fail closed");
265    }
266
267    #[test]
268    fn skip_verification_accepts_anything() {
269        let (der, _pubkey) = synthetic_ed25519_cert();
270        let verifier = SkipServerVerification::new();
271        let result = verifier.verify_server_cert(
272            &der,
273            &[],
274            &fake_server_name(),
275            &[],
276            rustls::pki_types::UnixTime::now(),
277        );
278        assert!(
279            result.is_ok(),
280            "insecure mode must accept any cert, by design"
281        );
282    }
283}