Repository navigation
Conversation
The custom OpenSSL build exists so blasthttp can reach servers that only speak deprecated ciphers, but nothing was reaching them. `SslConnector:: builder` installs its own cipher list, `DEFAULT:!aNULL:!eNULL:!MD5:!3DES: !DES:!RC4:!IDEA:!SEED:...`, and we never replaced it, so RC4, DES, 3DES and SEED never made it into the ClientHello. A server speaking only one of them was unreachable unless the caller passed `cipher_string` by hand. `set_security_level(0)` looked like it covered this and does not. The security level decides how weak a negotiated cipher may be; the cipher list decides which ones are offered at all. Set `ALL` when the caller names nothing, on both the pooled path and the `connect_stream` path used by `raw_connect`, `resolve_ip` and `request_target`, which build their SSL contexts separately. Null-encryption suites stay out of the default. They remain available through an explicit `cipher_string`, but negotiating one by accident would return a connection that looks like TLS and encrypts nothing. tests/legacy_default.rs covers this end to end: each test stands up a real TLS server pinned to one legacy cipher or protocol version and connects with a client that sets no TLS options at all. All five cipher cases failed before this change. Side effect worth knowing: the ClientHello grows from 31 cipher suites to 105, so the client's JA3/JA4 fingerprint changes. README dropped its claims about export ciphers and SSLv3. OpenSSL removed export ciphers in 1.1.0, and SSLv3 is disabled in our build despite `enable-ssl3` being passed, so neither has ever worked.
SSLv3 has never been reachable in any build, despite the README advertising it and the build script asking for it. Three separate things were in the way. The build. `scripts/build-openssl.sh` passed `enable-ssl3`, but OpenSSL's Configure carries a disable cascade that reads "if ssl3-method is off, turn ssl3 off too", and ssl3-method is off by default. So the flag was undone during configure. `configdata.pm` recorded both `enable-ssl3` and `no-ssl3`, and every shipped build came out with OPENSSL_NO_SSL3 defined and no SSLv3_method symbols in libssl.a. Passing `enable-ssl3-method` alongside it fixes this. The SSL options. `SslConnector::builder` sets NO_SSLV3 and nothing cleared it, so even a build with SSLv3 compiled in would refuse to negotiate it. Cleared on both the pooled path and `connect_stream`. The spelling. `parse_tls_version` understood 1.0 through 1.3 and nothing else, so there was no way to name SSLv3 at all. It now takes `3.0`, `ssl3` and `sslv3`, and the error message mentions it. An SSLv3-only server is now reachable without asking, matching how the legacy ciphers behave after the previous commit, and can also be pinned explicitly. The test server clears NO_SSLV3 too so a test can pin it. Separately, the build cache was keyed only on the target, so changing what the build asks for was silently ignored and a stale install stayed in place. It now keys on the version and feature flags as well, which is what makes this fix reach anyone with an existing checkout. Upgrading triggers one rebuild. Measured: JA4 is unchanged, `supported_versions` gains 0300, and google, github, cloudflare, amazon, reddit, wikipedia, microsoft and apple all still return 200.
en0f
left a comment
There was a problem hiding this comment.
Turning on verify_certs no longer protects against an attacker in the middle (src/client/hyper.rs, DEFAULT_CIPHER_LIST = "ALL")
ALL includes the anonymous (aNULL) suites: ADH and AECDH. These are now offered on every connection, including ones where verify_certs=True / --verify is set.
An anonymous suite sends no certificate, so the certificate check never runs. OpenSSL's documentation says verify-peer is ignored in this case. The hostname check set with param.set_host() / set_ip() only runs during that certificate check, so it's skipped too, and the handshake reports success.
Failure scenario: a user sets verify_certs=True and requests https://bank.example. An attacker in the path answers with only ADH-AES128-SHA. The client offers that suite, the handshake completes with no certificate, the verify result is OK, and the request goes to the attacker. I reproduced this: s_client with -cipher ALL:@SECLEVEL=0 -verify_return_error -verify_hostname example.com, connected to an ADH-only server, printed Verification: OK / Verify return code: 0 (ok).
The code comment's reasoning only holds while verification is off. When it's on, this switches it off without telling the user.
Fix: drop the anonymous suites from the default whenever verification is on, for example if config.should_verify_certs() { "ALL:!aNULL" } else { "ALL" }. Apply that on both TLS paths: the pooled builder and connect_stream. The ADH test still passes because it runs with verification off. A good regression test would be an ADH-only server with verify_certs=Some(true), asserting the request fails.
`ALL` includes the aNULL suites, which carry no certificate. OpenSSL ignores verify-peer when no certificate arrives, and the hostname check rides along with the certificate check, so a caller who set verify_certs got a handshake that reported success while checking nothing. Anyone in the path could answer with an anonymous suite and be believed. The reasoning in the old comment was about the default rather than the setting: verification is off by default, so the anonymous suites gave up nothing that was being enforced. That holds right up until somebody turns it on, which is the case that matters. Append `!aNULL` to whichever list is in effect when verification is on, including one the caller named. verify_certs and cipher_string are in conflict there and this resolves it toward checking identities, since the alternative is skipping the check without saying so. Reaching anonymous suites is still a matter of leaving verification off. Both TLS paths get it, the pooled builder and connect_stream, which build their contexts separately. Nothing changes while verification is off: the ClientHello still carries the same 105 suites. With it on, the 17 anonymous ones are gone and 88 remain. PSK and SRP are the only other cert-less suites and both need a secret this client never configures, so neither can complete. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both sides appended tests to the same module, nothing semantic. The two TLS context builders still both go through verified_cipher_list, so moving the target handshake inside the proxy tunnel did not add a third path that could miss it.
The custom OpenSSL build exists so blasthttp can reach servers that only speak deprecated ciphers and protocols. It turns out almost none of that was reaching them.
Legacy ciphers were never offered
SslConnector::builderinstalls its own cipher list,DEFAULT:!aNULL:!eNULL:!MD5:!3DES:!DES:!RC4:!IDEA:!SEED:..., and we never replaced it. RC4, DES, 3DES and SEED never made it into the ClientHello, so a server speaking only one of them was unreachable unless the caller passedcipher_stringby hand.set_security_level(0)looks like it covers this and does not: the security level decides how weak a negotiated cipher may be, the cipher list decides which ones are offered at all.Now sets
ALLwhen the caller names nothing. Null-encryption suites stay out of the default: still reachable through an explicitcipher_string, but negotiating one by accident would hand back a connection that looks like TLS and encrypts nothing.SSLv3 has never worked in any build
Three things were in the way, and all three had to go.
The build.
scripts/build-openssl.shpassedenable-ssl3, but OpenSSL's Configure carries a disable cascade reading "ifssl3-methodis off, turnssl3off too", andssl3-methodis off by default. The flag was undone during configure.configdata.pmrecorded bothenable-ssl3andno-ssl3, and every shipped build hadOPENSSL_NO_SSL3defined with noSSLv3_methodsymbols inlibssl.a. Fixed by also passingenable-ssl3-method.The SSL options.
SslConnector::buildersetsNO_SSLV3and nothing cleared it, so even a correct build would refuse to negotiate.The spelling.
parse_tls_versionknew 1.0 through 1.3 and nothing else, so SSLv3 could not be named at all. It now accepts3.0,ssl3andsslv3.Build cache
The cache was keyed only on the target, so changing what the build asks for was silently ignored and a stale install stayed in place. It now keys on the OpenSSL version and feature flags too, which is what makes the SSLv3 fix reach existing checkouts. Upgrading triggers one rebuild.
Fingerprint impact, worth knowing before merging
The ClientHello goes from 31 cipher suites to 105 and
supported_versionsgains0300. JA4 changes from the cipher widening (t13d3112h2_...tot13d10512h2_...) and is unaffected by the SSLv3 part.Measured against tlsfingerprint.io: the old ClientHello had been observed 903 times across roughly 17B connections, the new one does not appear at all. No behavior change showed up in practice. akamai.com was 403 before and after, and cloudflare, google, github, wikipedia, amazon, reddit, microsoft and apple all still return 200.
Also
Export ciphers are the one thing the build genuinely cannot provide. OpenSSL removed them in 1.1.0 and no flag brings them back. The README claim is gone.
tests/legacy_default.rsstands up a real TLS server per case, pinned to one legacy cipher or protocol version, and connects with a client that sets no TLS options at all. Every cipher case and the SSLv3 case failed before these changes.