mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-25 21:04:04 +02:00
Restore HTTP/2 and client certificate parity on the native TLS path (#543)
This commit is contained in:
Generated
+2
@@ -11226,6 +11226,7 @@ dependencies = [
|
|||||||
"reqwest 0.12.20",
|
"reqwest 0.12.20",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
@@ -11441,6 +11442,7 @@ dependencies = [
|
|||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"url",
|
"url",
|
||||||
"yaak-models",
|
"yaak-models",
|
||||||
|
"yasna",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -18,11 +18,12 @@ zstd = "0.13"
|
|||||||
hyper-util = { version = "0.1.17", default-features = false, features = ["client-legacy"] }
|
hyper-util = { version = "0.1.17", default-features = false, features = ["client-legacy"] }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
mime_guess = "2.0.5"
|
mime_guess = "2.0.5"
|
||||||
native-tls = "0.2"
|
native-tls = { version = "0.2", features = ["alpn"] }
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
reqwest = { workspace = true, features = [
|
reqwest = { workspace = true, features = [
|
||||||
"rustls-tls-manual-roots-no-provider",
|
"rustls-tls-manual-roots-no-provider",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
|
"native-tls-alpn",
|
||||||
"socks",
|
"socks",
|
||||||
"http2",
|
"http2",
|
||||||
"stream",
|
"stream",
|
||||||
@@ -38,3 +39,6 @@ yaak-common = { workspace = true }
|
|||||||
yaak-models = { workspace = true }
|
yaak-models = { workspace = true }
|
||||||
yaak-templates = { workspace = true }
|
yaak-templates = { workspace = true }
|
||||||
yaak-tls = { workspace = true }
|
yaak-tls = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3"
|
||||||
|
|||||||
+182
-26
@@ -2,9 +2,9 @@ use crate::dns::LocalhostResolver;
|
|||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use reqwest::{Client, ClientBuilder, Proxy, redirect};
|
use reqwest::{Client, ClientBuilder, Proxy, redirect};
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, Mutex};
|
||||||
use yaak_models::models::DnsOverride;
|
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;
|
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_certs(true);
|
||||||
builder.danger_accept_invalid_hostnames(true);
|
builder.danger_accept_invalid_hostnames(true);
|
||||||
builder.min_protocol_version(Some(native_tls::Protocol::Tlsv10));
|
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)? {
|
if let Some(identity) = build_native_tls_identity(client_cert)? {
|
||||||
builder.identity(identity);
|
builder.identity(identity);
|
||||||
@@ -50,35 +53,20 @@ fn build_native_tls_connector(
|
|||||||
Ok(builder.build()?)
|
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(
|
fn build_native_tls_identity(
|
||||||
client_cert: Option<ClientCertificateConfig>,
|
client_cert: Option<ClientCertificateConfig>,
|
||||||
) -> Result<Option<native_tls::Identity>> {
|
) -> Result<Option<native_tls::Identity>> {
|
||||||
let config = match client_cert {
|
let Some((pkcs12, password)) = load_client_identity_pkcs12(client_cert)? else {
|
||||||
None => return Ok(None),
|
return Ok(None);
|
||||||
Some(c) => c,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Try PFX/PKCS12 first
|
let _guard = IDENTITY_IMPORT.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
if let Some(pfx_path) = &config.pfx_file {
|
Ok(Some(native_tls::Identity::from_pkcs12(&pkcs12, &password)?))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -206,3 +194,171 @@ fn build_enabled_proxy(
|
|||||||
|
|
||||||
proxies
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ serde = { workspace = true, features = ["derive"] }
|
|||||||
thiserror = "2.0.17"
|
thiserror = "2.0.17"
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
yaak-models = { workspace = true }
|
yaak-models = { workspace = true }
|
||||||
|
yasna = "0.5"
|
||||||
|
|||||||
@@ -11,9 +11,18 @@ use std::io::BufReader;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use yasna::models::ObjectIdentifier;
|
||||||
|
|
||||||
pub mod error;
|
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)]
|
#[derive(Clone, Default)]
|
||||||
pub struct ClientCertificateConfig {
|
pub struct ClientCertificateConfig {
|
||||||
pub crt_file: Option<String>,
|
pub crt_file: Option<String>,
|
||||||
@@ -98,6 +107,101 @@ fn load_client_cert(
|
|||||||
Ok(None)
|
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<ClientCertificateConfig>,
|
||||||
|
) -> Result<Option<(Vec<u8>, 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<Vec<u8>> {
|
||||||
|
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<ObjectIdentifier>, key_der: &[u8]) -> Vec<u8> {
|
||||||
|
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<u8>)> {
|
||||||
|
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(
|
fn load_pem_files(
|
||||||
crt_path: &str,
|
crt_path: &str,
|
||||||
key_path: &str,
|
key_path: &str,
|
||||||
|
|||||||
Reference in New Issue
Block a user