Replace WebPkiClientVerifier with pinned cert verifier for mTLS

ober

9c43493bf21bddda20a932040b71673d28bb0a9d

diff --git a/jerboa-native-rs/src/tls.rs b/jerboa-native-rs/src/tls.rs
index 11eb311..ac2fbf4 100644
--- a/jerboa-native-rs/src/tls.rs
+++ b/jerboa-native-rs/src/tls.rs
@@ -253,6 +253,69 @@ impl rustls::client::danger::ServerCertVerifier for PinVerifier {
     }
 }
 
+// Client certificate verifier for self-signed mTLS: accepts client certs
+// whose SHA-256 fingerprint matches one of the trusted cert fingerprints.
+// This avoids full PKI chain verification (which requires CA:TRUE, proper
+// key usage, etc.) and is ideal for self-signed deployments where both
+// sides share the same certificate.
+#[derive(Debug)]
+struct PinnedClientVerifier {
+    accepted_hashes: Vec<Vec<u8>>,
+}
+
+impl rustls::server::danger::ClientCertVerifier for PinnedClientVerifier {
+    fn root_hint_subjects(&self) -> &[rustls::DistinguishedName] {
+        &[]
+    }
+
+    fn verify_client_cert(
+        &self,
+        end_entity: &CertificateDer<'_>,
+        _intermediates: &[CertificateDer<'_>],
+        _now: rustls::pki_types::UnixTime,
+    ) -> Result<rustls::server::danger::ClientCertVerified, rustls::Error> {
+        let digest = ring::digest::digest(&ring::digest::SHA256, end_entity.as_ref());
+        for accepted in &self.accepted_hashes {
+            if digest.as_ref() == accepted.as_slice() {
+                return Ok(rustls::server::danger::ClientCertVerified::assertion());
+            }
+        }
+        Err(rustls::Error::General("client certificate not in trusted set".to_string()))
+    }
+
+    fn verify_tls12_signature(
+        &self,
+        _message: &[u8],
+        _cert: &CertificateDer<'_>,
+        _dss: &rustls::DigitallySignedStruct,
+    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
+        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
+    }
+
+    fn verify_tls13_signature(
+        &self,
+        _message: &[u8],
+        _cert: &CertificateDer<'_>,
+        _dss: &rustls::DigitallySignedStruct,
+    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
+        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
+    }
+
+    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
+        rustls::crypto::ring::default_provider()
+            .signature_verification_algorithms
+            .supported_schemes()
+    }
+
+    fn offer_client_auth(&self) -> bool {
+        true
+    }
+
+    fn client_auth_mandatory(&self) -> bool {
+        true
+    }
+}
+
 // ============================================================
 // Server: create TLS server context and accept connections
 // ============================================================
@@ -500,7 +563,8 @@ pub extern "C" fn jerboa_tls_server_new_mtls_pem(
             }
         };
 
-        // Parse client CA certs from PEM bytes
+        // Parse client CA certs from PEM bytes — compute their SHA-256 hashes
+        // as the set of accepted client certificate fingerprints.
         let mut ca_cursor = std::io::Cursor::new(ca_data);
         let ca_certs: Vec<CertificateDer<'static>> =
             rustls_pemfile::certs(&mut ca_cursor)
@@ -511,25 +575,17 @@ pub extern "C" fn jerboa_tls_server_new_mtls_pem(
             return 0;
         }
 
-        // Build root cert store from client CA
-        let mut client_root_store = rustls::RootCertStore::empty();
-        for cert in ca_certs {
-            if let Err(e) = client_root_store.add(cert) {
-                set_last_error(format!("add CA cert: {}", e));
-                return 0;
-            }
-        }
-
-        // Build client cert verifier
-        let client_verifier = match rustls::server::WebPkiClientVerifier::builder(
-            Arc::new(client_root_store),
-        ).build() {
-            Ok(v) => v,
-            Err(e) => {
-                set_last_error(format!("client verifier: {}", e));
-                return 0;
-            }
-        };
+        // For self-signed mTLS: instead of full PKI chain verification
+        // (which fails with self-signed certs that lack CA:TRUE),
+        // verify the client presents a cert whose SHA-256 matches one
+        // of the trusted CA certs. This is equivalent to certificate
+        // pinning and is stricter than CA-based verification for our
+        // use case (single shared cert).
+        let accepted_hashes: Vec<Vec<u8>> = ca_certs
+            .iter()
+            .map(|c| ring::digest::digest(&ring::digest::SHA256, c.as_ref()).as_ref().to_vec())
+            .collect();
+        let client_verifier = Arc::new(PinnedClientVerifier { accepted_hashes });
 
         // Build server config with client auth required
         let config = match ServerConfig::builder()