mTLS: skip hostname verification, add in-memory cert/key APIs

ober

1af4f8399b60ac39c606f751d41270a308540a8e

diff --git a/jerboa-native-rs/src/tls.rs b/jerboa-native-rs/src/tls.rs
index d5a787c..fc5abdd 100644
--- a/jerboa-native-rs/src/tls.rs
+++ b/jerboa-native-rs/src/tls.rs
@@ -330,6 +330,78 @@ pub extern "C" fn jerboa_tls_server_new(
     }
 }
 
+/// Create a TLS server context from in-memory PEM cert and key data.
+/// Same as jerboa_tls_server_new but reads from byte buffers instead of files.
+/// Returns context handle (>0) on success, 0 on error.
+#[no_mangle]
+pub extern "C" fn jerboa_tls_server_new_pem(
+    cert_pem: *const u8,
+    cert_pem_len: usize,
+    key_pem: *const u8,
+    key_pem_len: usize,
+) -> u64 {
+    match std::panic::catch_unwind(|| {
+        if cert_pem.is_null() || key_pem.is_null() {
+            set_last_error("null cert/key PEM pointer".to_string());
+            return 0;
+        }
+
+        let cert_data = unsafe { std::slice::from_raw_parts(cert_pem, cert_pem_len) };
+        let key_data = unsafe { std::slice::from_raw_parts(key_pem, key_pem_len) };
+
+        // Parse cert chain from PEM bytes
+        let mut cert_cursor = std::io::Cursor::new(cert_data);
+        let certs: Vec<CertificateDer<'static>> =
+            rustls_pemfile::certs(&mut cert_cursor)
+                .filter_map(|r| r.ok())
+                .collect();
+        if certs.is_empty() {
+            set_last_error("no certificates found in PEM data".to_string());
+            return 0;
+        }
+
+        // Parse private key from PEM bytes
+        let mut key_cursor = std::io::Cursor::new(key_data);
+        let key = match rustls_pemfile::private_key(&mut key_cursor) {
+            Ok(Some(k)) => k,
+            Ok(None) => {
+                set_last_error("no private key found in PEM data".to_string());
+                return 0;
+            }
+            Err(e) => {
+                set_last_error(format!("read key PEM: {}", e));
+                return 0;
+            }
+        };
+
+        let config = match ServerConfig::builder()
+            .with_no_client_auth()
+            .with_single_cert(certs, PrivateKeyDer::from(key))
+        {
+            Ok(c) => c,
+            Err(e) => {
+                set_last_error(format!("server config: {}", e));
+                return 0;
+            }
+        };
+
+        let handle = next_handle();
+        server_ctxs()
+            .lock()
+            .unwrap()
+            .insert(handle, TlsServerCtx {
+                config: Arc::new(config),
+            });
+        handle
+    }) {
+        Ok(h) => h,
+        Err(_) => {
+            set_last_error("panic in tls_server_new_pem".to_string());
+            0
+        }
+    }
+}
+
 /// Accept a TLS connection on an already-accepted TCP fd.
 /// Takes the server context handle and a raw fd (from accept()).
 /// Returns a connection handle (>0) on success, 0 on error.
@@ -579,18 +651,13 @@ pub extern "C" fn jerboa_tls_connect_mtls(
             .filter_map(|r| r.ok())
             .collect();
 
-        // Build root store from CA cert
-        let mut root_store = rustls::RootCertStore::empty();
-        for cert in ca_certs {
-            if let Err(e) = root_store.add(cert) {
-                set_last_error(format!("add CA cert: {}", e));
-                return 0;
-            }
-        }
-
-        // Build client config with client cert
+        // Build client config: skip hostname verification (self-signed),
+        // but present client cert for mutual authentication.
         let config = match ClientConfig::builder()
-            .with_root_certificates(root_store)
+            .dangerous()
+            .with_custom_certificate_verifier(Arc::new(PinVerifier {
+                expected_sha256: None,
+            }))
             .with_client_auth_cert(client_certs, PrivateKeyDer::from(client_key))
         {
             Ok(c) => c,
diff --git a/jerboa-native-rs/src/x509.rs b/jerboa-native-rs/src/x509.rs
index 77e4690..1f16094 100644
--- a/jerboa-native-rs/src/x509.rs
+++ b/jerboa-native-rs/src/x509.rs
@@ -1,12 +1,13 @@
 use crate::panic::{ffi_wrap, set_last_error};
-use rcgen::{CertificateParams, KeyPair, SanType, PKCS_ED25519};
+use rcgen::{CertificateParams, Ia5String, KeyPair, SanType, PKCS_ECDSA_P256_SHA256};
 use ring::digest;
 use std::net::IpAddr;
 use time::{Duration, OffsetDateTime};
 
-/// Generate a self-signed Ed25519 certificate with IP address SANs.
+/// Generate a self-signed ECDSA P-256 certificate with IP and/or DNS SANs.
 ///
-/// ip_addrs_csv: comma-separated IP addresses (e.g., "192.168.1.1,10.0.0.5")
+/// ip_addrs_csv: comma-separated SANs — IP addresses or hostnames
+///   (e.g., "192.168.1.1,example.com,10.0.0.5")
 /// validity_days: certificate lifetime in days (e.g., 365)
 /// cert_path: filesystem path to write PEM certificate
 /// key_path: filesystem path to write PEM private key
@@ -41,7 +42,7 @@ pub extern "C" fn jerboa_x509_generate_self_signed(
         let key_path_str = unsafe { std::str::from_utf8_unchecked(
             std::slice::from_raw_parts(key_path, key_path_len)) };
 
-        // Parse IP addresses
+        // Parse SANs — each entry is either an IP address or a DNS hostname
         let mut sans: Vec<SanType> = Vec::new();
         for addr_str in ip_str.split(',') {
             let trimmed = addr_str.trim();
@@ -50,19 +51,25 @@ pub extern "C" fn jerboa_x509_generate_self_signed(
             }
             match trimmed.parse::<IpAddr>() {
                 Ok(ip) => sans.push(SanType::IpAddress(ip)),
-                Err(e) => {
-                    set_last_error(format!("invalid IP address '{}': {}", trimmed, e));
-                    return -1;
+                Err(_) => {
+                    // Not an IP — treat as DNS hostname
+                    match Ia5String::try_from(trimmed) {
+                        Ok(name) => sans.push(SanType::DnsName(name)),
+                        Err(e) => {
+                            set_last_error(format!("invalid hostname '{}': {}", trimmed, e));
+                            return -1;
+                        }
+                    }
                 }
             }
         }
         if sans.is_empty() {
-            set_last_error("at least one IP address is required".to_string());
+            set_last_error("at least one IP or hostname is required".to_string());
             return -1;
         }
 
-        // Generate Ed25519 key pair
-        let key_pair = match KeyPair::generate_for(&PKCS_ED25519) {
+        // Generate ECDSA P-256 key pair (browser-compatible)
+        let key_pair = match KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256) {
             Ok(kp) => kp,
             Err(e) => {
                 set_last_error(format!("key generation failed: {}", e));
@@ -115,6 +122,115 @@ pub extern "C" fn jerboa_x509_generate_self_signed(
     })
 }
 
+/// Generate a self-signed ECDSA P-256 certificate and return PEM data in memory.
+///
+/// Same parameters as jerboa_x509_generate_self_signed but writes PEM data to
+/// caller-provided buffers instead of files.
+///
+/// Returns 0 on success, -1 on error.
+#[no_mangle]
+pub extern "C" fn jerboa_x509_generate_self_signed_mem(
+    ip_addrs_csv: *const u8,
+    ip_addrs_len: usize,
+    validity_days: i32,
+    cert_out: *mut u8,
+    cert_out_max: usize,
+    cert_out_len: *mut usize,
+    key_out: *mut u8,
+    key_out_max: usize,
+    key_out_len: *mut usize,
+) -> i32 {
+    ffi_wrap(|| {
+        if ip_addrs_csv.is_null() || cert_out.is_null() || key_out.is_null()
+            || cert_out_len.is_null() || key_out_len.is_null()
+        {
+            set_last_error("null pointer argument".to_string());
+            return -1;
+        }
+        if validity_days <= 0 {
+            set_last_error("validity_days must be positive".to_string());
+            return -1;
+        }
+
+        let ip_str = unsafe {
+            std::str::from_utf8_unchecked(std::slice::from_raw_parts(ip_addrs_csv, ip_addrs_len))
+        };
+
+        // Parse SANs
+        let mut sans: Vec<SanType> = Vec::new();
+        for addr_str in ip_str.split(',') {
+            let trimmed = addr_str.trim();
+            if trimmed.is_empty() {
+                continue;
+            }
+            match trimmed.parse::<IpAddr>() {
+                Ok(ip) => sans.push(SanType::IpAddress(ip)),
+                Err(_) => match Ia5String::try_from(trimmed) {
+                    Ok(name) => sans.push(SanType::DnsName(name)),
+                    Err(e) => {
+                        set_last_error(format!("invalid hostname '{}': {}", trimmed, e));
+                        return -1;
+                    }
+                },
+            }
+        }
+        if sans.is_empty() {
+            set_last_error("at least one IP or hostname is required".to_string());
+            return -1;
+        }
+
+        let key_pair = match KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256) {
+            Ok(kp) => kp,
+            Err(e) => {
+                set_last_error(format!("key generation failed: {}", e));
+                return -1;
+            }
+        };
+
+        let mut params = CertificateParams::default();
+        params.subject_alt_names = sans;
+        params.not_before = OffsetDateTime::now_utc();
+        params.not_after = OffsetDateTime::now_utc() + Duration::days(validity_days as i64);
+
+        let cert = match params.self_signed(&key_pair) {
+            Ok(c) => c,
+            Err(e) => {
+                set_last_error(format!("certificate generation failed: {}", e));
+                return -1;
+            }
+        };
+
+        let cert_pem = cert.pem();
+        let key_pem = key_pair.serialize_pem();
+
+        if cert_pem.len() > cert_out_max {
+            set_last_error(format!(
+                "cert buffer too small: need {} have {}",
+                cert_pem.len(),
+                cert_out_max
+            ));
+            return -1;
+        }
+        if key_pem.len() > key_out_max {
+            set_last_error(format!(
+                "key buffer too small: need {} have {}",
+                key_pem.len(),
+                key_out_max
+            ));
+            return -1;
+        }
+
+        unsafe {
+            std::ptr::copy_nonoverlapping(cert_pem.as_ptr(), cert_out, cert_pem.len());
+            *cert_out_len = cert_pem.len();
+            std::ptr::copy_nonoverlapping(key_pem.as_ptr(), key_out, key_pem.len());
+            *key_out_len = key_pem.len();
+        }
+
+        0
+    })
+}
+
 /// Compute SHA-256 fingerprint of a PEM certificate file.
 ///
 /// Reads the PEM file, extracts the DER certificate, and computes SHA-256.