Skip to main content

Module tls

Module tls 

Source
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

ReachingConfigured withNot with
https://Builder::tls — a CA, a client certificateany Credential::ssh_*
ssh://, git@host:repoCredential::ssh_agent, ssh_key, ssh_commandtlsssh has its own trust model, in known_hosts
file://, a pathnothingeither

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 a configure_request closure, and reqwest::blocking::Request has a timeout_mut, so a deadline could be installed without touching gix at 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 into RejectConfiguredHeaders — every source would silently stop following the redirect git itself follows, in exchange for a timeout.
  • Using this transport for every https:// source. What gix’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 cost http.extraHeader and 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 that gix is 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.