Expand description
HTTPS to a host this machine does not already trust.
An enterprise GitLab behind a private certificate authority, or a host that
wants a client certificate before it will say hello, is an ordinary
deployment and this crate could not reach either one. The vocabulary is
TlsConfig, which is dynamic-config-store-core’s and shared with every
other store crate; what is here is the part that is git’s alone, which is
where that configuration has to be put.
§What gix allows, measured
gix is configured here with blocking-http-transport-reqwest-rust-tls —
pure Rust, no C toolchain and no OpenSSL question, which is why this
workspace has no C dependency. Its HTTP options type,
gix::protocol::transport::client::blocking_io::http::Options, has the
two fields this needs — ssl_ca_info and ssl_verify — and gix even maps
http.sslCAInfo and GIT_SSL_CAINFO onto them. Only the curl backend
reads them. The reqwest backend clones that options struct and uses
three of its fields — the extra headers, the redirect policy and its own
backend hook — and never looks at either of the SSL ones. Its
reqwest::blocking::Client is built once, inside a worker thread, from a
ClientBuilder with no root store, no identity and no hook to reach either.
Its one extension point, configure_request, configures a request: a
request has a URL, a method, headers and a body, and no TLS anywhere in it.
So setting http.sslCAInfo through this crate would do nothing at all, and
a store that silently ignores “trust this certificate authority” is a
program that believes it is pinned to a private CA and is not. The C
transport would read it, and adding a C TLS stack to a workspace that has
none — for a crate whose whole argument is that it needs no toolchain — is
the wrong trade.
§What is here instead
gix takes a transport of the caller’s own — that is what
Remote::to_connection_with_transport is for — and its HTTP transport is
generic over a small Http trait: three methods over GET, POST and a
backend hook. So the git half stays gix’s entirely, including the
handshake, the protocol version, the credential header and the packet
framing; only the seven lines that build an HTTP client are ours, and those
are the seven lines a root store and an identity go into.
It is used only when a TlsConfig was configured. A source that asks
for nothing new keeps gix’s own transport, byte for byte, and nothing on
this page can have broken it.
§Which knob applies to which transport
| Reaching | Configured with | Not with |
|---|---|---|
https:// | Builder::tls — a CA, a client certificate | any Credential::ssh_* |
ssh://, git@host:repo | Credential::ssh_agent, ssh_key, ssh_command | tls — ssh has its own trust model, in known_hosts |
file://, a path | nothing | either |
They are refused rather than ignored: a tls on an ssh:// url fails at
Builder::build, where the mistake was made.
§The deadline this transport keeps, and the one gix’s cannot
with_timeout is a bound on one fetch, and
most of a fetch is bounded by gix’s interrupt flag, which it checks
between packets while negotiating and while receiving a pack. What an
interrupt flag cannot bound is a host that accepts the connection and then
sends nothing: there are no packets for the check to be between. On this
transport the client is ours, so the caller’s number is the connect deadline
and the stall deadline on every read. On gix’s it is neither.
Closing that on gix’s transport is an upstream change, and a small one:
gix_transport’s HTTP options already carry a connect_timeout, populated
from gitoxide.http.connectTimeout, and its reqwest backend never reads
it — the same way it never reads ssl_ca_info. Only curl does. Measured
against gix-transport 0.58.1, where the backend hardcodes twenty seconds.
Two ways to close it from here were measured and both were refused:
- Through the backend hook.
gix’s reqwest backend takes aconfigure_requestclosure, andreqwest::blocking::Requesthas atimeout_mut, so a deadline could be installed without touchinggixat all. Installing the hook is also what makes that backend treat the request as one whose headers must not be replayed, which turns its redirect policy intoRejectConfiguredHeaders— every source would silently stop following the redirectgititself follows, in exchange for a timeout. - Using this transport for every
https://source. Whatgix’s reqwest backend reads out of the options it is handed is three fields — the extra headers, the redirect policy and this hook — so the swap would costhttp.extraHeaderand redirect following and nothing else, which is less than it sounds. It is still the wrong trade: it puts every caller, including every caller whose host answers in milliseconds and who never asked for TLS, on this crate’s HTTP client to bound a stall thatgixis one field away from bounding itself.
So the honest thing is to say which transport bounds what, which
with_timeout does in a table.
§It follows no redirect, and that is not the obvious reason
The obvious reason is that every request here carries an Authorization
header and a redirect is a stranger’s opportunity to be handed it — and it
is not quite the true one: reqwest removes Authorization itself when a
redirect crosses to another host, port or scheme, so the token would not
travel. The real reason is what a git fetch is.
Smart HTTP is two requests against one base url: a GET of the ref
advertisement and a POST of the negotiation. Following a 301 on the
first leaves the second still addressed to the old url — and a 301 on a
POST is turned into a GET with no body by every HTTP client that obeys
the specification, this one included. Making a redirect work therefore means
rewriting the base url for the rest of the conversation, verifying that what
changed was only the part that may change, and deciding whether the identity
may be reused at the new address. That is gix’s redirect module and the
bookkeeping around it: security-relevant code, and not worth a second copy
for a transport that is only reached by callers who named an unusual host.
Such a host is named by its final url instead, and the error says so.
§There is no way to turn verification off
The reasoning is dynamic_config_store_core::tls’s and it holds here
unchanged: the two situations anybody reaches for it in — a development
server with a self-signed certificate, an enterprise private CA — are both
trusting one more certificate, which is
with_ca_certificate_file and keeps
the server authenticated. Turning verification off does not make TLS weaker
the way a checklist means; it makes it absent.
git gives that a second, sharper edge. A fetch presents a credential — the
Authorization header this crate puts on every request — before it has
received anything. A connection with no verification is one any party on the
path can terminate, and what they get for it is the token. So the knob a
caller would reach for “just to get past a certificate error in staging” is
the knob that hands a personal access token to whoever is in the way, and
there is no name frightening enough to fix that. gix’s own
gitoxide.http.sslNoVerify is not reachable from here either, because the
transport on this page is the one being used and it never reads it.