From 1f91cddab9cbd3c08101cf9b12f2718c9599ef42 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Fri, 14 Aug 2026 14:08:14 -0700 Subject: [PATCH] Restore HTTP/2 and client certificate parity on the native TLS path (#543) --- Cargo.lock | 2 + crates/yaak-http/Cargo.toml | 6 +- crates/yaak-http/src/client.rs | 208 ++++++++++++++++++++++++++++----- crates/yaak-tls/Cargo.toml | 1 + crates/yaak-tls/src/lib.rs | 104 +++++++++++++++++ 5 files changed, 294 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5a2856fb..7ca05257 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11226,6 +11226,7 @@ dependencies = [ "reqwest 0.12.20", "serde", "serde_json", + "tempfile", "thiserror 2.0.17", "tokio", "tokio-util", @@ -11441,6 +11442,7 @@ dependencies = [ "thiserror 2.0.17", "url", "yaak-models", + "yasna", ] [[package]] diff --git a/crates/yaak-http/Cargo.toml b/crates/yaak-http/Cargo.toml index 128b2e41..0da64313 100644 --- a/crates/yaak-http/Cargo.toml +++ b/crates/yaak-http/Cargo.toml @@ -18,11 +18,12 @@ zstd = "0.13" hyper-util = { version = "0.1.17", default-features = false, features = ["client-legacy"] } log = { workspace = true } mime_guess = "2.0.5" -native-tls = "0.2" +native-tls = { version = "0.2", features = ["alpn"] } regex = "1.11.1" reqwest = { workspace = true, features = [ "rustls-tls-manual-roots-no-provider", "native-tls", + "native-tls-alpn", "socks", "http2", "stream", @@ -38,3 +39,6 @@ yaak-common = { workspace = true } yaak-models = { workspace = true } yaak-templates = { workspace = true } yaak-tls = { workspace = true } + +[dev-dependencies] +tempfile = "3" diff --git a/crates/yaak-http/src/client.rs b/crates/yaak-http/src/client.rs index 10bfd18f..d958b6e9 100644 --- a/crates/yaak-http/src/client.rs +++ b/crates/yaak-http/src/client.rs @@ -2,9 +2,9 @@ use crate::dns::LocalhostResolver; use crate::error::Result; use log::{debug, info, warn}; use reqwest::{Client, ClientBuilder, Proxy, redirect}; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use yaak_models::models::DnsOverride; -use yaak_tls::{ClientCertificateConfig, get_tls_config}; +use yaak_tls::{ClientCertificateConfig, get_tls_config, load_client_identity_pkcs12}; pub const HTTP2_MAX_RESPONSE_HEADER_LIST_SIZE: u32 = 1024 * 1024; @@ -42,6 +42,9 @@ fn build_native_tls_connector( builder.danger_accept_invalid_certs(true); builder.danger_accept_invalid_hostnames(true); builder.min_protocol_version(Some(native_tls::Protocol::Tlsv10)); + // reqwest cannot add ALPN to a connector it did not build, so without this + // the native path would silently negotiate HTTP/1.1 for every request. + builder.request_alpns(&["h2", "http/1.1"]); if let Some(identity) = build_native_tls_identity(client_cert)? { builder.identity(identity); @@ -50,35 +53,20 @@ fn build_native_tls_connector( Ok(builder.build()?) } +/// Serializes PKCS#12 imports. On macOS native-tls imports every identity into +/// one process-wide temporary keychain, and it releases its own lock before +/// importing, so concurrent imports fail with an opaque OSStatus. +static IDENTITY_IMPORT: Mutex<()> = Mutex::new(()); + fn build_native_tls_identity( client_cert: Option, ) -> Result> { - let config = match client_cert { - None => return Ok(None), - Some(c) => c, + let Some((pkcs12, password)) = load_client_identity_pkcs12(client_cert)? else { + return Ok(None); }; - // Try PFX/PKCS12 first - if let Some(pfx_path) = &config.pfx_file { - if !pfx_path.is_empty() { - let pfx_data = std::fs::read(pfx_path)?; - let password = config.passphrase.as_deref().unwrap_or(""); - let identity = native_tls::Identity::from_pkcs12(&pfx_data, password)?; - return Ok(Some(identity)); - } - } - - // Try CRT + KEY files - if let (Some(crt_path), Some(key_path)) = (&config.crt_file, &config.key_file) { - if !crt_path.is_empty() && !key_path.is_empty() { - let crt_data = std::fs::read(crt_path)?; - let key_data = std::fs::read(key_path)?; - let identity = native_tls::Identity::from_pkcs8(&crt_data, &key_data)?; - return Ok(Some(identity)); - } - } - - Ok(None) + let _guard = IDENTITY_IMPORT.lock().unwrap_or_else(|e| e.into_inner()); + Ok(Some(native_tls::Identity::from_pkcs12(&pkcs12, &password)?)) } #[derive(Clone)] @@ -206,3 +194,171 @@ fn build_enabled_proxy( proxies } + +#[cfg(test)] +mod client_certificate_tests { + use super::*; + use yaak_tls::get_tls_config; + + // Self-signed throwaway certificates, generated only for these tests. + const RSA_CRT: &str = r#"-----BEGIN CERTIFICATE----- +MIIDEzCCAfugAwIBAgIUI3A1FyWKpilH/z1Ehnd68J9bpeEwDQYJKoZIhvcNAQEL +BQAwGDEWMBQGA1UEAwwNeWFhay10ZXN0LXJzYTAgFw0yNjA4MTQyMDQ2MjRaGA8y +MTI2MDcyMTIwNDYyNFowGDEWMBQGA1UEAwwNeWFhay10ZXN0LXJzYTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAK6o+exvMUs02ChHi81ILcaDkEaCzgFP +of6yDTF2m7jU1CUVwAcH4R0OKPetPVVqxPF+b/+LMdIeRIT/F5yaJgzWI6LQGmkZ +2hBrqQwuHKJq7g01+OuY0nQ13j/6HKRNKGdOiQRsKomMao1tcFYo1QMgD3lvhroO +Kr+zXPG8BP1T+b3taYyc9apeNaCouVBlY8cHk5/JxDjzOAP2s2oWBUGMSIFdjvQ+ +h0oCJ5MavHXC9mMf/7nUi9bdfClZKfitUOuLiPf4qAjPW0zsyj+6IVzOG69ds5CV +/yM5B30fZxpRBqOlklisLW3r+aU1X8PsUeNyM31n8HfUzJMN+QBVmAkCAwEAAaNT +MFEwHQYDVR0OBBYEFBN40FBTuNs2ITuWp54ydZjYEx8YMB8GA1UdIwQYMBaAFBN4 +0FBTuNs2ITuWp54ydZjYEx8YMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBAHdr7OgkQueHqkV31MyZAZQqOkEn6SnNo1lKwCdl5Axby2gNbSCdZLey +sHoQwEI1nbeRjXhK/Un+UB/aPBSpNTXjh7kLC6EhEj+xL1moUqcxEBxpSHY6awim +vAqRVnxA9IeZPDQy6si6W7nomaZzvdS5YBEDh9xFwsfbo/HGaHIGtngQwnglVKt2 +XedN38Z71J0ZPPdBcod5trSkJGJwsh5q4i9h1TbLvVT4Am/3IU/WsPldmQs654cn +Vaj38PL32yhzo2HUUndf+4XRQezQXcqgAZBqJTpZShXXoJuvqYobB9gdeS8HvBoa +c9aYuXK5sppEg3haLax8XRWjhra/yAI= +-----END CERTIFICATE-----"#; + + const RSA_PKCS1_KEY: &str = r#"-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEArqj57G8xSzTYKEeLzUgtxoOQRoLOAU+h/rINMXabuNTUJRXA +BwfhHQ4o9609VWrE8X5v/4sx0h5EhP8XnJomDNYjotAaaRnaEGupDC4comruDTX4 +65jSdDXeP/ocpE0oZ06JBGwqiYxqjW1wVijVAyAPeW+Gug4qv7Nc8bwE/VP5ve1p +jJz1ql41oKi5UGVjxweTn8nEOPM4A/azahYFQYxIgV2O9D6HSgInkxq8dcL2Yx// +udSL1t18KVkp+K1Q64uI9/ioCM9bTOzKP7ohXM4br12zkJX/IzkHfR9nGlEGo6WS +WKwtbev5pTVfw+xR43IzfWfwd9TMkw35AFWYCQIDAQABAoIBAATW9PvpR6BrnFr0 +QwM/i4MrbJhgB1cFIIFeL3/Kkf10YyoorjY+VB89g4Lto/++cmOndaoup3blO8HD +j6hneLzU0dyw/a8+so4kwnL8l1YkdovpcbLVw5dX0cdE/vZYa/wMK4aA7gK0GRvb +oIuNwCZkjA9x1TXl/PxsXpu/1fc5j/CUj0Kk3f/5Rj5cxM/nLqxVvd+EzVgt9TVv +fyspUPK0uRz2PK4ZXIVFAbJXVcpV3V3nY7UGjy8OukOVfEbAXtYeSq/eoI4H5Biu +gS1AnfST8ESe8r4OKl0Ii1sdhVS9etmBx+/EQjyjAGDe9Q38AgJL/6kPR7fuDdvU +SJ1ZM20CgYEA2NR01F2Yt8Z8/+02v213NPK+WYqFfOq7qusnBYdcibaivbszFNKO +kAzkaiWs4seou7ga9BbF5/kKjSlx2ud3Ks548IYg6/05XddXdiN4MT2rNn9m3IAm +zis5Sk/Elhw6HIilBJJgISjQVNj19nq1o2YTCSrvlccZNqn205DAEtUCgYEAzjZT +8OBaLcF1sEGY7hpVOeOe7377dG5B0ZO3k/q9IeW8f/W0xP6rcxdK+Rdun25Uig9H +36t2VYpboWTh1adTRUQRgcTFlFjulZilvofvFIgErLpRELqSghrd/O+0e1DPvx1w +YiMjZ5VMfvoOIvoYrNtgAmRBrkq/ohDvFEIGgmUCgYAPTh/ZBapL/pTAM+xTYtSx +RhktlNuLT75jeCnO+BkOF3gxUE9wvtQVUvOkkng7ocBFT9+HLzxU/X1DLZO90ezV +drGOuMkGH1+3QgYIbsSDJUk6lY+bLOiQUPjASBUmS2PGs9aCFhr2/DyIYLAr78l2 +eTQKx58VwXIEK8cic+s66QKBgQC0VdyIvZr/gr0KPAOizpKTwpS+u1zqEHYs8rLL +ja6TE1cKzHSfBlwnlUoyliRe9tyltAFWAJvG6O2DMjcxYlg3LfTleJCVUEStvMXN +3xDc8qqr53B3YcI4V4ik53f9k8lqSDN9D1+p+W3haYqtWev0VxEaZlTuOF5oO8jo +/Wi15QKBgQDOc9FRdEPrW6KEA9QJROjt8vEmElWgJu14KxMpK8Jq6HgZ2/CqC6Hl +wdphddrz57mWSuJtrAnmbSiiGyw3uUZFSQV8CLW12k5qaulxR9ODdosNjaEjJ/Rl +yUXONJ+DpurH+/ZzRKM5q1AqMG54EeX/WoCQbBOPB9zfYJJ1AKuZ5g== +-----END RSA PRIVATE KEY-----"#; + + const RSA_PKCS8_KEY: &str = r#"-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCuqPnsbzFLNNgo +R4vNSC3Gg5BGgs4BT6H+sg0xdpu41NQlFcAHB+EdDij3rT1VasTxfm//izHSHkSE +/xecmiYM1iOi0BppGdoQa6kMLhyiau4NNfjrmNJ0Nd4/+hykTShnTokEbCqJjGqN +bXBWKNUDIA95b4a6Diq/s1zxvAT9U/m97WmMnPWqXjWgqLlQZWPHB5OfycQ48zgD +9rNqFgVBjEiBXY70PodKAieTGrx1wvZjH/+51IvW3XwpWSn4rVDri4j3+KgIz1tM +7Mo/uiFczhuvXbOQlf8jOQd9H2caUQajpZJYrC1t6/mlNV/D7FHjcjN9Z/B31MyT +DfkAVZgJAgMBAAECggEABNb0++lHoGucWvRDAz+LgytsmGAHVwUggV4vf8qR/XRj +KiiuNj5UHz2Dgu2j/75yY6d1qi6nduU7wcOPqGd4vNTR3LD9rz6yjiTCcvyXViR2 +i+lxstXDl1fRx0T+9lhr/AwrhoDuArQZG9ugi43AJmSMD3HVNeX8/Gxem7/V9zmP +8JSPQqTd//lGPlzEz+curFW934TNWC31NW9/KylQ8rS5HPY8rhlchUUBsldVylXd +XedjtQaPLw66Q5V8RsBe1h5Kr96gjgfkGK6BLUCd9JPwRJ7yvg4qXQiLWx2FVL16 +2YHH78RCPKMAYN71DfwCAkv/qQ9Ht+4N29RInVkzbQKBgQDY1HTUXZi3xnz/7Ta/ +bXc08r5ZioV86ruq6ycFh1yJtqK9uzMU0o6QDORqJazix6i7uBr0FsXn+QqNKXHa +53cqznjwhiDr/Tld11d2I3gxPas2f2bcgCbOKzlKT8SWHDociKUEkmAhKNBU2PX2 +erWjZhMJKu+Vxxk2qfbTkMAS1QKBgQDONlPw4FotwXWwQZjuGlU5457vfvt0bkHR +k7eT+r0h5bx/9bTE/qtzF0r5F26fblSKD0ffq3ZViluhZOHVp1NFRBGBxMWUWO6V +mKW+h+8UiASsulEQupKCGt3877R7UM+/HXBiIyNnlUx++g4i+his22ACZEGuSr+i +EO8UQgaCZQKBgA9OH9kFqkv+lMAz7FNi1LFGGS2U24tPvmN4Kc74GQ4XeDFQT3C+ +1BVS86SSeDuhwEVP34cvPFT9fUMtk73R7NV2sY64yQYfX7dCBghuxIMlSTqVj5ss +6JBQ+MBIFSZLY8az1oIWGvb8PIhgsCvvyXZ5NArHnxXBcgQrxyJz6zrpAoGBALRV +3Ii9mv+CvQo8A6LOkpPClL67XOoQdizyssuNrpMTVwrMdJ8GXCeVSjKWJF723KW0 +AVYAm8bo7YMyNzFiWDct9OV4kJVQRK28xc3fENzyqqvncHdhwjhXiKTnd/2TyWpI +M30PX6n5beFpiq1Z6/RXERpmVO44Xmg7yOj9aLXlAoGBAM5z0VF0Q+tbooQD1AlE +6O3y8SYSVaAm7XgrEykrwmroeBnb8KoLoeXB2mF12vPnuZZK4m2sCeZtKKIbLDe5 +RkVJBXwItbXaTmpq6XFH04N2iw2NoSMn9GXJRc40n4Om6sf79nNEozmrUCowbngR +5f9agJBsE48H3N9gknUAq5nm +-----END PRIVATE KEY-----"#; + + const EC_CRT: &str = r#"-----BEGIN CERTIFICATE----- +MIIBhTCCASugAwIBAgIUB8703dqXCUOJQbhbyaMUMbVFOjwwCgYIKoZIzj0EAwIw +FzEVMBMGA1UEAwwMeWFhay10ZXN0LWVjMCAXDTI2MDgxNDIwNDYyNFoYDzIxMjYw +NzIxMjA0NjI0WjAXMRUwEwYDVQQDDAx5YWFrLXRlc3QtZWMwWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAATCYYKhzgHEaRaGsYVjJSoXvoroL8qe1yeEA0VtfxFzMBg+ ++bkPQ0nCtMyFfvQQtXWYIakxzsWJyhI8wPjUj6QSo1MwUTAdBgNVHQ4EFgQUKq40 +Hl+2DziVkBVR/tGsPj9FRo0wHwYDVR0jBBgwFoAUKq40Hl+2DziVkBVR/tGsPj9F +Ro0wDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNIADBFAiEAj1dx5XLl9iCZ +rD0CW+a3RTluxQ5icXno9WJ9qaS6L08CIFx2t0y9znQr7n5x+SmfXbfZtkDola8e +8nEZga/HXSeu +-----END CERTIFICATE-----"#; + + const EC_SEC1_KEY: &str = r#"-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIIoiiZ/hb4h6eHkZUVBTQFz7KLrVKJqQtWee2ygOjijNoAoGCCqGSM49 +AwEHoUQDQgAEwmGCoc4BxGkWhrGFYyUqF76K6C/KntcnhANFbX8RczAYPvm5D0NJ +wrTMhX70ELV1mCGpMc7FicoSPMD41I+kEg== +-----END EC PRIVATE KEY-----"#; + + const EC_PKCS8_KEY: &str = r#"-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgiiKJn+FviHp4eRlR +UFNAXPsoutUompC1Z57bKA6OKM2hRANCAATCYYKhzgHEaRaGsYVjJSoXvoroL8qe +1yeEA0VtfxFzMBg++bkPQ0nCtMyFfvQQtXWYIakxzsWJyhI8wPjUj6QS +-----END PRIVATE KEY-----"#; + + /// Every key format the rustls path accepts, in the encodings users + /// actually have on disk. + fn key_formats() -> Vec<(&'static str, &'static str, &'static str)> { + vec![ + ("RSA cert with PKCS#8 key", RSA_CRT, RSA_PKCS8_KEY), + ("RSA cert with PKCS#1 key", RSA_CRT, RSA_PKCS1_KEY), + ("EC cert with PKCS#8 key", EC_CRT, EC_PKCS8_KEY), + ("EC cert with SEC1 key", EC_CRT, EC_SEC1_KEY), + ] + } + + fn write_pair(dir: &tempfile::TempDir, crt: &str, key: &str) -> ClientCertificateConfig { + let crt_path = dir.path().join("client.crt"); + let key_path = dir.path().join("client.key"); + std::fs::write(&crt_path, crt).unwrap(); + std::fs::write(&key_path, key).unwrap(); + ClientCertificateConfig { + crt_file: Some(crt_path.to_str().unwrap().to_string()), + key_file: Some(key_path.to_str().unwrap().to_string()), + pfx_file: None, + passphrase: None, + } + } + + #[test] + fn native_tls_accepts_every_format_rustls_accepts() { + for (name, crt, key) in key_formats() { + let dir = tempfile::TempDir::new().unwrap(); + let config = write_pair(&dir, crt, key); + + get_tls_config(false, true, Some(config.clone())) + .unwrap_or_else(|e| panic!("rustls rejected {name}: {e}")); + + let identity = build_native_tls_identity(Some(config)) + .unwrap_or_else(|e| panic!("native-tls rejected {name}: {e}")); + assert!(identity.is_some(), "no identity built for {name}"); + } + } + + #[test] + fn a_leading_header_line_before_the_key_is_tolerated() { + let dir = tempfile::TempDir::new().unwrap(); + let key = format!("Bag Attributes: friendlyName=client\n{RSA_PKCS8_KEY}"); + let config = write_pair(&dir, RSA_CRT, &key); + assert!(build_native_tls_identity(Some(config)).unwrap().is_some()); + } + + #[test] + fn no_identity_is_built_without_a_configured_certificate() { + assert!(build_native_tls_identity(None).unwrap().is_none()); + + let empty = ClientCertificateConfig { + crt_file: Some("".into()), + key_file: Some("".into()), + pfx_file: Some("".into()), + passphrase: None, + }; + assert!(build_native_tls_identity(Some(empty)).unwrap().is_none()); + } +} diff --git a/crates/yaak-tls/Cargo.toml b/crates/yaak-tls/Cargo.toml index ac628b3c..f7c78b65 100644 --- a/crates/yaak-tls/Cargo.toml +++ b/crates/yaak-tls/Cargo.toml @@ -14,3 +14,4 @@ serde = { workspace = true, features = ["derive"] } thiserror = "2.0.17" url = "2.5" yaak-models = { workspace = true } +yasna = "0.5" diff --git a/crates/yaak-tls/src/lib.rs b/crates/yaak-tls/src/lib.rs index 5579cdfb..b94a6528 100644 --- a/crates/yaak-tls/src/lib.rs +++ b/crates/yaak-tls/src/lib.rs @@ -11,9 +11,18 @@ use std::io::BufReader; use std::path::Path; use std::str::FromStr; use std::sync::Arc; +use yasna::models::ObjectIdentifier; pub mod error; +const OID_RSA_ENCRYPTION: &[u64] = &[1, 2, 840, 113549, 1, 1, 1]; +const OID_EC_PUBLIC_KEY: &[u64] = &[1, 2, 840, 10045, 2, 1]; + +/// Password for the PKCS#12 blob [`load_client_identity_pkcs12`] builds from PEM +/// files. The blob never leaves the process, so the value only has to agree with +/// the caller that immediately re-parses it. +const IN_MEMORY_PKCS12_PASSWORD: &str = "yaak"; + #[derive(Clone, Default)] pub struct ClientCertificateConfig { pub crt_file: Option, @@ -98,6 +107,101 @@ fn load_client_cert( Ok(None) } +/// Load the configured client certificate as PKCS#12 DER, along with the +/// password needed to open it. +/// +/// Native TLS stacks accept a client identity as either PKCS#12 or a PKCS#8 +/// PEM, and the PKCS#8 route rejects EC keys on macOS outright. Going through +/// PKCS#12 keeps the key formats we accept identical to the rustls path, which +/// reads PKCS#1 and SEC1 keys directly. +pub fn load_client_identity_pkcs12( + client_cert: Option, +) -> Result, String)>> { + let config = match client_cert { + None => return Ok(None), + Some(c) => c, + }; + + // Pass a user-supplied PFX through untouched. The OS parser understands more + // encryption algorithms than re-encoding it here would preserve. + if let Some(pfx_path) = &config.pfx_file { + if !pfx_path.is_empty() { + let data = fs::read(Path::new(pfx_path))?; + return Ok(Some((data, config.passphrase.clone().unwrap_or_default()))); + } + } + + let Some((certs, key)) = load_client_cert(Some(config))? else { + return Ok(None); + }; + + let key_der = to_pkcs8_der(&key)?; + let (leaf, cas) = certs.split_first().ok_or(GenericError("No certificates found".into()))?; + let cas: Vec<&[u8]> = cas.iter().map(|c| c.as_ref()).collect(); + + let pfx = p12::PFX::new_with_cas(leaf, &key_der, &cas, IN_MEMORY_PKCS12_PASSWORD, "yaak") + .ok_or(GenericError("Failed to build PKCS#12 from client certificate".into()))?; + + Ok(Some((pfx.to_der(), IN_MEMORY_PKCS12_PASSWORD.to_string()))) +} + +/// Re-encode a private key as PKCS#8 DER, wrapping PKCS#1 and SEC1 keys. +fn to_pkcs8_der(key: &PrivateKeyDer<'_>) -> Result> { + match key { + PrivateKeyDer::Pkcs8(k) => Ok(k.secret_pkcs8_der().to_vec()), + PrivateKeyDer::Pkcs1(k) => Ok(wrap_pkcs8(OID_RSA_ENCRYPTION, None, k.secret_pkcs1_der())), + PrivateKeyDer::Sec1(k) => { + let (curve, inner) = split_sec1(k.secret_sec1_der())?; + Ok(wrap_pkcs8(OID_EC_PUBLIC_KEY, Some(curve), &inner)) + } + _ => Err(GenericError("Unsupported private key format".into())), + } +} + +/// Build a PKCS#8 `PrivateKeyInfo` (RFC 5208) around an already-encoded key. +fn wrap_pkcs8(algorithm: &[u64], parameters: Option, key_der: &[u8]) -> Vec { + yasna::construct_der(|w| { + w.write_sequence(|w| { + w.next().write_u8(0); + w.next().write_sequence(|w| { + w.next().write_oid(&ObjectIdentifier::from_slice(algorithm)); + match ¶meters { + Some(oid) => w.next().write_oid(oid), + None => w.next().write_null(), + } + }); + w.next().write_bytes(key_der); + }) + }) +} + +/// Split a SEC1 `ECPrivateKey` (RFC 5915) into its named curve and a copy of the +/// key with that curve removed. PKCS#8 carries the curve in the algorithm +/// identifier, and RFC 5915 says it should not also be repeated inside the key. +fn split_sec1(der: &[u8]) -> Result<(ObjectIdentifier, Vec)> { + yasna::parse_der(der, |r| { + r.read_sequence(|r| { + let version = r.next().read_u8()?; + let private_key = r.next().read_bytes()?; + let curve = r.next().read_tagged(yasna::Tag::context(0), |r| r.read_oid())?; + let public_key = r.read_optional(|r| r.read_tagged_der())?; + + let inner = yasna::construct_der(|w| { + w.write_sequence(|w| { + w.next().write_u8(version); + w.next().write_bytes(&private_key); + if let Some(public_key) = &public_key { + w.next().write_tagged_der(public_key); + } + }) + }); + + Ok((curve, inner)) + }) + }) + .map_err(|e| GenericError(format!("EC private key is missing a named curve: {e}"))) +} + fn load_pem_files( crt_path: &str, key_path: &str,