Files
seaweedfs/weed/security/tls_client_cert_test.go
T
Chris LuGitHubDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com>Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
80a26020d7 util: serialize all ViperProxy access so startup cannot hit concurrent map read/write (#11470)
* util: serialize every ViperProxy method; stop promoting unlocked viper calls

ViperProxy embedded *viper.Viper, so only the five declared methods took
the mutex while every promoted call — GetStringMap in backend.LoadConfiguration
was the reported crash — touched viper's maps unsynchronized. `weed server`
starts the volume server (SetDefault writer) and the master (GetStringMap
reader) back to back, and a race build reports the pair on a plain start.

The wrapped viper is now a named field: a method must be declared here to
exist on the proxy, so unsynchronized access fails at compile time rather
than at runtime. Every promoted use in the tree (GetStringMap, GetUint32,
GetFloat64, GetDuration, IsSet, AllKeys, Set) gets a locked wrapper;
NewViperProxy replaces struct literals for local vipers. GetStringMap
deep-copies its result — viper hands back the internal subtree, so
iterating it after the lock is released would race the next writer.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* util: take the shared lock while LoadConfiguration merges a config file

viper.MergeInConfig rewrites the same maps the proxy serializes; without
the lock a merge can race a concurrent SetDefault or reader exactly like
the reported startup crash.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* util: deep-copy slice elements in the GetStringMap snapshot

A slice of maps inside the returned subtree still shared the inner maps —
copy elements recursively so nothing the caller mutates is viper's
internal state.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* util: add the missing AutomaticEnv wrapper used by tests

sse_reader_test reaches it through GetViper(); without the wrapper the
call no longer exists once the viper field stopped being embedded.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* util: return a fresh slice from GetStringSlice

A stored []string comes back uncast from viper — the backing array is
shared internal state like the GetStringMap subtree, so copy it while
holding the lock.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-26 16:09:29 +08:00

184 lines
5.6 KiB
Go

package security
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net"
"os"
"path/filepath"
"testing"
"time"
"github.com/spf13/viper"
"github.com/seaweedfs/seaweedfs/weed/util"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
)
type testCA struct {
cert *x509.Certificate
key *ecdsa.PrivateKey
file string
}
func newTestCA(t *testing.T, dir string) *testCA {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "test-ca"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageCertSign,
BasicConstraintsValid: true,
IsCA: true,
}
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
if err != nil {
t.Fatal(err)
}
cert, err := x509.ParseCertificate(der)
if err != nil {
t.Fatal(err)
}
file := filepath.Join(dir, "ca.pem")
writePem(t, file, "CERTIFICATE", der)
return &testCA{cert: cert, key: key, file: file}
}
// issue creates a leaf certificate restricted to the given extended key usages.
func (ca *testCA) issue(t *testing.T, dir, name string, ekus []x509.ExtKeyUsage) (certFile, keyFile string) {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
template := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().UnixNano()),
Subject: pkix.Name{CommonName: name},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: ekus,
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
DNSNames: []string{"localhost"},
}
der, err := x509.CreateCertificate(rand.Reader, template, ca.cert, &key.PublicKey, ca.key)
if err != nil {
t.Fatal(err)
}
keyDer, err := x509.MarshalECPrivateKey(key)
if err != nil {
t.Fatal(err)
}
certFile = filepath.Join(dir, name+".pem")
keyFile = filepath.Join(dir, name+".key")
writePem(t, certFile, "CERTIFICATE", der)
writePem(t, keyFile, "EC PRIVATE KEY", keyDer)
return certFile, keyFile
}
func writePem(t *testing.T, file, blockType string, der []byte) {
t.Helper()
if err := os.WriteFile(file, pem.EncodeToMemory(&pem.Block{Type: blockType, Bytes: der}), 0600); err != nil {
t.Fatal(err)
}
}
func startTestGrpcServer(t *testing.T, config *util.ViperProxy, component string) string {
t.Helper()
creds, _ := LoadServerTLS(config, component)
if creds == nil {
t.Fatal("LoadServerTLS returned nil")
}
lis, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
server := grpc.NewServer(creds)
healthpb.RegisterHealthServer(server, health.NewServer())
go server.Serve(lis)
t.Cleanup(server.Stop)
return lis.Addr().String()
}
func healthCheck(t *testing.T, addr string, dialOption grpc.DialOption) error {
t.Helper()
conn, err := grpc.NewClient(addr, dialOption)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err = healthpb.NewHealthClient(conn).Check(ctx, &healthpb.HealthCheckRequest{})
return err
}
// A component configured with a serverAuth-only serving cert plus a
// clientAuth-only client_cert/client_key pair must dial with the client pair.
func TestLoadClientTLSPrefersClientCert(t *testing.T) {
dir := t.TempDir()
ca := newTestCA(t, dir)
serverCert, serverKey := ca.issue(t, dir, "server", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth})
clientCert, clientKey := ca.issue(t, dir, "client", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})
v := util.NewViperProxy(viper.New())
v.Set("grpc.ca", ca.file)
v.Set("grpc.master.cert", serverCert)
v.Set("grpc.master.key", serverKey)
v.Set("grpc.master.client_cert", clientCert)
v.Set("grpc.master.client_key", clientKey)
addr := startTestGrpcServer(t, v, "grpc.master")
if err := healthCheck(t, addr, LoadClientTLS(v, "grpc.master")); err != nil {
t.Fatalf("health check with split client cert failed: %v", err)
}
}
// Without client_cert, the component keeps presenting its serving cert.
func TestLoadClientTLSFallsBackToServingCert(t *testing.T) {
dir := t.TempDir()
ca := newTestCA(t, dir)
dualCert, dualKey := ca.issue(t, dir, "dual", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
v := util.NewViperProxy(viper.New())
v.Set("grpc.ca", ca.file)
v.Set("grpc.master.cert", dualCert)
v.Set("grpc.master.key", dualKey)
addr := startTestGrpcServer(t, v, "grpc.master")
if err := healthCheck(t, addr, LoadClientTLS(v, "grpc.master")); err != nil {
t.Fatalf("health check with dual-EKU cert failed: %v", err)
}
}
// A serverAuth-only cert presented as the client identity fails the peer's
// clientAuth EKU verification — the failure mode client_cert exists to fix.
func TestLoadClientTLSServerOnlyEkuRejected(t *testing.T) {
dir := t.TempDir()
ca := newTestCA(t, dir)
serverCert, serverKey := ca.issue(t, dir, "server", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth})
v := util.NewViperProxy(viper.New())
v.Set("grpc.ca", ca.file)
v.Set("grpc.master.cert", serverCert)
v.Set("grpc.master.key", serverKey)
addr := startTestGrpcServer(t, v, "grpc.master")
if err := healthCheck(t, addr, LoadClientTLS(v, "grpc.master")); err == nil {
t.Fatal("expected handshake failure when presenting a serverAuth-only cert as client identity")
}
}