Repository navigation
Add TLS client connections over Tcp and trust options for Tcp and Http - #552
Draft
Karakatiza666 wants to merge 1 commit into
Draft
Karakatiza666 wants to merge 1 commit into
Karakatiza666 wants to merge 1 commit into
Conversation
Tcp.connect_tls! opens a TLS client connection and returns the existing Tcp.Connection, so reads, writes, timeouts, line limits, chunks and lines work as for Tcp.connect!. The handshake runs under connect_timeout and is interruptible; decrypted data is taken before any wait, so a timed-out read still loses nothing. The new Tls module holds the shared settings. Tls.Options chooses which server certificates are accepted: the bundled WebPKI roots (the default, unchanged), the bundled roots plus certificates from PEM, or DangerAcceptInvalidCerts. Http.SendOptions and Http.StreamingOptions take the same tls field, applied on every redirect hop. A failed handshake is TlsErr(Tls.Err), naming an unknown issuer, an expired certificate, a name mismatch, unusable PEM (reported before any network activity) or another cause. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A basic-cli app cannot open a TLS connection over
Tcp, and HTTPS only trusts the bundled WebPKI roots. Consider a CLI client for a self-hosted service that uses HTTPS andwss://. It cannot run a WebSocket client over TLS, and it cannot trust the service's private CA. During development it also cannot accept the service's self-signed certificate. Other TLS protocols overTcphit the same limit: Redis, Postgres and SMTP over TLS.This PR adds:
Tcp.connect_tls!, which opens a TLS client connection and returns the existingTcp.Connection.Tlsmodule with trust options shared byTcpandHttp:Default: the bundled WebPKI roots, unchanged.AddPem(bytes): the bundled roots plus certificates from PEM, like curl's--cacert.DangerAcceptInvalidCerts: accept any certificate, like curl's--insecure.tlsfield onHttp.SendOptionsandHttp.StreamingOptions.TlsErr(Tls.Err): a handshake failure names its cause:UnknownIssuer,Expired,NameMismatch,BadPem(Str)orOther(Str).Related to #454. This PR does not change the default trust source (OS store vs. bundled roots), which #454 is still researching. It only adds explicit, app-chosen trust on top of the current default, and any outcome of #454 would change only what
Defaultmeans. It also covers the TLS part of #438 ("map TLS failures intentionally"). #331 was an earlier attempt at TLS overTcp; it was closed without review.Design
Roc API
Why this shape:
connect_tls!returns the existingTcp.Connection. All of its operations work unchanged over TLS: reads, writes,with_timeout, line limits,chunksandlines. So does every package already written againstTcp.Connection, such as a pure-Roc WebSocket client, which getswss://for free.tlsfield onconnect!. A TLS connect has a different failure set. AddingTlsErrtoconnect!'s error row would break every exhaustive match on it.Tlsmodule serves bothTcpandHttp. It holds one vocabulary for trust and for errors.Httpdoes not importTcpjust to name a type, and future TLS settings (client certificates, ALPN) have an obvious home. The module follows the nested nominal style ofSqlite.Err/Sqlite.OpenOptions, andErrhasis_eqandto_stras CONTRIBUTING asks.SendOptions/StreamingOptions. It does not go on the request, becauseRequest(and sotimeout) belongs to the externalhttppackage. These two records already carry the platform-owned per-call transport policy (max_redirects), and they are what reachessend_redirected!. The trust is passed to the host on every hop, so redirects use it too. That includes cross-origin hops: the next host is verified against the same trust.DangerAcceptInvalidCerts, and it is never a default. Even then, the handshake signatures are still verified withring, so the server must hold the key for the certificate it presents.{}keeps today's behaviour.BadPemis reported before any DNS lookup, connection or I/O, even forhttp://URLs, so a broken CA file is never hidden.TlsErrsits insideHttpErr(...)and is not a new outer tag. The result rows ofsend!,get!,send_streaming!and the others are unchanged.Host
src/tls.rs(new) holds the TLS code:rustls::ClientConfigper trust. The default and insecure configs are built once. PEM configs are cached in a small cache keyed by the PEM bytes.Tls.Err, including the…Contextvariants.Tcpchanges stay inside the existing connection type.BufferedSocketgets an optionalrustls::ClientConnection, so there is no new resource kind.close_notifywithout waiting.src/http.rschange is small.https_connectortakes the config (with_tls_config, with the samehttps_or_http/http1features).hosted_http_send_streamingtakes the trust as one new argument.Failure::Tls.rustlsmoves from dev-dependencies to dependencies, andwebpki-rootsbecomes a direct dependency. Both were already linked throughhyper-rustls.THIRD_PARTY_LICENSES.mdis unchanged.Compatibility
Http.TransportErrexhaustively, without a_arm, needs aTlsErrarm. The module has added variants before (RedirectLimitExceeded…InsecureRedirect), and no example or test matches it exhaustively.ClientConfig, so TLS session resumption can happen across requests. Configs are per trust, so a session never resumes across trusts.Questions for review
close_notifyis reported asTcpErr(Other(…))rather than end of input, because data may have been truncated. This matches rustls. Some servers close TCP withoutclose_notify. Is strict the right default, or should it be a distinct, matchable signal?OnlyPem(bytes), which replaces the bundled roots instead of adding to them? It is easy to add later.DangerAcceptInvalidCertscarries across cross-origin redirects, as curl's-kdoes. Is that the behaviour we want?NameMismatch.Verification
Tests:
tests/tlsfixtures. The fixtures add PEM forms and an unrelated key. Norcgenis needed: "expired" uses a fixed clock in 2040.AddPem, rejected byDefault(UnknownIssuer).NameMismatchfor127.0.0.1,Expired, and accepted byDangerAcceptInvalidCerts.DangerAcceptInvalidCertsstill rejects a server whose key does not match its certificate.BadPemis reported before connecting.Interrupted.close_notify; dropping a connection sendsclose_notify.send_streaming_atand the hosted function.examples/tcp-tls.rocandtests/tls-trust.rocrun against a new stdlib-only helper,scripts/tls_test_server.py: TLS echo on 8443 and HTTPS on 9443, with no internet access.BadPemagainst a closed port (no I/O first), and reads and lines over TLS.Commands, all passing:
cargo fmt --all -- --checkcargo clippy --locked --all-targets -- -D warnings. It also passes for--lib --testsonx86_64-pc-windows-gnuusingzig cc, to type-check the Windows poll path.cargo test --locked --all-targets: 224 passed on Linux../ci/regenerate_glue.sh --checkroc checkandroc test platform/main.roc: 764 tests passed.python3 scripts/third_party_notices.py --checkROC=/path/to/nightly-2026-11-06-34f137a/roc python3 scripts/test.py: the full native Linux suite passed.scripts/build.py --target arm64muslbuilt.🤖 Generated with Claude Code