From f5a684f26507dbc4ce8af04afbd48724ab6c031b Mon Sep 17 00:00:00 2001 From: yohimik Date: Sun, 30 Aug 2026 15:36:48 +0400 Subject: [PATCH] net/http: give the HTTPS client trust roots on darwin The client dialled TLS with a nil config, which leaves RootCAs nil, which sends crypto/x509 to the platform verifier on darwin. crypto/x509/internal/macos in TinyGo is a stub, so every HTTPS request to a real server failed verification as soon as the package compiled against the standard library crypto/tls and not against the no-op stub. Give the dial a config from defaultTLSConfig. Off darwin that is still nil, because crypto/x509 finds the system roots in the usual files. On darwin it carries a pool that is read once from $SSL_CERT_FILE, or from the bundle of macOS at /etc/ssl/cert.pem. A non-nil pool makes x509 build the chain in pure Go instead of a call to the stubbed verifier. If no file can be read, the config has no roots, so the result is an ordinary verification error and not a check that is silently skipped. A direct tls.Dial with a nil config still fails on darwin for the same reason. A caller that does not use this package supplies its own RootCAs. --- http/client.go | 4 ++- http/tlsconfig_darwin.go | 58 ++++++++++++++++++++++++++++++++++++++++ http/tlsconfig_other.go | 14 ++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 http/tlsconfig_darwin.go create mode 100644 http/tlsconfig_other.go diff --git a/http/client.go b/http/client.go index 6f6589e..a140e9b 100644 --- a/http/client.go +++ b/http/client.go @@ -293,7 +293,9 @@ func roundTrip(req *Request) (*Response, error) { if missingPort { host = host + ":443" } - conn, err = tls.Dial("tcp", host, nil) + // TINYGO: defaultTLSConfig is nil everywhere but darwin, where it + // TINYGO: carries the trust roots crypto/x509 cannot find for itself. + conn, err = tls.Dial("tcp", host, defaultTLSConfig()) } if err != nil { req.closeBody() diff --git a/http/tlsconfig_darwin.go b/http/tlsconfig_darwin.go new file mode 100644 index 0000000..4611562 --- /dev/null +++ b/http/tlsconfig_darwin.go @@ -0,0 +1,58 @@ +//go:build darwin + +// TINYGO: darwin trust roots for the HTTPS client. + +package http + +import ( + "crypto/tls" + "crypto/x509" + "os" + "sync" +) + +// certFileEnv is the environment variable that crypto/x509 reads on unix for +// an alternative root bundle. +const certFileEnv = "SSL_CERT_FILE" + +// darwinCertFile is the PEM bundle of macOS. crypto/x509 does not read it on +// darwin, because it uses the system verifier, so the client must read it. +const darwinCertFile = "/etc/ssl/cert.pem" + +var darwinRoots struct { + once sync.Once + pool *x509.CertPool +} + +// defaultTLSConfig returns the config that the client dials HTTPS with. +// +// On darwin crypto/x509 verifies a certificate with the platform verifier and +// not with a root pool, and crypto/x509/internal/macos in TinyGo is a stub, so +// a nil RootCAs makes every verification fail. A non-nil pool sends x509 to its +// pure Go path, which works. The roots come from $SSL_CERT_FILE, or from +// /etc/ssl/cert.pem. +// +// If no file can be read, the config has no RootCAs, so the result is an +// ordinary verification error and not a check that is silently skipped. +func defaultTLSConfig() *tls.Config { + darwinRoots.once.Do(loadDarwinRoots) + if darwinRoots.pool == nil { + return nil + } + return &tls.Config{RootCAs: darwinRoots.pool} +} + +func loadDarwinRoots() { + path := os.Getenv(certFileEnv) + if path == "" { + path = darwinCertFile + } + pem, err := os.ReadFile(path) + if err != nil { + return + } + pool := x509.NewCertPool() + if pool.AppendCertsFromPEM(pem) { + darwinRoots.pool = pool + } +} diff --git a/http/tlsconfig_other.go b/http/tlsconfig_other.go new file mode 100644 index 0000000..66b8fff --- /dev/null +++ b/http/tlsconfig_other.go @@ -0,0 +1,14 @@ +//go:build !darwin + +// TINYGO: trust roots for the HTTPS client, everywhere but darwin. + +package http + +import "crypto/tls" + +// defaultTLSConfig returns the config that the client dials HTTPS with. Off +// darwin crypto/x509 reads the system roots from the usual certificate files, +// so a nil config is correct. +func defaultTLSConfig() *tls.Config { + return nil +}