diff --git a/.github/workflows/codeChecks.yml b/.github/workflows/codeChecks.yml index 1e67962..1681afb 100644 --- a/.github/workflows/codeChecks.yml +++ b/.github/workflows/codeChecks.yml @@ -26,7 +26,7 @@ jobs: strategy: max-parallel: 2 matrix: - go-version: ["1.27.0"] + go-version: ["1.27.1"] steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -47,7 +47,7 @@ jobs: strategy: max-parallel: 2 matrix: - go-version: ["1.27.0"] + go-version: ["1.27.1"] steps: - name: Checkout @@ -74,7 +74,7 @@ jobs: strategy: max-parallel: 2 matrix: - go-version: ["1.27.0"] + go-version: ["1.27.1"] steps: - name: Checkout @@ -103,7 +103,7 @@ jobs: - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: - go-version: "1.27.0" + go-version: "1.27.1" - name: generate test coverage run: go test ./... -coverprofile=./cover.out -covermode=atomic -coverpkg=./... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1d289a2..126717d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ permissions: env: CGO_ENABLED: 0 DOCKER_CLI_EXPERIMENTAL: "enabled" - GO_VERSION: "1.27.0" + GO_VERSION: "1.27.1" jobs: code-checks: diff --git a/CHANGELOG.md b/CHANGELOG.md index c920bcd..05064a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ + + # https-wrench - changelog +## Unreleased + +### CI + + switch to Go 1.27.1 + +### Tests + + Certinfo: GetRemoteCerts tests apply SetTLSInsecure before SetTLSEndpoint so endpoint certificate retrieval uses the intended TLS verification mode. + ## 0.15.1 (2026-09-03) ### Feat diff --git a/Taskfile.yml b/Taskfile.yml deleted file mode 100644 index 8afc53a..0000000 --- a/Taskfile.yml +++ /dev/null @@ -1,61 +0,0 @@ -version: "3" - -tasks: - goreleaser:test:pkg: - internal: true - desc: Test a package installation - vars: - CONTAINER_NAME: "test-{{.Image}}-{{.Platform}}" - cmds: - - defer: docker rm -f {{.CONTAINER_NAME}} - - docker create --name {{.CONTAINER_NAME}} --platform linux/{{.Platform}} --workdir /tmp {{.Image}} sh -c 'ls -la /tmp && {{.Cmd}} && https-wrench --version' - - docker cp {{.TASKFILE_DIR}}/dist/. {{.CONTAINER_NAME}}:/tmp/ - - docker start -a {{.CONTAINER_NAME}} - - goreleaser:test:rpm: - desc: Tests rpm packages - vars: - RPM_INSTALL: "rpm --nodeps -ivh" - cmds: - - task: goreleaser:test:pkg - vars: - Platform: "amd64" - Image: fedora - Cmd: "{{.RPM_INSTALL}} https-wrench*_linux_amd64.rpm" - - task: goreleaser:test:pkg - vars: - Platform: "arm64" - Image: fedora - Cmd: "{{.RPM_INSTALL}} https-wrench*_linux_arm64.rpm" - - goreleaser:test:deb: - desc: Tests deb packages - vars: - DPKG_INSTALL: "dpkg --ignore-depends=git -i" - cmds: - - task: goreleaser:test:pkg - vars: - Platform: "amd64" - Image: ubuntu - Cmd: "{{.DPKG_INSTALL}} https-wrench*_linux_amd64.deb" - - task: goreleaser:test:pkg - vars: - Platform: "arm64" - Image: ubuntu - Cmd: "{{.DPKG_INSTALL}} https-wrench*_linux_arm64.deb" - - goreleaser:test:apk: - desc: Tests apk packages - vars: - APK_INSTALL: "apk add --allow-untrusted -U" - cmds: - - task: goreleaser:test:pkg - vars: - Platform: "amd64" - Image: alpine - Cmd: "{{.APK_INSTALL}} https-wrench*_linux_amd64.apk" - - task: goreleaser:test:pkg - vars: - Platform: "arm64" - Image: alpine - Cmd: "{{.APK_INSTALL}} https-wrench*_linux_arm64.apk" diff --git a/go.mod b/go.mod index ca37540..4007ccf 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/xenos76/https-wrench -go 1.27.0 +go 1.27.1 require ( github.com/MicahParks/jwkset v0.11.3 diff --git a/internal/certinfo/certinfo_handlers.go b/internal/certinfo/certinfo_handlers.go index b389032..b1c50c5 100644 --- a/internal/certinfo/certinfo_handlers.go +++ b/internal/certinfo/certinfo_handlers.go @@ -26,6 +26,17 @@ import ( "github.com/xenos76/https-wrench/internal/style" ) +// defaultCurvePreferences lists Go 1.27 TLS hybrids plus classical fallbacks. +// Explicit CurvePreferences keeps PQ on when GODEBUG=tlsmlkem=0 / tlssecpmlkem=0. +var defaultCurvePreferences = []tls.CurveID{ + tls.X25519MLKEM768, + tls.SecP256r1MLKEM768, + tls.SecP384r1MLKEM1024, + tls.X25519, + tls.CurveP256, + tls.CurveP384, +} + // PrintData prints all collected certificate and key information (local files and remote endpoints) // to the provided writer in a human-readable format. // @@ -202,6 +213,7 @@ func (c *Config) GetRemoteCerts(ctx context.Context) error { tlsConfig := &tls.Config{ RootCAs: c.CACertsPool, InsecureSkipVerify: c.TLSInsecure, + CurvePreferences: slices.Clone(defaultCurvePreferences), } verifyName := c.TLSServerName diff --git a/internal/certinfo/certinfo_handlers_test.go b/internal/certinfo/certinfo_handlers_test.go index 522241d..4eb9ed9 100644 --- a/internal/certinfo/certinfo_handlers_test.go +++ b/internal/certinfo/certinfo_handlers_test.go @@ -3,9 +3,11 @@ package certinfo import ( "bytes" "context" + "crypto/tls" "crypto/x509" "crypto/x509/pkix" "math/big" + "net" "testing" "time" @@ -15,31 +17,26 @@ import ( //nolint:revive func TestCertinfo_GetRemoteCerts(t *testing.T) { tests := []struct { - desc string - srvCfg demoHTTPServerConfig - caCertFile string - insecure bool - expectSrvHost string - expectSrvPort string - expectError bool - expectMsg string + desc string + srvCfg demoHTTPServerConfig + caCertFile string + insecure bool + expectError bool + expectMsg string + wantCurveID string }{ { desc: "RSA Cert Success", srvCfg: demoHTTPServerConfig{ - serverAddr: "localhost:46301", serverName: "example.com", serverCertFile: RSASampleCertBundleFile, serverKeyFile: RSASampleCertKeyFile, }, - caCertFile: RSACaCertFile, - expectSrvHost: "localhost", - expectSrvPort: "46301", + caCertFile: RSACaCertFile, }, { desc: "Error Secure and No CA Cert", srvCfg: demoHTTPServerConfig{ - serverAddr: "localhost:46302", serverName: "example.com", serverCertFile: RSASampleCertFile, serverKeyFile: RSASampleCertKeyFile, @@ -53,75 +50,60 @@ func TestCertinfo_GetRemoteCerts(t *testing.T) { { desc: "Malformed Server Certificate", srvCfg: demoHTTPServerConfig{ - serverAddr: "localhost:46303", serverName: "example.com", serverCertFile: RSASamplePKCS8Certificate, serverKeyFile: RSASamplePKCS8PlaintextPrivateKey, }, - caCertFile: RSACaCertFile, - expectSrvHost: "localhost", + caCertFile: RSACaCertFile, //nolint:revive - expectSrvPort: "46303", - expectError: true, - expectMsg: "TLS handshake failed: tls: failed to verify certificate: x509: certificate relies on legacy Common Name field, use SANs instead", + expectError: true, + expectMsg: "TLS handshake failed: tls: failed to verify certificate: x509: certificate relies on legacy Common Name field, use SANs instead", }, { desc: "No CA Cert and Insecure", srvCfg: demoHTTPServerConfig{ - serverAddr: "localhost:46304", serverName: "example.com", serverCertFile: RSASampleCertFile, serverKeyFile: RSASampleCertKeyFile, }, - insecure: true, - expectSrvHost: "localhost", - expectSrvPort: "46304", - caCertFile: emptyString, + insecure: true, + caCertFile: emptyString, }, { desc: "Wrong CA Cert and Secure", srvCfg: demoHTTPServerConfig{ - serverAddr: "localhost:46305", serverName: "example.com", serverCertFile: RSASampleCertFile, serverKeyFile: RSASampleCertKeyFile, }, caCertFile: RSASamplePKCS8Certificate, //nolint:revive - expectSrvHost: "localhost", - expectSrvPort: "46305", - expectError: true, - expectMsg: "TLS handshake failed: tls: failed to verify certificate: x509: certificate signed by unknown authority", + expectError: true, + expectMsg: "TLS handshake failed: tls: failed to verify certificate: x509: certificate signed by unknown authority", }, { desc: "Wrong CA Cert and Insecure", srvCfg: demoHTTPServerConfig{ - serverAddr: "localhost:46306", serverName: "example.com", serverCertFile: RSASampleCertFile, serverKeyFile: RSASampleCertKeyFile, }, - caCertFile: RSASamplePKCS8Certificate, - insecure: true, - expectSrvHost: "localhost", - expectSrvPort: "46306", + caCertFile: RSASamplePKCS8Certificate, + insecure: true, }, { desc: "IPV6 Endpoint RSA Cert Success", srvCfg: demoHTTPServerConfig{ - serverAddr: "[::1]:46307", + listenHost: "::1", serverName: "example.com", serverCertFile: RSASampleCertFile, serverKeyFile: RSASampleCertKeyFile, }, - caCertFile: RSACaCertFile, - expectSrvHost: "::1", - expectSrvPort: "46307", + caCertFile: RSACaCertFile, }, { desc: "Error wrong ServerName", srvCfg: demoHTTPServerConfig{ - serverAddr: "localhost:46308", serverName: "example.co.uk", serverCertFile: RSASampleCertFile, //nolint:revive @@ -131,32 +113,74 @@ func TestCertinfo_GetRemoteCerts(t *testing.T) { expectError: true, expectMsg: "TLS handshake failed: tls: failed to verify certificate: x509: certificate is valid for example.com, example.net, example.de, not example.co.uk", }, + { + desc: "X25519MLKEM768 key exchange", + srvCfg: demoHTTPServerConfig{ + serverName: "example.com", + serverCertFile: RSASampleCertBundleFile, + serverKeyFile: RSASampleCertKeyFile, + tlsCurvePreferences: []tls.CurveID{tls.X25519MLKEM768}, + }, + caCertFile: RSACaCertFile, + wantCurveID: "X25519MLKEM768", + }, + { + desc: "SecP256r1MLKEM768 key exchange", + srvCfg: demoHTTPServerConfig{ + serverName: "example.com", + serverCertFile: RSASampleCertBundleFile, + serverKeyFile: RSASampleCertKeyFile, + tlsCurvePreferences: []tls.CurveID{tls.SecP256r1MLKEM768}, + }, + caCertFile: RSACaCertFile, + wantCurveID: "SecP256r1MLKEM768", + }, + { + desc: "SecP384r1MLKEM1024 key exchange", + srvCfg: demoHTTPServerConfig{ + serverName: "example.com", + serverCertFile: RSASampleCertBundleFile, + serverKeyFile: RSASampleCertKeyFile, + tlsCurvePreferences: []tls.CurveID{tls.SecP384r1MLKEM1024}, + }, + caCertFile: RSACaCertFile, + wantCurveID: "SecP384r1MLKEM1024", + }, } for _, tc := range tests { tt := tc t.Run(tt.desc, func(t *testing.T) { + t.Parallel() + ts, err := NewHTTPSTestServer(tt.srvCfg) require.NoError(t, err) + t.Cleanup(ts.Close) - defer ts.Close() + endpoint := testServerHostPort(ts) + host, port, err := net.SplitHostPort(endpoint) + require.NoError(t, err) cc, err := New() require.NoError(t, err) cc.SetTLSServerName(tt.srvCfg.serverName) cc.SetCaPoolFromFile(tt.caCertFile, inputReader) - cc.SetTLSEndpoint(context.Background(), tt.srvCfg.serverAddr) cc.SetTLSInsecure(tt.insecure) + cc.SetTLSEndpoint(t.Context(), endpoint) - err = cc.GetRemoteCerts(context.Background()) + err = cc.GetRemoteCerts(t.Context()) if !tt.expectError { require.NoError(t, err, "check error not expected") require.Equal(t, tt.srvCfg.serverName, cc.TLSServerName, "check TLSServerName") - require.Equal(t, tt.expectSrvHost, cc.TLSEndpointHost, "check TLSEndpointHost") - require.Equal(t, tt.expectSrvPort, cc.TLSEndpointPort, "check TLSEndpointPort") + require.Equal(t, host, cc.TLSEndpointHost, "check TLSEndpointHost") + require.Equal(t, port, cc.TLSEndpointPort, "check TLSEndpointPort") require.Equal(t, tt.insecure, cc.TLSInsecure, "check TLSInsecure") + if tt.wantCurveID != emptyString { + require.Equal(t, tt.wantCurveID, cc.NegotiatedCurveID, "check NegotiatedCurveID") + } + return } @@ -396,10 +420,8 @@ func TestCertinfo_PrintData(t *testing.T) { keyFile: RSASampleCertKeyFile, caCertFile: RSACaCertFile, keyCertMatch: true, - tlsEndpoint: "localhost:46401", tlsServerName: "example.com", srvCfg: demoHTTPServerConfig{ - serverAddr: "localhost:46401", serverName: "example.com", serverCertFile: RSASampleCertFile, serverKeyFile: RSASampleCertKeyFile, @@ -409,11 +431,9 @@ func TestCertinfo_PrintData(t *testing.T) { desc: "local key and remote TLS Endpoint, certs NOT validated", keyFile: RSASampleCertKeyFile, caCertFile: emptyString, - tlsEndpoint: "localhost:46402", tlsServerName: "example.com", //nolint:revive srvCfg: demoHTTPServerConfig{ - serverAddr: "localhost:46402", serverName: "example.com", serverCertFile: RSASampleCertFile, serverKeyFile: RSASampleCertKeyFile, @@ -429,25 +449,21 @@ func TestCertinfo_PrintData(t *testing.T) { keyFile: RSASampleCertKeyFile, caCertFile: emptyString, keyCertMatch: true, - tlsEndpoint: "localhost:46403", tlsInsecure: true, tlsServerName: "example.com", srvCfg: demoHTTPServerConfig{ - serverAddr: "localhost:46403", serverName: "example.com", serverCertFile: RSASampleCertFile, serverKeyFile: RSASampleCertKeyFile, }, }, { - desc: "local key and remote TLS Endpoint, missing TLS ServerName", - keyFile: RSASampleCertKeyFile, - caCertFile: RSACaCertFile, - tlsEndpoint: "localhost:46404", + desc: "local key and remote TLS Endpoint, missing TLS ServerName", + keyFile: RSASampleCertKeyFile, + caCertFile: RSACaCertFile, //nolint:revive tlsServerName: emptyString, srvCfg: demoHTTPServerConfig{ - serverAddr: "localhost:46404", serverName: "example.com", serverCertFile: RSASampleCertFile, //nolint:revive @@ -462,10 +478,8 @@ func TestCertinfo_PrintData(t *testing.T) { keyFile: ED25519SamplePlaintextPrivateKey, caCertFile: RSACaCertFile, keyCertMatch: false, - tlsEndpoint: "localhost:46405", tlsServerName: "example.com", srvCfg: demoHTTPServerConfig{ - serverAddr: "localhost:46405", serverName: "example.com", serverCertFile: RSASampleCertFile, serverKeyFile: RSASampleCertKeyFile, @@ -476,6 +490,7 @@ func TestCertinfo_PrintData(t *testing.T) { for _, tc := range tests { tt := tc t.Run("No errors test - "+tt.desc, func(t *testing.T) { + t.Parallel() runPrintDataSubtest(t, tt) }) } @@ -551,16 +566,24 @@ func runPrintDataSubtest(t *testing.T, tt printDataTestCase) { require.NoError(t, cc.SetCertsFromFile(tt.certFile, inputReader)) require.NoError(t, cc.SetCaPoolFromFile(tt.caCertFile, inputReader)) - if tt.tlsEndpoint != emptyString { + if tt.srvCfg.serverCertFile != emptyString { ts, errSrv := NewHTTPSTestServer(tt.srvCfg) require.NoError(t, errSrv) + t.Cleanup(ts.Close) - defer ts.Close() + tt.tlsEndpoint = testServerHostPort(ts) + if tt.tlsServerName == emptyString { + // Cert SANs include 127.0.0.1; dial by hostname so empty SNI still mismatches. + _, port, splitErr := net.SplitHostPort(tt.tlsEndpoint) + require.NoError(t, splitErr) + + tt.tlsEndpoint = net.JoinHostPort("localhost", port) + } cc.SetTLSServerName(tt.tlsServerName) cc.SetTLSInsecure(tt.tlsInsecure) - err = cc.SetTLSEndpoint(context.Background(), tt.tlsEndpoint) + err = cc.SetTLSEndpoint(t.Context(), tt.tlsEndpoint) if tt.expectCertsFetchErr { require.EqualError(t, err, tt.expectCertsFetcMsg) } else { diff --git a/internal/certinfo/certinfo_test.go b/internal/certinfo/certinfo_test.go index 362729a..8177619 100644 --- a/internal/certinfo/certinfo_test.go +++ b/internal/certinfo/certinfo_test.go @@ -552,6 +552,7 @@ func TestCertinfo_PrintTLSInfo_HappyPath(t *testing.T) { cc.TLSInfoRequested = true cc.NegotiatedProtocol = "TLS 1.3" cc.NegotiatedCipher = "TLS_AES_128_GCM_SHA256" + cc.NegotiatedCurveID = "X25519MLKEM768" cc.ProbedProtocols = map[string]bool{ "TLS 1.3": true, "TLS 1.2": true, @@ -585,6 +586,8 @@ func TestCertinfo_PrintTLSInfo_HappyPath(t *testing.T) { require.Contains(t, got, "Negotiated TLS Connection") require.Contains(t, got, "TLS 1.3") require.Contains(t, got, "TLS_AES_128_GCM_SHA256") + require.Contains(t, got, "Key Exchange") + require.Contains(t, got, "X25519MLKEM768") require.Contains(t, got, "Protocol Support Scan") require.Contains(t, got, "Cipher Suite Scan") require.Contains(t, got, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256") diff --git a/internal/certinfo/main_test.go b/internal/certinfo/main_test.go index 818a34f..630e74a 100644 --- a/internal/certinfo/main_test.go +++ b/internal/certinfo/main_test.go @@ -32,13 +32,14 @@ type ( } demoHTTPServerConfig struct { - serverAddr string - proxyprotoEnabled bool - serverName string - tlsCipherSuites []uint16 - tlsMaxVersion uint16 - serverCertFile string - serverKeyFile string + listenHost string + proxyprotoEnabled bool + serverName string + tlsCipherSuites []uint16 + tlsCurvePreferences []tls.CurveID + tlsMaxVersion uint16 + serverCertFile string + serverKeyFile string } MockErrReader struct{} @@ -374,6 +375,16 @@ func RSAPrivateKeyToPEM(key *rsa.PrivateKey) []byte { return keyPEM } +func testServerHostPort(ts *httptest.Server) string { + return ts.Listener.Addr().String() +} + +// NewHTTPSTestServer starts an httptest TLS server configured by cfg. +// Cipher suites default to the TLS 1.3 AEADs, CurvePreferences to the Go 1.27 +// PQ hybrids plus classical fallbacks, and MaxVersion to TLS 1.3. Non-empty +// cfg.tlsCipherSuites, cfg.tlsCurvePreferences, or a non-zero cfg.tlsMaxVersion +// override those defaults. Optional cfg.listenHost and cfg.proxyprotoEnabled +// replace the listener. The caller must Close the returned server. func NewHTTPSTestServer(cfg demoHTTPServerConfig) (*httptest.Server, error) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "DemoHTTPSServer Handler - client output\n") @@ -385,27 +396,21 @@ func NewHTTPSTestServer(cfg demoHTTPServerConfig) (*httptest.Server, error) { ts := httptest.NewUnstartedServer(handler) ts.EnableHTTP2 = true - if cfg.serverAddr != emptyString && !cfg.proxyprotoEnabled { - listener, err := net.Listen("tcp", cfg.serverAddr) + if cfg.listenHost != emptyString { + ln, err := net.Listen("tcp", net.JoinHostPort(cfg.listenHost, "0")) if err != nil { return nil, fmt.Errorf("error creating listener: %w", err) } - ts.Listener = listener + _ = ts.Listener.Close() + ts.Listener = ln } - if cfg.serverAddr != emptyString && cfg.proxyprotoEnabled { - ln, err := net.Listen("tcp", cfg.serverAddr) - if err != nil { - return nil, fmt.Errorf("error creating proxyproto enabled listener: %w", err) - } - - proxyListener := &proxyproto.Listener{ - Listener: ln, + if cfg.proxyprotoEnabled { + ts.Listener = &proxyproto.Listener{ + Listener: ts.Listener, ReadHeaderTimeout: 10 * time.Second, } - - ts.Listener = proxyListener } cert, err := tls.LoadX509KeyPair( @@ -428,6 +433,12 @@ func NewHTTPSTestServer(cfg demoHTTPServerConfig) (*httptest.Server, error) { tlsCipherSuites = cfg.tlsCipherSuites } + tlsCurvePreferences := defaultCurvePreferences + + if len(cfg.tlsCurvePreferences) > 0 { + tlsCurvePreferences = cfg.tlsCurvePreferences + } + // Set default TLS MaxVersion to 1.3 var tlsMaxVersion uint16 = tls.VersionTLS13 @@ -436,9 +447,10 @@ func NewHTTPSTestServer(cfg demoHTTPServerConfig) (*httptest.Server, error) { } ts.TLS = &tls.Config{ - Certificates: []tls.Certificate{cert}, - CipherSuites: tlsCipherSuites, - MaxVersion: tlsMaxVersion, + Certificates: []tls.Certificate{cert}, + CipherSuites: tlsCipherSuites, + CurvePreferences: tlsCurvePreferences, + MaxVersion: tlsMaxVersion, } ts.StartTLS()