From 00b4d53529d8aa947d1dea4b4e82fb4500a4f7cb Mon Sep 17 00:00:00 2001 From: Feng Ruohang Date: Wed, 9 Sep 2026 18:50:12 +0800 Subject: [PATCH] test(tls): cover client key exchange defaults Check real S3/Admin and alias-dialer handshakes with ML-KEM enabled and disabled. Document TLS compatibility controls and macOS CA replacement. Validation: command suite, focused race tests, and lint pass. Adversarial review: Claude Code Fable 5.1, max effort. Final verdict: APPROVE FOR COMMIT. Signed-off-by: Feng Ruohang --- README.md | 16 +++++++++ cmd/tls_defaults_test.go | 71 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 cmd/tls_defaults_test.go diff --git a/README.md b/README.md index ec1327620f..4bdd675a8d 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,22 @@ Use [Download & Install](https://silo.pgsty.com/download/#client) to choose a cl | Container image | [`pgsty/mc`](https://hub.docker.com/r/pgsty/mc), multi-arch for `linux/amd64` and `linux/arm64`, with `mc` as the entrypoint | | Silo bundle | [`pgsty/silo`](https://hub.docker.com/r/pgsty/silo) includes the client as `mcli` with an `mc` compatibility alias | +### Go and TLS compatibility + +Go 1.27 builds require macOS 13 or later. On macOS, builds targeting Go 1.27 +replace Keychain trust with on-disk roots and Go's verifier when either +`SSL_CERT_FILE` or `SSL_CERT_DIR` is set. Stale or incomplete CA paths can break +previously trusted connections; unset inherited values to restore Keychain +trust. Certificates in mcli's configured `CAs` directory are still added to the +selected root pool. + +S3, Admin, and alias TLS connections use Go's default key exchanges. +`GODEBUG=tlsmlkem=0` can temporarily accommodate an ML-KEM-intolerant endpoint; +it does not disable certificate verification or ML-DSA signatures. +`GODEBUG=tlssecpmlkem=0` is the narrower option for disabling only the SecP +hybrids while retaining X25519MLKEM768. The relevant +changes are described in the [Go release notes](https://go.dev/doc/go1.27). + ## Quick Start Standalone archives and Linux packages expose the command as `mcli`: diff --git a/cmd/tls_defaults_test.go b/cmd/tls_defaults_test.go new file mode 100644 index 0000000000..f599693966 --- /dev/null +++ b/cmd/tls_defaults_test.go @@ -0,0 +1,71 @@ +// Copyright (c) 2026 Pigsty +// SPDX-License-Identifier: AGPL-3.0-or-later + +package cmd + +import ( + "crypto/tls" + "crypto/x509" + "io" + "net/http" + "net/http/httptest" + "slices" + "testing" + "time" +) + +func TestClientTLSKeyExchangeDefaults(t *testing.T) { + for _, debug := range []string{"tlsmlkem=0", "tlsmlkem=1"} { + t.Run(debug, func(t *testing.T) { + t.Setenv("GODEBUG", debug) + hellos := make(chan []tls.CurveID, 1) + server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = io.WriteString(w, "ok") + })) + server.TLS = &tls.Config{GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) { + select { + case hellos <- slices.Clone(hello.SupportedCurves): + default: + } + return nil, nil + }} + server.StartTLS() + defer server.Close() + roots := x509.NewCertPool() + roots.AddCert(server.Certificate()) + previousRoots := globalRootCAs + globalRootCAs = roots + t.Cleanup(func() { globalRootCAs = previousRoots }) + config := &Config{HostURL: server.URL} + config.initTransport(false) + aliasTransport := &http.Transport{DialTLSContext: newCustomDialTLSContext(&tls.Config{RootCAs: roots})} + defer aliasTransport.CloseIdleConnections() + for name, transport := range map[string]http.RoundTripper{ + "S3-and-admin": config.Transport, + "alias-dialer": aliasTransport, + } { + t.Run(name, func(t *testing.T) { + client := &http.Client{Transport: transport, Timeout: 5 * time.Second} + req, err := http.NewRequest(http.MethodGet, server.URL, nil) + if err != nil { + t.Fatal(err) + } + // The S3 transport is wrapped; close this connection explicitly. + req.Close = true + resp, err := client.Do(req) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK || len(resp.TLS.VerifiedChains) == 0 { + t.Fatalf("status %d, verified chains %d", resp.StatusCode, len(resp.TLS.VerifiedChains)) + } + curves := <-hellos + if got, want := slices.Contains(curves, tls.X25519MLKEM768), debug == "tlsmlkem=1"; got != want { + t.Errorf("ML-KEM offered = %v, want %v; curves %v", got, want, curves) + } + }) + } + }) + } +}