Replace WebPkiClientVerifier with pinned cert verifier for mTLS
ober
9c43493bf21bddda20a932040b71673d28bb0a9d
--- 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()