mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-15 07:36:50 +03:00
The engine verified TLS against the Mozilla roots bundled through webpki-roots only, so behind a TLS-inspecting proxy (Aikido, Zscaler, Netskope) whose root lives in the OS trust store, `impeccable update` and `install` failed with `invalid peer certificate: UnknownIssuer` while curl and npm on the same machine succeeded. crates/context/src/http.rs builds one rustls ClientConfig per process: the OS trust store (rustls-native-certs: Keychain, Windows store, the OpenSSL paths on Linux) merged with the bundled roots. A union, not a replacement, so a container without ca-certificates or a store that fails to load still verifies exactly as before. SSL_CERT_FILE and SSL_CERT_DIR replace the OS store the way they do for OpenSSL and curl. Every HTTPS call site (bundle and signature downloads, /api/version, /api/commands, the roll API, image generation) builds its agent from this module; the plain-HTTP live-server calls on localhost are untouched. Verified against a local HTTPS server signed by a throwaway CA: trusted through SSL_CERT_FILE the update check reaches it; without it the same server is rejected as UnknownIssuer; with SSL_CERT_FILE pointing at that CA or at a missing file, impeccable.style still verifies through the bundled roots. cargo test --workspace and the oracle corpus (832) pass. Written with AI assistance (Claude Code). Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
44e825090e
commit
bd6964c35b
@@ -47,7 +47,7 @@ fn card_base(env: &Env) -> String {
|
||||
}
|
||||
|
||||
fn agent(timeout: Duration) -> ureq::Agent {
|
||||
ureq::AgentBuilder::new().timeout_connect(timeout).timeout(timeout).build()
|
||||
crate::http::agent_builder().timeout_connect(timeout).timeout(timeout).build()
|
||||
}
|
||||
|
||||
/// URLSearchParams serialization (application/x-www-form-urlencoded).
|
||||
|
||||
@@ -468,7 +468,7 @@ fn fetch_latest_skill_version(env: &Env) -> Option<String> {
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "https://impeccable.style".to_string());
|
||||
let host = host.strip_suffix('/').unwrap_or(&host).to_string();
|
||||
let agent = ureq::AgentBuilder::new()
|
||||
let agent = crate::http::agent_builder()
|
||||
.timeout(std::time::Duration::from_millis(FETCH_TIMEOUT_MS))
|
||||
.build();
|
||||
let res = agent.get(&format!("{}/api/version", host)).call().ok()?;
|
||||
|
||||
@@ -294,7 +294,7 @@ pub fn run(args: &[String], io: &mut Io) -> i32 {
|
||||
}
|
||||
}
|
||||
}
|
||||
let agent = ureq::AgentBuilder::new().build();
|
||||
let agent = crate::http::agent_builder().build();
|
||||
let response = if !refs.is_empty() {
|
||||
let boundary = format!("----impeccable{:x}", crate::util::now_ms() as u64);
|
||||
let mut body: Vec<u8> = Vec::new();
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
//! One TLS trust configuration for every HTTPS request the engine makes.
|
||||
//!
|
||||
//! `ureq`'s default rustls config trusts only the Mozilla roots compiled in
|
||||
//! through `webpki-roots`. On a machine where an endpoint security agent
|
||||
//! inspects TLS (Aikido, Zscaler, Netskope: routine in managed corporate
|
||||
//! setups), every connection terminates at a proxy whose root lives in the
|
||||
//! OS trust store and nowhere else, so `update` and `install` failed with
|
||||
//! `invalid peer certificate: UnknownIssuer` while curl and npm on the same
|
||||
//! machine succeeded (#757).
|
||||
//!
|
||||
//! The store built here is the union of the OS trust store
|
||||
//! (`rustls-native-certs`: the macOS Keychain, the Windows store, the
|
||||
//! OpenSSL paths on Linux) and the bundled Mozilla roots. A union, not a
|
||||
//! replacement: a container without `ca-certificates`, or a store that
|
||||
//! fails to load, verifies against the bundled roots exactly as before.
|
||||
//! `SSL_CERT_FILE` / `SSL_CERT_DIR` stand in for the OS store, as they do
|
||||
//! for OpenSSL and curl; the bundled roots stay either way.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use ureq::rustls::pki_types::CertificateDer;
|
||||
use ureq::rustls::{self, ClientConfig, RootCertStore};
|
||||
|
||||
/// `ureq::AgentBuilder::new()` with the engine's trust store installed.
|
||||
/// Every HTTPS call site builds its agent from this; the plain-HTTP calls
|
||||
/// to the live server on localhost do not need it.
|
||||
pub fn agent_builder() -> ureq::AgentBuilder {
|
||||
ureq::AgentBuilder::new().tls_config(tls_config())
|
||||
}
|
||||
|
||||
fn tls_config() -> Arc<ClientConfig> {
|
||||
static CONFIG: Lazy<Arc<ClientConfig>> = Lazy::new(|| {
|
||||
// Mirrors ureq's own default config (provider and protocol versions);
|
||||
// only the root store differs.
|
||||
let config =
|
||||
ClientConfig::builder_with_provider(rustls::crypto::ring::default_provider().into())
|
||||
.with_protocol_versions(&[&rustls::version::TLS12, &rustls::version::TLS13])
|
||||
.expect("the ring provider supports TLS 1.2 and 1.3")
|
||||
.with_root_certificates(root_store(rustls_native_certs::load_native_certs().certs))
|
||||
.with_no_client_auth();
|
||||
Arc::new(config)
|
||||
});
|
||||
CONFIG.clone()
|
||||
}
|
||||
|
||||
/// The bundled Mozilla roots plus every parsable certificate in `native`.
|
||||
/// Unparsable entries are dropped, so one broken certificate in the OS
|
||||
/// store cannot take the bundled roots down with it.
|
||||
fn root_store(native: Vec<CertificateDer<'static>>) -> RootCertStore {
|
||||
let mut store = RootCertStore {
|
||||
roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
|
||||
};
|
||||
store.add_parsable_certificates(native);
|
||||
store
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ureq::rustls::pki_types::pem::PemObject;
|
||||
|
||||
/// Self-signed CA minted for this test (P-256, v3, CA:TRUE): the shape
|
||||
/// of the root a TLS-inspecting proxy installs into the OS store.
|
||||
const PROXY_ROOT_PEM: &str = "-----BEGIN CERTIFICATE-----
|
||||
MIIBdTCCARugAwIBAgIJANhTZvQvv7HJMAoGCCqGSM49BAMCMB0xGzAZBgNVBAMM
|
||||
EmltcGVjY2FibGUgdGVzdCBDQTAgFw0yNjA5MDcwNjQxMjdaGA8yMTI2MDgxNDA2
|
||||
NDEyN1owHTEbMBkGA1UEAwwSaW1wZWNjYWJsZSB0ZXN0IENBMFkwEwYHKoZIzj0C
|
||||
AQYIKoZIzj0DAQcDQgAEYVZtCOXaZsY71/0Roy62iBVcyx8UfMDkPbEbf/IEw5Bm
|
||||
yNBfKTFS/8FbRBMWHXOwNE0Ns1BLVOB1oQ1XFC5Bz6NCMEAwDwYDVR0TAQH/BAUw
|
||||
AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFFONzBxi7ewOfuP6cBIIqsxu
|
||||
3pEiMAoGCCqGSM49BAMCA0gAMEUCIQD98Q0ZRe8ceuopnUwQKYleZd5IzfWhhpmO
|
||||
tB0WGTOG3QIgdJa8gBPU9Y6WsrursItsnUeGTYHKDCZZ6MjlekLFuoc=
|
||||
-----END CERTIFICATE-----
|
||||
";
|
||||
|
||||
fn bundled() -> usize {
|
||||
webpki_roots::TLS_SERVER_ROOTS.len()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_roots_alone_when_the_os_store_is_empty() {
|
||||
assert_eq!(root_store(Vec::new()).len(), bundled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn os_store_root_joins_the_bundled_roots() {
|
||||
let proxy = CertificateDer::from_pem_slice(PROXY_ROOT_PEM.as_bytes()).unwrap();
|
||||
assert_eq!(root_store(vec![proxy]).len(), bundled() + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unparsable_os_store_entry_is_dropped() {
|
||||
let junk = CertificateDer::from(b"not a certificate".to_vec());
|
||||
assert_eq!(root_store(vec![junk]).len(), bundled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_builds_from_this_hosts_store() {
|
||||
// Runs the real rustls-native-certs load: it must not panic, and the
|
||||
// shared config must be accepted by a ureq agent.
|
||||
let _agent = agent_builder().build();
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
pub mod jsp;
|
||||
pub mod util;
|
||||
pub mod url;
|
||||
pub mod http;
|
||||
pub mod provider;
|
||||
pub mod hook_markers;
|
||||
pub mod target_args;
|
||||
|
||||
Reference in New Issue
Block a user