diff --git a/Cargo.lock b/Cargo.lock index 5d60a3b7..9366951e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -355,6 +355,16 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -919,6 +929,7 @@ dependencies = [ "hyper", "hyper-util", "rustls", + "rustls-native-certs", "tokio", "tokio-rustls", "tower-service", @@ -1403,6 +1414,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "outref" version = "0.5.2" @@ -1697,6 +1714,7 @@ dependencies = [ "pin-project-lite", "quinn", "rustls", + "rustls-native-certs", "rustls-pki-types", "serde", "serde_json", @@ -1796,6 +1814,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" version = "1.15.1" @@ -1844,12 +1874,44 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.229" diff --git a/Cargo.toml b/Cargo.toml index 1d09921a..275a9288 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,11 @@ chrono = { version = "0.4", default-features = false, features = ["clock", "std" base64 = "0.22" getrandom = "0.3" unicode-general-category = "1.1" -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } +# rustls trusting the bundled roots and the platform's certificate store (spec/providers.md §2). +reqwest = { version = "0.12", default-features = false, features = [ + "rustls-tls-webpki-roots", + "rustls-tls-native-roots", +] } url = "2" # init --template: the release's source archive (spec/service.md §2.1). tar = { version = "0.4", default-features = false } diff --git a/crates/grida-fx-providers/src/transport/http.rs b/crates/grida-fx-providers/src/transport/http.rs index 52c9a92a..1c641875 100644 --- a/crates/grida-fx-providers/src/transport/http.rs +++ b/crates/grida-fx-providers/src/transport/http.rs @@ -16,6 +16,10 @@ //! machine, not this one. Every other request is `https` to a remote host: through a proxy it //! is tunnelled (`CONNECT`), so the proxy sees the host and port, and the request stays //! encrypted end to end; +//! - **trust**: a server's certificate is accepted when it chains to the bundled Mozilla roots or +//! to a root the platform's certificate store trusts (the macOS keychain, the Windows store, the +//! system bundle elsewhere; `SSL_CERT_FILE` / `SSL_CERT_DIR`, when set, name it instead), read +//! when the clients are built; //! - `redirect::Policy::none()`, no retries (reqwest's own retry of protocol NACKs is turned //! off), `accept-encoding: identity` on every request (the adapter reads the entity as sent), a //! connect timeout ([`HttpConfig::connect_timeout`], 30 s by default) inside the request's diff --git a/spec/providers.md b/spec/providers.md index cb75c137..5be89522 100644 --- a/spec/providers.md +++ b/spec/providers.md @@ -28,9 +28,10 @@ Adapters are thin clients over one injected transport. The transport MUST make * - `after_send`: anything else. A reset, a read timeout, a truncated body and a response over the cap all are `after_send`. - A transport that cannot tell reports `after_send`. 8. **The network can be turned off.** With `GRIDA_FX_NETWORK=off`, a live invocation's transport refuses every exchange before it leaves (`not_sent`, refused). Tests of live paths use it; nothing is sent. -9. **Proxies never see a credential.** A request whose URL is plain `http`, or whose host is a loopback host (`localhost`, a loopback IP), goes straight to its host, whatever the environment's proxy settings say: a proxy would read a plain request, credential included, in clear text, and a loopback host is this machine, not the proxy's. Any other request is `https` to a remote host, and honours the proxy settings of the environment (`HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY`; `HTTP_PROXY` names a proxy for plain `http` only, so it never applies). Through a proxy it is tunnelled (`CONNECT`): the proxy sees the host and port, and the request stays encrypted to the provider. These variables are the transport's; they are not keys or endpoints (§3). +9. **Proxies never see a credential.** A request whose URL is plain `http`, or whose host is a loopback host (`localhost`, a loopback IP), goes straight to its host, whatever the environment's proxy settings say: a proxy would read a plain request, credential included, in clear text, and a loopback host is this machine, not the proxy's. Any other request is `https` to a remote host, and honours the proxy settings of the environment (`HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY`; `HTTP_PROXY` names a proxy for plain `http` only, so it never applies). Through a proxy it is tunnelled (`CONNECT`): the proxy sees the host and port, and the request stays encrypted to the provider, unless the platform trusts the proxy's own certificates (item 10). These variables are the transport's; they are not keys or endpoints (§3). +10. **Certificates chain to a trusted root.** A server's certificate is accepted when it chains to a well-known public root or to a root the platform's certificate store trusts, as the machine's other programs accept it. A proxy that presents certificates under a root the platform trusts can read a tunnelled request, credential included; which roots a machine trusts is its owner's choice, not the transport's. -The default transport is HTTP over rustls. Besides what the request sets, it sends `accept-encoding: identity`, `accept: */*` unless the request sets `accept`, and a `user-agent` on the provider and upload lanes. Tests use a replay transport that plays synthetic exchanges and asserts every request, or one that fails any send. The default transport MUST NOT be constructed in a test. A request, a response or a credential printed for debugging shows no URL path or query, no header value, no body and no key. +The default transport is HTTP over rustls, trusting the bundled Mozilla roots and the platform's certificate store (`SSL_CERT_FILE` / `SSL_CERT_DIR` in its place when set). Besides what the request sets, it sends `accept-encoding: identity`, `accept: */*` unless the request sets `accept`, and a `user-agent` on the provider and upload lanes. Tests use a replay transport that plays synthetic exchanges and asserts every request, or one that fails any send. The default transport MUST NOT be constructed in a test. A request, a response or a credential printed for debugging shows no URL path or query, no header value, no body and no key. ## 3. Keys and endpoints