build: update Go to 1.27.3 and refresh dependencies

Update the server and five debugging modules, regenerate dependency credits,
and refresh static curl and the native Red Hat CA mitigation.

Preserve Kafka's default broker protocol and terminal widths. Copy the
returned DARE plaintext when unsealing legacy keys, and cover old encrypted
keys, Kafka producer exchanges, and MinKMS TLS identity with regression tests.
Keep the published PGSTY component references for reproducible builds.

Validated with make test, targeted race tests, module integrity and tidy
checks, and govulncheck v1.9.0 source analysis. Rechecked the frozen source
and artifact hashes behind the earlier whole-stack compatibility evidence.

Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
Feng Ruohang
2026-10-23 02:15:16 +08:00
parent 856f8f4c55
commit 011b0df5f5
23 changed files with 1684 additions and 753 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ import (
// DARECiphers returns a list of supported cipher suites
// for the DARE object encryption.
func DARECiphers() []byte { return []byte{sio.AES_256_GCM, sio.CHACHA20_POLY1305} }
func DARECiphers() []byte { return []byte{sio.AES_GCM, sio.CHACHA20_POLY1305} }
// TLSCiphers returns a list of supported TLS transport
// cipher suite IDs.
+88
View File
@@ -0,0 +1,88 @@
// Copyright (c) 2026 Ruohang Feng
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package crypto
import (
"bytes"
"encoding/hex"
"testing"
"github.com/minio/sio"
)
// These sealed keys were generated with minio/sio v0.4.3 before the v0.5 API
// change, using a 32-byte zero external key and IV bytes 32 through 63. They
// provide positive legacy DARE coverage instead of only rejecting bad keys.
func TestUnsealLegacyDAREKeys(t *testing.T) {
for _, test := range []struct{ name, algorithm, ciphertext string }{
{"v1-aes256", InsecureSealAlgorithm, "10001f000000000001020304050607081e469edeb77bba11c91cf3132fc00ab28b8effbaa92784ea004f07a1cf1c9af28b5b725e1e5c96819a94703b10aa5943"},
{"v1-chacha20", InsecureSealAlgorithm, "10011f00000000000102030405060708948d4d3ed99f047010c7c9b54ec2c58d0b437a9e9a7df3c032fcdf99e863b2bda7c9f995a3313f65bff8b941cb689e0d"},
{"v2-aes256", SealAlgorithm, "20001f008102030405060708090a0b0c990455a32f26fd5a3417b4357ef3b92455c13e37947e0fac9e85380daa7b2a3eaffb032630b8b4652f66f01322abb449"},
{"v2-chacha20", SealAlgorithm, "20011f008102030405060708090a0b0c34f030b5744d4ad777d03b1ec5e0771480911ec60cc796b4b1842ee9d3f8aa96a047bd4d8c26896613bca7e0dbefa85c"},
} {
t.Run(test.name, func(t *testing.T) {
raw, err := hex.DecodeString(test.ciphertext)
if err != nil {
t.Fatal(err)
}
sealed := SealedKey{Algorithm: test.algorithm}
copy(sealed.Key[:], raw)
var expected ObjectKey
for i := range expected {
expected[i] = byte(i)
sealed.IV[i] = byte(i + 32)
}
var key ObjectKey
if err := key.Unseal(make([]byte, 32), sealed, "SSE-S3", "bucket", "object"); err != nil {
t.Fatal(err)
}
if key != expected {
t.Fatalf("unsealed key: got %x, want %x", key, expected)
}
sealed.Key[32] ^= 1
if err := key.Unseal(make([]byte, 32), sealed, "SSE-S3", "bucket", "object"); err == nil {
t.Fatal("tampered sealed key was accepted")
}
})
}
}
func TestDARE32ByteKeyRoundTrip(t *testing.T) {
var key ObjectKey
for i := range key {
key[i] = byte(i)
}
// Include both sides of a DARE package boundary and a multi-package payload.
for _, size := range []int{0, 1, 65535, 65536, 65537, 131073} {
plaintext := bytes.Repeat([]byte{0xa5}, size)
for _, cipher := range DARECiphers() {
var ciphertext, decoded bytes.Buffer
if _, err := sio.Encrypt(&ciphertext, bytes.NewReader(plaintext), sio.Config{Key: key[:], CipherSuites: []byte{cipher}}); err != nil {
t.Fatal(err)
}
if size > 0 && ciphertext.Bytes()[0] != sio.Version20 {
t.Fatal("new objects must retain DARE v2 format")
}
if _, err := sio.Decrypt(&decoded, bytes.NewReader(ciphertext.Bytes()), sio.Config{Key: key[:], CipherSuites: DARECiphers()}); err != nil {
t.Fatal(err)
}
if !bytes.Equal(decoded.Bytes(), plaintext) {
t.Fatalf("cipher %d size %d did not round-trip", cipher, size)
}
}
}
}
+4 -1
View File
@@ -130,9 +130,12 @@ func (key *ObjectKey) Unseal(extKey []byte, sealedKey SealedKey, domain, bucket,
unsealConfig = sio.Config{MinVersion: sio.Version10, Key: sha.Sum(nil)}
}
if out, err := sio.DecryptBuffer(key[:0], sealedKey.Key[:], unsealConfig); len(out) != 32 || err != nil {
out, err := sio.DecryptBuffer(key[:0], sealedKey.Key[:], unsealConfig)
if len(out) != 32 || err != nil {
return ErrSecretKeyMismatch
}
// DARE v1 may allocate a new buffer instead of reusing key's backing array.
copy(key[:], out)
return nil
}
+2
View File
@@ -341,6 +341,8 @@ func (target *KafkaTarget) initKafka() error {
args := target.args
config := sarama.NewConfig()
// Preserve SILO's default broker protocol when no version is configured.
config.Version = sarama.V2_1_0_0
if args.Version != "" {
kafkaVersion, err := sarama.ParseKafkaVersion(args.Version)
if err != nil {
+101
View File
@@ -0,0 +1,101 @@
// Copyright (c) 2026 Ruohang Feng
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package target
import (
"context"
"testing"
"github.com/IBM/sarama"
"github.com/minio/minio/internal/event"
xnet "github.com/pgsty/silo-pkg/v3/net"
)
// TestKafkaBrokerVersionCompatibility exercises the actual target constructor
// and producer: dependency upgrades must not raise the default broker protocol.
func TestKafkaBrokerVersionCompatibility(t *testing.T) {
for _, test := range []struct {
name, version string
want sarama.KafkaVersion
metadata int16
sasl bool
}{
{"default", "", sarama.V2_1_0_0, 7, false},
{"explicit-old", "0.10.2.0", sarama.V0_10_2_0, 2, false},
{"explicit-new", "2.8.0", sarama.V2_8_0_0, 11, false},
{"default-sasl", "", sarama.V2_1_0_0, 7, true},
} {
t.Run(test.name, func(t *testing.T) {
broker := sarama.NewMockBroker(t, 1)
defer broker.Close()
broker.SetHandlerByMap(map[string]sarama.MockResponse{
"ApiVersionsRequest": sarama.NewMockApiVersionsResponse(t).SetApiKeys([]sarama.ApiVersionsResponseKey{
{ApiKey: 0, MinVersion: 0, MaxVersion: 8},
{ApiKey: 3, MinVersion: 0, MaxVersion: 11},
{ApiKey: 17, MinVersion: 0, MaxVersion: 1},
{ApiKey: 18, MinVersion: 0, MaxVersion: 3},
{ApiKey: 36, MinVersion: 0, MaxVersion: 2},
}),
"MetadataRequest": sarama.NewMockMetadataResponse(t).SetBroker(broker.Addr(), broker.BrokerID()).SetLeader("events", 0, broker.BrokerID()),
"ProduceRequest": sarama.NewMockProduceResponse(t),
"SaslHandshakeRequest": sarama.NewMockSaslHandshakeResponse(t).SetEnabledMechanisms([]string{sarama.SASLTypePlaintext}),
"SaslAuthenticateRequest": sarama.NewMockSaslAuthenticateResponse(t),
})
host, err := xnet.ParseHost(broker.Addr())
if err != nil {
t.Fatal(err)
}
target := &KafkaTarget{args: KafkaArgs{Brokers: []xnet.Host{*host}, Topic: "events", Version: test.version}}
target.args.SASL.Enable = test.sasl
target.args.SASL.User = "user"
target.args.SASL.Password = "password"
target.loggerOnce = func(context.Context, error, string, ...any) {}
if err := target.Save(event.Event{EventName: event.ObjectCreatedPut}); err != nil {
t.Fatal(err)
}
defer target.client.Close()
defer target.producer.Close()
if target.config.Version != test.want {
t.Fatalf("broker protocol: got %v, want %v", target.config.Version, test.want)
}
if target.config.Net.SASL.Version != sarama.SASLHandshakeV1 || !target.config.ApiVersionsRequest {
t.Fatal("SASL/API version negotiation changed")
}
if err := target.config.Validate(); err != nil {
t.Fatal(err)
}
var metadata, produced, authenticated bool
for _, exchange := range broker.History() {
switch req := exchange.Request.(type) {
case *sarama.MetadataRequest:
metadata = true
if req.Version != test.metadata {
t.Fatalf("metadata request version: got %d, want %d", req.Version, test.metadata)
}
case *sarama.ProduceRequest:
produced = true
case *sarama.SaslAuthenticateRequest:
authenticated = true
}
}
if !metadata || !produced || (test.sasl && !authenticated) {
t.Fatalf("incomplete broker exchange: metadata=%v produce=%v authenticated=%v", metadata, produced, authenticated)
}
})
}
}
+117
View File
@@ -0,0 +1,117 @@
// Copyright (c) 2026 Ruohang Feng
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package kms
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"io"
"log"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sync"
"testing"
"time"
"aead.dev/mtls"
)
// Exercise the configured MinKMS transport, including the API-key certificate.
// A KES integration test does not cover this separate client implementation.
func TestConnectMinKMSTLSIdentity(t *testing.T) {
for _, key := range []string{EnvKESEndpoint, EnvKMSSecretKey, EnvKMSSecretKeyFile} {
t.Setenv(key, "") // Restore the original value after testing absence.
if err := os.Unsetenv(key); err != nil {
t.Fatal(err)
}
}
key, err := mtls.GenerateKey()
if err != nil {
t.Fatal(err)
}
var mu sync.Mutex
var connections []tls.ConnectionState
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/version" {
t.Errorf("unexpected KMS request: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
identity, err := mtls.PeerIdentity(r.TLS)
if err != nil || identity != key.Identity() {
t.Errorf("API key identity: got %s, error %v", identity, err)
w.WriteHeader(http.StatusForbidden)
return
}
mu.Lock()
connections = append(connections, *r.TLS)
mu.Unlock()
w.Header().Set("Content-Type", "application/json")
// Force a new TLS connection so the second request checks session reuse.
w.Header().Set("Connection", "close")
_, _ = io.WriteString(w, `{"version":"2026-10-22T00-00-00Z","api_version":"v1"}`)
}))
server.Config.ErrorLog = log.New(io.Discard, "", 0)
server.TLS = &tls.Config{MinVersion: tls.VersionTLS12, MaxVersion: tls.VersionTLS12, ClientAuth: tls.RequireAnyClientCert}
server.StartTLS()
defer server.Close()
caDir := t.TempDir()
if err := os.WriteFile(filepath.Join(caDir, "ca.pem"), pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: server.Certificate().Raw}), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv(EnvKMSEndpoint, server.URL)
t.Setenv(EnvKMSAPIKey, key.String())
t.Setenv(EnvKMSDefaultKey, "silo-test-key")
t.Setenv(EnvKMSEnclave, "silo-test-enclave")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
client, err := Connect(ctx, &ConnectionOptions{CADir: caDir})
if err != nil {
t.Fatal(err)
}
if client.Type != MinKMS || client.DefaultKey != "silo-test-key" || client.conn.(*kmsConn).enclave != "silo-test-enclave" {
t.Fatal("MinKMS connection lost its configured identity or key scope")
}
for range 2 {
version, err := client.Version(ctx)
if err != nil || version != "2026-10-22T00-00-00Z" {
t.Fatalf("trusted MinKMS: version %q, error %v", version, err)
}
}
mu.Lock()
if len(connections) != 2 || connections[0].DidResume || !connections[1].DidResume || connections[1].Version != tls.VersionTLS12 {
t.Errorf("TLS 1.2 client session cache was not preserved: %+v", connections)
}
mu.Unlock()
// An empty CA directory cannot authenticate this private server. The client
// must reject it instead of silently falling back to insecure TLS.
untrusted, err := Connect(ctx, &ConnectionOptions{CADir: t.TempDir()})
if err != nil {
t.Fatal(err)
}
_, err = untrusted.Version(ctx)
var unknownCA x509.UnknownAuthorityError
if !errors.As(err, &unknownCA) {
t.Fatalf("untrusted KMS server: got %v, want unknown certificate authority", err)
}
}
+2
View File
@@ -238,6 +238,8 @@ func (h *Target) init() error {
}
sconfig := sarama.NewConfig()
// Preserve SILO's default broker protocol when no version is configured.
sconfig.Version = sarama.V2_1_0_0
if h.kconfig.Version != "" {
kafkaVersion, err := sarama.ParseKafkaVersion(h.kconfig.Version)
if err != nil {
@@ -0,0 +1,98 @@
// Copyright (c) 2026 Ruohang Feng
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package kafka
import (
"testing"
"github.com/IBM/sarama"
xnet "github.com/pgsty/silo-pkg/v3/net"
)
// TestKafkaBrokerVersionCompatibility exercises the actual target constructor
// and producer: dependency upgrades must not raise the default broker protocol.
func TestKafkaBrokerVersionCompatibility(t *testing.T) {
for _, test := range []struct {
name, version string
want sarama.KafkaVersion
metadata int16
sasl bool
}{
{"default", "", sarama.V2_1_0_0, 7, false},
{"explicit-old", "0.10.2.0", sarama.V0_10_2_0, 2, false},
{"explicit-new", "2.8.0", sarama.V2_8_0_0, 11, false},
{"default-sasl", "", sarama.V2_1_0_0, 7, true},
} {
t.Run(test.name, func(t *testing.T) {
broker := sarama.NewMockBroker(t, 1)
defer broker.Close()
broker.SetHandlerByMap(map[string]sarama.MockResponse{
"ApiVersionsRequest": sarama.NewMockApiVersionsResponse(t).SetApiKeys([]sarama.ApiVersionsResponseKey{
{ApiKey: 0, MinVersion: 0, MaxVersion: 8},
{ApiKey: 3, MinVersion: 0, MaxVersion: 11},
{ApiKey: 17, MinVersion: 0, MaxVersion: 1},
{ApiKey: 18, MinVersion: 0, MaxVersion: 3},
{ApiKey: 36, MinVersion: 0, MaxVersion: 2},
}),
"MetadataRequest": sarama.NewMockMetadataResponse(t).SetBroker(broker.Addr(), broker.BrokerID()).SetLeader("events", 0, broker.BrokerID()),
"ProduceRequest": sarama.NewMockProduceResponse(t),
"SaslHandshakeRequest": sarama.NewMockSaslHandshakeResponse(t).SetEnabledMechanisms([]string{sarama.SASLTypePlaintext}),
"SaslAuthenticateRequest": sarama.NewMockSaslAuthenticateResponse(t),
})
host, err := xnet.ParseHost(broker.Addr())
if err != nil {
t.Fatal(err)
}
target := &Target{kconfig: Config{Brokers: []xnet.Host{*host}, Topic: "events", Version: test.version}}
target.kconfig.SASL.Enable = test.sasl
target.kconfig.SASL.User = "user"
target.kconfig.SASL.Password = "password"
if err := target.send(map[string]string{"event": "probe"}); err != nil {
t.Fatal(err)
}
defer target.client.Close()
defer target.producer.Close()
if target.config.Version != test.want {
t.Fatalf("broker protocol: got %v, want %v", target.config.Version, test.want)
}
if target.config.Net.SASL.Version != sarama.SASLHandshakeV1 || !target.config.ApiVersionsRequest {
t.Fatal("SASL/API version negotiation changed")
}
if err := target.config.Validate(); err != nil {
t.Fatal(err)
}
var metadata, produced, authenticated bool
for _, exchange := range broker.History() {
switch req := exchange.Request.(type) {
case *sarama.MetadataRequest:
metadata = true
if req.Version != test.metadata {
t.Fatalf("metadata request version: got %d, want %d", req.Version, test.metadata)
}
case *sarama.ProduceRequest:
produced = true
case *sarama.SaslAuthenticateRequest:
authenticated = true
}
}
if !metadata || !produced || (test.sasl && !authenticated) {
t.Fatalf("incomplete broker exchange: metadata=%v produce=%v authenticated=%v", metadata, produced, authenticated)
}
})
}
}