Repository navigation
Conversation
| /// | ||
| pub fn verify_tls(config: Configuration, which: Bool) -> Configuration { | ||
| Builder(..config, verify_tls: which) | ||
| pub fn verify_tls( |
There was a problem hiding this comment.
It seems advantageous to do this without a breaking change if possible. Right now this method flags a single keyword to ssl, which is sort of orthongal to the rest of what is here.
There was a problem hiding this comment.
this is indeed a breaking change :/
i tried to get my head around what was discussed here: #36 (comment):
I don't think it would make sense to say "do not verify TLS" and "here's a cert to verify TLS with" at the same time, so instead we make a custom type with variants for not verifying, verifying with system certs, and verifying with custom certs. Do you think that would cover all the possibilities?
@lpil can we get your input on this?
There was a problem hiding this comment.
There are many options in a full-featured TLS stack that are not included in gleam_httpc today. The reason you see stacks split them is that there is potentially interest in configuring with this CA, but only verifying the chain, not the hostname. The axes become "how much to verify in this connection" and "how to verify the connection." On further thought, there is more benefit to making it harder to misuse.
| ssl_verify_host_options(True) | ||
| |> list.map(fn(opt) { | ||
| case opt { | ||
| Cacerts(_) -> Cacertfile(charlist.from_string(path)) |
There was a problem hiding this comment.
This seems like a footgun because, while {cacerts, CACerts} is in the default options, if for some reason it wasn't or that ever changed, this wouldn't get added. It should probably be two steps: first, get the default options; if {cacertfile, Path} is needed, the cacerts key is removed and replaced.
There was a problem hiding this comment.
done. i transformed this part by first filtering the list, then prepending the option. still need to traverse the list, but i don't see another solution
jtdowney
left a comment
There was a problem hiding this comment.
I am not affiliated with the Gleam project in any way; I am just a Gleam user with 15 years of reviewing TLS-related code. So feel free to ignore me.
The other thing I'd add is that there isn't much in the way of tests for the client certificate code, though it is difficult/tedious to test these things. You basically have to generate a mini-PKI and stand up a test server.
| let verify_opts = verification_options(config.tls_verification) | ||
|
|
||
| case config.tls_verification, cert_opts { | ||
| VerifyWithSystemCerts, [] -> option.None |
There was a problem hiding this comment.
Seems odd to resolve verify_opts outside of the case if it is only needed in one branch.
|
@lpil Any updates on this wll be super helpful. TIA! |
Client certificate and custom CA paths are the first user-supplied ssl
options, so a wrong path or password now surfaces as
InvalidTlsOptions rather than an unexpected_httpc_ip_error crash.
Remove two unreachable clauses: ssl never produces a bare
{tls_alert, Atom}, and httpc only emits the 3-tuple {ssl_error, Socket,
Reason} form. Document why that form is mapped to FailedToConnect
(TLS 1.3 rejects a missing client certificate after the handshake).
With TLS 1.3 a server rejects a missing or invalid client certificate after the handshake completes, so the alert and the TCP close race in httpc: sometimes the request fails with an ssl_error, sometimes with socket_closed_remotely. The latter previously crashed the caller.
Relying on httpc's default when no client certificate is configured meant VerifyWithSystemCerts only verified on OTP 26+, while the other variants verified on any supported version. Building the option list unconditionally makes the behaviour independent of the OTP default.
VerifyPeer and CustomizeHostnameCheck are never built or matched by this code (they come from httpc:ssl_verify_host_options/1), and the Erlang wrapper only forwarded to that function. Cacerts holds OTP cert records, not binaries.
ssl_config:file_error/2 only wraps badmatch failures in {options, _};
other failures, such as reading an encrypted key without a password,
surface as a bare {keyfile, Reason} and previously crashed the caller.
A throwaway PKI is generated at test start and served by two ssl listeners, one of which requires a client certificate. This covers custom CA verification, mTLS, encrypted keys, and misconfigured paths without external services or committed key material.
Deriving them from httpc:ssl_verify_host_options/1 and dropping cacerts still called public_key:cacerts_get/0, which raises when the OS has no CA store, so VerifyWithCustomCa crashed in exactly the environments a custom CA is for. Assemble verify_peer, cacertfile, and the https hostname check directly instead.
erlang:unique_integer/1 is only unique within one VM and repeats across runs, so a second gleam test could fail on eexist before any test ran.
The custom CA options are now assembled by hand, so check that the hostname is still verified. A TLS 1.2 only listener shows the rejection of a missing client certificate as a plain handshake failure, in contrast to the post-handshake behaviour of TLS 1.3.
The public_key cacerts_path application env is pointed at a missing file for the duration of the request, which is how the OS store lookup fails on every platform, and restored afterwards.
The public_key cacerts_path override only exists from OTP 27.2, so on older supported versions the helper reports that and the test passes without asserting. The probe now runs inside try/after so a failure there also restores the environment.
Fixes #54
Closes #36
Adds TLS verification options and mutual TLS (client certificate) support to
Configuration.API
verify_tlsnow takes aTlsVerificationinstead of aBool, following the design proposed in #36:NoVerificationVerifyWithSystemCerts(default)VerifyWithCustomCa(path)— verify against a PEM CA file instead of the system storeClient certificates:
client_certificate(certfile:, keyfile:)client_certificate_with_password(certfile:, keyfile:, password:)Both build on the same
ssloption list, and verification options are always passed explicitly so behaviour does not depend on the OTP default.Errors
ConnectError.InvalidTlsOptions(detail)is returned when ssl rejects the options (missing cert/key/CA file, wrong key password) instead of crashing the caller.HttpError.ConnectionClosedis returned when the server closes the connection before responding. With TLS 1.3 a server rejects a client certificate after the handshake, so the alert and the close race, which previously crashed about half the time.Tests
A throwaway PKI is generated at test start with
public_key:pkix_test_data/1and served by two localssllisteners, one requiring a client certificate. Covers custom CA verification, mTLS, encrypted keys, and misconfigured paths with no external services or committed key material.Requirements
Erlang/OTP 25.1 or later (
httpc:ssl_verify_host_options/1).