feat: add optional native-tls support as alternative to rustls (#8037)
Signed-off-by: Rodolfo Olivieri <rolivier@redhat.com> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
+28
-6
@@ -8,7 +8,7 @@ repository.workspace = true
|
||||
description.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["code-mode", "local-inference", "aws-providers"]
|
||||
default = ["code-mode", "local-inference", "aws-providers", "rustls-tls"]
|
||||
code-mode = ["dep:pctx_code_mode"]
|
||||
local-inference = [
|
||||
"dep:candle-core",
|
||||
@@ -27,6 +27,26 @@ aws-providers = [
|
||||
"dep:aws-sdk-sagemakerruntime",
|
||||
]
|
||||
cuda = ["local-inference", "candle-core/cuda", "candle-nn/cuda", "llama-cpp-2/cuda"]
|
||||
rustls-tls = [
|
||||
"reqwest/rustls",
|
||||
"rmcp/reqwest",
|
||||
"sqlx/runtime-tokio-rustls",
|
||||
"jsonwebtoken/aws_lc_rs",
|
||||
"oauth2/reqwest",
|
||||
"oauth2/rustls-tls",
|
||||
]
|
||||
native-tls = [
|
||||
"dep:pem",
|
||||
"dep:pkcs1",
|
||||
"dep:pkcs8",
|
||||
"dep:sec1",
|
||||
"reqwest/native-tls",
|
||||
"rmcp/reqwest-native-tls",
|
||||
"sqlx/runtime-tokio-native-tls",
|
||||
"jsonwebtoken/rust_crypto",
|
||||
"oauth2/reqwest",
|
||||
"oauth2/native-tls",
|
||||
]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -35,17 +55,16 @@ workspace = true
|
||||
lru = { workspace = true }
|
||||
rmcp = { workspace = true, features = [
|
||||
"client",
|
||||
"reqwest",
|
||||
"transport-child-process",
|
||||
"transport-streamable-http-client",
|
||||
"transport-streamable-http-client-reqwest",
|
||||
] }
|
||||
oauth2 = "5.0"
|
||||
oauth2 = { version = "5.0", default-features = false }
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["rustls", "json", "cookies", "gzip", "brotli", "deflate", "zstd", "charset", "http2", "stream", "blocking", "multipart", "system-proxy"], default-features = false }
|
||||
reqwest = { workspace = true, features = ["json", "cookies", "gzip", "brotli", "deflate", "zstd", "charset", "http2", "stream", "blocking", "multipart", "system-proxy"], default-features = false }
|
||||
tokio = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
@@ -94,7 +113,6 @@ tokio-cron-scheduler = "0.14.0"
|
||||
urlencoding = { workspace = true }
|
||||
v_htmlescape = "0.15"
|
||||
sqlx = { version = "0.8", default-features = false, features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
"chrono",
|
||||
"json",
|
||||
@@ -111,7 +129,7 @@ aws-sdk-bedrockruntime = { version = "=1.120.0", default-features = false, featu
|
||||
aws-sdk-sagemakerruntime = { version = "1.62.0", default-features = false, features = ["default-https-client", "rt-tokio"], optional = true }
|
||||
|
||||
# For GCP Vertex AI provider auth
|
||||
jsonwebtoken = { version = "10.3.0", features = ["aws_lc_rs"] }
|
||||
jsonwebtoken = { version = "10.3.0", default-features = false, features = ["use_pem"] }
|
||||
|
||||
blake3 = "1.5"
|
||||
fs2 = { workspace = true }
|
||||
@@ -160,6 +178,10 @@ llama-cpp-2 = { version = "0.1.137", features = ["sampler"], optional = true }
|
||||
encoding_rs = "0.8.35"
|
||||
pastey = "0.2.1"
|
||||
shell-words = { workspace = true }
|
||||
pem = { version = "3", optional = true }
|
||||
pkcs1 = { version = "0.7", default-features = false, features = ["pkcs8"], optional = true }
|
||||
pkcs8 = { version = "0.10", default-features = false, features = ["alloc"], optional = true }
|
||||
sec1 = { version = "0.7", default-features = false, features = ["der", "pkcs8"], optional = true }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
winapi = { workspace = true }
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
#[cfg(not(any(feature = "rustls-tls", feature = "native-tls")))]
|
||||
compile_error!("At least one of `rustls-tls` or `native-tls` features must be enabled");
|
||||
|
||||
#[cfg(all(feature = "rustls-tls", feature = "native-tls"))]
|
||||
compile_error!("Features `rustls-tls` and `native-tls` are mutually exclusive");
|
||||
|
||||
pub mod acp;
|
||||
pub mod action_required_manager;
|
||||
pub mod agents;
|
||||
|
||||
@@ -119,12 +119,21 @@ impl TlsConfig {
|
||||
let key_pem = read_to_string(&cert_key_pair.key_path)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to read client private key: {}", e))?;
|
||||
|
||||
// Create a combined PEM file with certificate and private key
|
||||
let combined_pem = format!("{}\n{}", cert_pem, key_pem);
|
||||
#[cfg(not(feature = "native-tls"))]
|
||||
let identity = {
|
||||
let combined_pem = format!("{}\n{}", cert_pem, key_pem);
|
||||
Identity::from_pem(combined_pem.as_bytes()).map_err(|e| {
|
||||
anyhow::anyhow!("Failed to create identity from cert and key: {}", e)
|
||||
})?
|
||||
};
|
||||
|
||||
let identity = Identity::from_pem(combined_pem.as_bytes()).map_err(|e| {
|
||||
anyhow::anyhow!("Failed to create identity from cert and key: {}", e)
|
||||
})?;
|
||||
#[cfg(feature = "native-tls")]
|
||||
let identity = {
|
||||
let pkcs8_key_pem = convert_key_to_pkcs8_pem(&key_pem)?;
|
||||
Identity::from_pkcs8_pem(cert_pem.as_bytes(), pkcs8_key_pem.as_bytes()).map_err(
|
||||
|e| anyhow::anyhow!("Failed to create identity from cert and key: {}", e),
|
||||
)?
|
||||
};
|
||||
|
||||
Ok(Some(identity))
|
||||
} else {
|
||||
@@ -154,6 +163,71 @@ impl Default for TlsConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a PEM private key from any format (PKCS#1, SEC1, PKCS#8) to PKCS#8 PEM.
|
||||
///
|
||||
/// `reqwest::Identity::from_pkcs8_pem` (native-tls) only accepts PKCS#8
|
||||
/// (`-----BEGIN PRIVATE KEY-----`), but private keys in the wild come in three formats:
|
||||
///
|
||||
/// - **PKCS#1** (`-----BEGIN RSA PRIVATE KEY-----`): Legacy RSA-specific format.
|
||||
/// Generated by default with `openssl genrsa`. Very common in older setups, tutorials,
|
||||
/// and CA-issued key files.
|
||||
/// - **SEC1** (`-----BEGIN EC PRIVATE KEY-----`): Legacy EC-specific format.
|
||||
/// Generated by default with `openssl ecparam -genkey`. Common with EC certificates.
|
||||
/// - **PKCS#8** (`-----BEGIN PRIVATE KEY-----`): Generic, algorithm-agnostic wrapper.
|
||||
/// This is the only format native-tls accepts.
|
||||
///
|
||||
/// Without this conversion, users with legacy-format keys (Kubernetes secrets, corporate
|
||||
/// PKI, etc.) would get a cryptic "Failed to create identity" error and need to manually
|
||||
/// run `openssl pkey -in key.pem -out key-pkcs8.pem`.
|
||||
///
|
||||
/// Note: the rustls code path (`Identity::from_pem`) accepts all formats natively,
|
||||
/// so this conversion is only needed for native-tls.
|
||||
#[cfg(feature = "native-tls")]
|
||||
fn convert_key_to_pkcs8_pem(key_pem_str: &str) -> Result<String> {
|
||||
use pkcs8::der::{Decode, Encode};
|
||||
|
||||
let parsed =
|
||||
pem::parse(key_pem_str).map_err(|e| anyhow::anyhow!("Failed to parse PEM key: {}", e))?;
|
||||
|
||||
match parsed.tag() {
|
||||
"PRIVATE KEY" => Ok(key_pem_str.to_string()),
|
||||
"RSA PRIVATE KEY" => {
|
||||
let info = pkcs8::PrivateKeyInfo::new(pkcs1::ALGORITHM_ID, parsed.contents());
|
||||
let der_bytes = info
|
||||
.to_der()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to encode PKCS#8: {}", e))?;
|
||||
Ok(pem::encode(&pem::Pem::new("PRIVATE KEY", der_bytes)))
|
||||
}
|
||||
"EC PRIVATE KEY" => {
|
||||
let ec_key = sec1::EcPrivateKey::from_der(parsed.contents())
|
||||
.map_err(|e| anyhow::anyhow!("Failed to parse EC key: {}", e))?;
|
||||
let curve_oid = ec_key
|
||||
.parameters
|
||||
.and_then(|p| p.named_curve())
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"EC key missing curve parameters. Convert to PKCS#8: \
|
||||
openssl pkey -in key.pem -out key-pkcs8.pem"
|
||||
)
|
||||
})?;
|
||||
let algorithm = pkcs8::AlgorithmIdentifierRef {
|
||||
oid: sec1::ALGORITHM_OID,
|
||||
parameters: Some((&curve_oid).into()),
|
||||
};
|
||||
let info = pkcs8::PrivateKeyInfo::new(algorithm, parsed.contents());
|
||||
let der_bytes = info
|
||||
.to_der()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to encode PKCS#8: {}", e))?;
|
||||
Ok(pem::encode(&pem::Pem::new("PRIVATE KEY", der_bytes)))
|
||||
}
|
||||
tag => Err(anyhow::anyhow!(
|
||||
"Unsupported key format '{}'. Expected PKCS#8, PKCS#1, or SEC1. \
|
||||
Convert with: openssl pkey -in key.pem -out key-pkcs8.pem",
|
||||
tag
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OAuthConfig {
|
||||
pub host: String,
|
||||
pub client_id: String,
|
||||
@@ -440,6 +514,127 @@ impl fmt::Debug for ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "native-tls"))]
|
||||
mod native_tls_tests {
|
||||
use super::convert_key_to_pkcs8_pem;
|
||||
|
||||
const PKCS8_RSA_KEY: &str = "\
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzmc7SPaB07+gK
|
||||
p/VETtmayzNASEaaGIEKO5eOrlrKfIMdmSyB38Ia4dCyxb8WKAROeBtaev8AXfKh
|
||||
5sPRPi42SNF77d171QWKU4WKXRWWpO2UWwzrXc/BaCNr0Ey9S4ej3v0DmdPkW+pY
|
||||
PZ5lzHUT6wTutGwJsceCN+/lgPmazaZvfnTdLlRChLyPFjwGkGFwmu7YZACdbIqd
|
||||
CW5oIs+weAK2L9+HZaLi0TWcnJrnONs00GHTm2DwLkl8ulKRn6+XygWZLpY4owE6
|
||||
/x4v4CrTKStvfJlPiErcR/t13vZ+kZq3XxcgIY5V0HEz+AT57/MvvGomQXyMmQnN
|
||||
bQrmdbvRAgMBAAECggEABd6knqSmuDeogeOmHYzFqhxSwRqzmwDqw7i5ne9HFpfN
|
||||
6bNaO+biF0KS0Wrq0Tiu1lkwzKrZJ2wKnF24P+AMPtgvp4EZRF0sjI4o/sIGzPj4
|
||||
LklxAUa8Pd4PTUxeG7Aqn1GP+5SFEzEpVieyY/GUEHn1e+8v2jvCEViOabXmwndA
|
||||
tGHScHwMA0qW1smCOLj12mfFJJjJjlj7QveJFSJ94wo8XjdZvzLDonQoAf4vN99f
|
||||
rf40zgAx8VQk1UmOPYgc7mqN3JerjWX3rG820UWilLnAA35rrVxjzeV9yfAbgpVC
|
||||
qHPaOaN4tBVTb3z9w94wK/uDjd4Px26Qq0Yipi4xGQKBgQD7RXE6hNlx1hW7pdWw
|
||||
hrxdH9ofptBzJfQwYnO0cq6KFF6+QQYzJcMAZAKEytDzfjkH1iHURtBFA1ndKBSI
|
||||
B1G8iR7Jie4szDghJ+zS9QHHhXLRprSqyamRefnLe/614YlaP7i9wq/sHg8KKm5b
|
||||
g+S49EfuKctAenMx8sDPDG7Y2QKBgQC2+xOZTE5uPPitOyR8eMUF1ye9zeONzyJ2
|
||||
l2rjEZuIOWd12wsrgGVTRXoecpqFNIxcmK2mnBtrWCJ/A5xFXjQjIZ1huJ/oNm+t
|
||||
bI/GtlM5BknfEP6m8NVtMx5ubzqDIlsgRpXCWco3ytaKZJtdnJhZAOmBogAKUiPr
|
||||
NL2afWbfuQKBgA722MJybPzBkjPFsY88xvUI05W0+o8RAJTtGBT0C37/rZDJEJR1
|
||||
OELKsfe0mHMX3k+gKg2ZVgf2JiDspSRgwzZmOCYqh7u1QdZ/qTP7EWsPgIIJ2pKd
|
||||
RfL6/6xlqwqr7uEoEFmEwbNfKughFMdweGunaK0/YfJqGHguC9uI0wUpAoGBAKDM
|
||||
U5TLscC+Y+oXpyCbIMjZIltxqx7bf/Wnao7Q0lUL3Rd8vnwkAOcEjyRiodedLhvR
|
||||
MAjR9maGtQnZKmLrPfYBfER78komTE2isVZ2svvRwuj8Dky2J1gnK/7wUAMdFedv
|
||||
H/wC2+nbnl1PvBivnFHas1jv/AkV1erEFjrFKLpBAoGABQ29M3xsVv7++/ivl0JM
|
||||
lNzUZwWbG4GI9Yx/zBMJpjiPi9Bkbe2Iu7EgLuiaTnktLSzm9hwVEIxd6a6X/wRA
|
||||
cGr4L/FDExxjHQPzYpeGQ5fA9HlDWZzQ7Ou+nZ/O+OwXuvnmjXTXyB0djVJHOmLU
|
||||
gOfZqLKYsL5zn6pefvXu0dU=
|
||||
-----END PRIVATE KEY-----";
|
||||
|
||||
const PKCS1_RSA_KEY: &str = "\
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAs5nO0j2gdO/oCqf1RE7ZmsszQEhGmhiBCjuXjq5aynyDHZks
|
||||
gd/CGuHQssW/FigETngbWnr/AF3yoebD0T4uNkjRe+3de9UFilOFil0VlqTtlFsM
|
||||
613PwWgja9BMvUuHo979A5nT5FvqWD2eZcx1E+sE7rRsCbHHgjfv5YD5ms2mb350
|
||||
3S5UQoS8jxY8BpBhcJru2GQAnWyKnQluaCLPsHgCti/fh2Wi4tE1nJya5zjbNNBh
|
||||
05tg8C5JfLpSkZ+vl8oFmS6WOKMBOv8eL+Aq0ykrb3yZT4hK3Ef7dd72fpGat18X
|
||||
ICGOVdBxM/gE+e/zL7xqJkF8jJkJzW0K5nW70QIDAQABAoIBAAXepJ6kprg3qIHj
|
||||
ph2MxaocUsEas5sA6sO4uZ3vRxaXzemzWjvm4hdCktFq6tE4rtZZMMyq2SdsCpxd
|
||||
uD/gDD7YL6eBGURdLIyOKP7CBsz4+C5JcQFGvD3eD01MXhuwKp9Rj/uUhRMxKVYn
|
||||
smPxlBB59XvvL9o7whFYjmm15sJ3QLRh0nB8DANKltbJgji49dpnxSSYyY5Y+0L3
|
||||
iRUifeMKPF43Wb8yw6J0KAH+LzffX63+NM4AMfFUJNVJjj2IHO5qjdyXq41l96xv
|
||||
NtFFopS5wAN+a61cY83lfcnwG4KVQqhz2jmjeLQVU298/cPeMCv7g43eD8dukKtG
|
||||
IqYuMRkCgYEA+0VxOoTZcdYVu6XVsIa8XR/aH6bQcyX0MGJztHKuihRevkEGMyXD
|
||||
AGQChMrQ8345B9Yh1EbQRQNZ3SgUiAdRvIkeyYnuLMw4ISfs0vUBx4Vy0aa0qsmp
|
||||
kXn5y3v+teGJWj+4vcKv7B4PCipuW4PkuPRH7inLQHpzMfLAzwxu2NkCgYEAtvsT
|
||||
mUxObjz4rTskfHjFBdcnvc3jjc8idpdq4xGbiDlnddsLK4BlU0V6HnKahTSMXJit
|
||||
ppwba1gifwOcRV40IyGdYbif6DZvrWyPxrZTOQZJ3xD+pvDVbTMebm86gyJbIEaV
|
||||
wlnKN8rWimSbXZyYWQDpgaIAClIj6zS9mn1m37kCgYAO9tjCcmz8wZIzxbGPPMb1
|
||||
CNOVtPqPEQCU7RgU9At+/62QyRCUdThCyrH3tJhzF95PoCoNmVYH9iYg7KUkYMM2
|
||||
ZjgmKoe7tUHWf6kz+xFrD4CCCdqSnUXy+v+sZasKq+7hKBBZhMGzXyroIRTHcHhr
|
||||
p2itP2Hyahh4LgvbiNMFKQKBgQCgzFOUy7HAvmPqF6cgmyDI2SJbcase23/1p2qO
|
||||
0NJVC90XfL58JADnBI8kYqHXnS4b0TAI0fZmhrUJ2Spi6z32AXxEe/JKJkxNorFW
|
||||
drL70cLo/A5MtidYJyv+8FADHRXnbx/8Atvp255dT7wYr5xR2rNY7/wJFdXqxBY6
|
||||
xSi6QQKBgAUNvTN8bFb+/vv4r5dCTJTc1GcFmxuBiPWMf8wTCaY4j4vQZG3tiLux
|
||||
IC7omk55LS0s5vYcFRCMXemul/8EQHBq+C/xQxMcYx0D82KXhkOXwPR5Q1mc0Ozr
|
||||
vp2fzvjsF7r55o1018gdHY1SRzpi1IDn2aiymLC+c5+qXn717tHV
|
||||
-----END RSA PRIVATE KEY-----";
|
||||
|
||||
const SEC1_EC_KEY: &str = "\
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIIQyP9q8dmPTyzxgCAF22fS5w6GJP7lVrkq3gdG216TtoAoGCCqGSM49
|
||||
AwEHoUQDQgAEAxC2ZMkrbwEImb3S2BD8qA9rquFbozW6d7VrhVk1tOqnIuM+vXkz
|
||||
ShGoCNbfNS+COlPMRAujyDlATZcLs9p4tA==
|
||||
-----END EC PRIVATE KEY-----";
|
||||
|
||||
#[test]
|
||||
fn test_pkcs8_key_passthrough() {
|
||||
let result = convert_key_to_pkcs8_pem(PKCS8_RSA_KEY).unwrap();
|
||||
assert_eq!(result, PKCS8_RSA_KEY.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pkcs1_rsa_key_conversion() {
|
||||
let result = convert_key_to_pkcs8_pem(PKCS1_RSA_KEY).unwrap();
|
||||
assert!(result.contains("-----BEGIN PRIVATE KEY-----"));
|
||||
assert!(result.contains("-----END PRIVATE KEY-----"));
|
||||
assert!(!result.contains("RSA PRIVATE KEY"));
|
||||
// Verify the result is valid PKCS#8 by re-parsing
|
||||
let re_parsed = pem::parse(&result).unwrap();
|
||||
assert_eq!(re_parsed.tag(), "PRIVATE KEY");
|
||||
use pkcs8::der::Decode;
|
||||
pkcs8::PrivateKeyInfo::from_der(re_parsed.contents()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sec1_ec_key_conversion() {
|
||||
let result = convert_key_to_pkcs8_pem(SEC1_EC_KEY).unwrap();
|
||||
assert!(result.contains("-----BEGIN PRIVATE KEY-----"));
|
||||
assert!(result.contains("-----END PRIVATE KEY-----"));
|
||||
assert!(!result.contains("EC PRIVATE KEY"));
|
||||
// Verify the result is valid PKCS#8 by re-parsing
|
||||
let re_parsed = pem::parse(&result).unwrap();
|
||||
assert_eq!(re_parsed.tag(), "PRIVATE KEY");
|
||||
use pkcs8::der::Decode;
|
||||
pkcs8::PrivateKeyInfo::from_der(re_parsed.contents()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unsupported_key_format() {
|
||||
let bad_pem =
|
||||
"-----BEGIN DSA PRIVATE KEY-----\nMIIBuwIBAAKB\n-----END DSA PRIVATE KEY-----";
|
||||
let result = convert_key_to_pkcs8_pem(bad_pem);
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("Unsupported key format"),);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_pem() {
|
||||
let result = convert_key_to_pkcs8_pem("not a pem");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user