From 12041806b0fdc76ae07c2eaf45fa440d71a4ba90 Mon Sep 17 00:00:00 2001 From: xiqueshuzi Date: Wed, 5 Aug 2026 10:07:29 +0800 Subject: [PATCH] fix: enforce client certificate revocation (IK6I9O) --- tms/deploy/configs/gateway-trpc_go.yaml | 1 + tms/gateway/cmd/gateway/main.go | 2 + tms/gateway/internal/server/server.go | 80 +++++++ .../internal/server/server_tls_test.go | 209 ++++++++++++++++++ tms/gateway/trpc_go.yaml | 1 + 5 files changed, 293 insertions(+) create mode 100644 tms/gateway/internal/server/server_tls_test.go diff --git a/tms/deploy/configs/gateway-trpc_go.yaml b/tms/deploy/configs/gateway-trpc_go.yaml index 9f08134..0383fc5 100644 --- a/tms/deploy/configs/gateway-trpc_go.yaml +++ b/tms/deploy/configs/gateway-trpc_go.yaml @@ -34,6 +34,7 @@ custom: cert_path: /data/ocm/ca/server.crt key_path: /data/ocm/ca/server.key ca_cert_path: /data/ocm/ca/ca.crt + crl_path: /data/ocm/ca/crl.pem limits: max_connections: 100 per_ip_limit: 3 diff --git a/tms/gateway/cmd/gateway/main.go b/tms/gateway/cmd/gateway/main.go index 7f0ad51..3f8ead2 100644 --- a/tms/gateway/cmd/gateway/main.go +++ b/tms/gateway/cmd/gateway/main.go @@ -37,6 +37,7 @@ type TLSConfig struct { CertPath string `yaml:"cert_path"` KeyPath string `yaml:"key_path"` CACertPath string `yaml:"ca_cert_path"` + CRLPath string `yaml:"crl_path"` } // LimitsConfig 承载连接相关的限额配置。 @@ -291,6 +292,7 @@ func main() { CertPath: custom.TCP.TLS.CertPath, KeyPath: custom.TCP.TLS.KeyPath, CACertPath: custom.TCP.TLS.CACertPath, + CRLPath: custom.TCP.TLS.CRLPath, }, } limitsCfg := server.LimitsConfig{ diff --git a/tms/gateway/internal/server/server.go b/tms/gateway/internal/server/server.go index c78d708..8c7290b 100644 --- a/tms/gateway/internal/server/server.go +++ b/tms/gateway/internal/server/server.go @@ -4,11 +4,14 @@ package server import ( + "bytes" "crypto/tls" "crypto/x509" + "encoding/pem" "fmt" "net" "os" + "path/filepath" "sync" "time" @@ -38,6 +41,7 @@ type TLSConfig struct { CertPath string KeyPath string CACertPath string + CRLPath string } // LimitsConfig 承载连接相关的限额配置。 @@ -263,11 +267,87 @@ func buildTLSConfig(cfg TLSConfig) (*tls.Config, error) { if !caPool.AppendCertsFromPEM(caCertPEM) { return nil, fmt.Errorf("failed to parse CA cert") } + caCerts, err := parseCertificatesPEM(caCertPEM) + if err != nil { + return nil, fmt.Errorf("parse CA cert: %w", err) + } + crlPath := cfg.CRLPath + if crlPath == "" { + crlPath = filepath.Join(filepath.Dir(cfg.CACertPath), "crl.pem") + } return &tls.Config{ Certificates: []tls.Certificate{cert}, ClientCAs: caPool, ClientAuth: tls.VerifyClientCertIfGiven, // 注册阶段允许没有客户端证书 MinVersion: tls.VersionTLS13, + VerifyConnection: func(state tls.ConnectionState) error { + if len(state.PeerCertificates) == 0 { + return nil + } + return verifyClientCertificateCRL(state.PeerCertificates[0], caCerts, crlPath, time.Now()) + }, }, nil } + +func parseCertificatesPEM(data []byte) ([]*x509.Certificate, error) { + var certs []*x509.Certificate + for len(data) > 0 { + block, rest := pem.Decode(data) + data = rest + if block == nil { + break + } + if block.Type != "CERTIFICATE" { + continue + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, err + } + certs = append(certs, cert) + } + if len(certs) == 0 { + return nil, fmt.Errorf("no certificates found") + } + return certs, nil +} + +func verifyClientCertificateCRL(client *x509.Certificate, caCerts []*x509.Certificate, crlPath string, now time.Time) error { + crlPEM, err := os.ReadFile(crlPath) + if err != nil { + return fmt.Errorf("read CRL: %w", err) + } + block, _ := pem.Decode(crlPEM) + if block == nil || block.Type != "X509 CRL" { + return fmt.Errorf("parse CRL PEM") + } + crl, err := x509.ParseRevocationList(block.Bytes) + if err != nil { + return fmt.Errorf("parse CRL: %w", err) + } + if crl.ThisUpdate.IsZero() || now.Before(crl.ThisUpdate) { + return fmt.Errorf("CRL is not yet valid") + } + if crl.NextUpdate.IsZero() || !now.Before(crl.NextUpdate) { + return fmt.Errorf("CRL is expired") + } + + validSigner := false + for _, caCert := range caCerts { + if bytes.Equal(crl.RawIssuer, caCert.RawSubject) && crl.CheckSignatureFrom(caCert) == nil { + validSigner = true + break + } + } + if !validSigner { + return fmt.Errorf("CRL signature or issuer is invalid") + } + + for _, entry := range crl.RevokedCertificateEntries { + if client.SerialNumber.Cmp(entry.SerialNumber) == 0 { + return fmt.Errorf("client certificate revoked") + } + } + return nil +} diff --git a/tms/gateway/internal/server/server_tls_test.go b/tms/gateway/internal/server/server_tls_test.go new file mode 100644 index 0000000..6922b42 --- /dev/null +++ b/tms/gateway/internal/server/server_tls_test.go @@ -0,0 +1,209 @@ +// Copyright (C) 2024 OpenCloudOS +// License: GPL-3.0-or-later + +package server + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "os" + "path/filepath" + "testing" + "time" +) + +type tlsFixture struct { + config TLSConfig + caCert *x509.Certificate + caKey *rsa.PrivateKey + clientCert *x509.Certificate + crlPath string +} + +func newTLSFixture(t *testing.T) *tlsFixture { + t.Helper() + now := time.Now() + dir := t.TempDir() + + caKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + caTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "test-ca"}, + NotBefore: now.Add(-time.Hour), + NotAfter: now.Add(24 * time.Hour), + IsCA: true, + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + } + caDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey) + if err != nil { + t.Fatal(err) + } + caCert, err := x509.ParseCertificate(caDER) + if err != nil { + t.Fatal(err) + } + + serverKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + serverTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{CommonName: "localhost"}, + DNSNames: []string{"localhost"}, + NotBefore: now.Add(-time.Hour), + NotAfter: now.Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } + serverDER, err := x509.CreateCertificate(rand.Reader, serverTemplate, caCert, &serverKey.PublicKey, caKey) + if err != nil { + t.Fatal(err) + } + + clientKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + clientTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(3), + Subject: pkix.Name{CommonName: "1001"}, + NotBefore: now.Add(-time.Hour), + NotAfter: now.Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + } + clientDER, err := x509.CreateCertificate(rand.Reader, clientTemplate, caCert, &clientKey.PublicKey, caKey) + if err != nil { + t.Fatal(err) + } + clientCert, err := x509.ParseCertificate(clientDER) + if err != nil { + t.Fatal(err) + } + + certPath := filepath.Join(dir, "server.crt") + keyPath := filepath.Join(dir, "server.key") + caPath := filepath.Join(dir, "ca.crt") + writePEM(t, certPath, "CERTIFICATE", serverDER) + writePEM(t, keyPath, "RSA PRIVATE KEY", x509.MarshalPKCS1PrivateKey(serverKey)) + writePEM(t, caPath, "CERTIFICATE", caDER) + + return &tlsFixture{ + config: TLSConfig{ + CertPath: certPath, + KeyPath: keyPath, + CACertPath: caPath, + }, + caCert: caCert, + caKey: caKey, + clientCert: clientCert, + crlPath: filepath.Join(dir, "crl.pem"), + } +} + +func writePEM(t *testing.T, path, blockType string, der []byte) { + t.Helper() + if err := os.WriteFile(path, pem.EncodeToMemory(&pem.Block{Type: blockType, Bytes: der}), 0o600); err != nil { + t.Fatal(err) + } +} + +func (f *tlsFixture) writeCRL(t *testing.T, revoked bool, thisUpdate, nextUpdate time.Time) { + t.Helper() + entries := []x509.RevocationListEntry{} + if revoked { + entries = append(entries, x509.RevocationListEntry{ + SerialNumber: f.clientCert.SerialNumber, + RevocationTime: time.Now().Add(-time.Minute), + }) + } + der, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ + SignatureAlgorithm: x509.SHA256WithRSA, + RevokedCertificateEntries: entries, + Number: big.NewInt(1), + ThisUpdate: thisUpdate, + NextUpdate: nextUpdate, + }, f.caCert, f.caKey) + if err != nil { + t.Fatal(err) + } + writePEM(t, f.crlPath, "X509 CRL", der) +} + +func TestBuildTLSConfigRejectsRevokedClientCertificate(t *testing.T) { + fixture := newTLSFixture(t) + now := time.Now() + fixture.writeCRL(t, true, now.Add(-time.Minute), now.Add(time.Hour)) + + cfg, err := buildTLSConfig(fixture.config) + if err != nil { + t.Fatal(err) + } + if cfg.VerifyConnection == nil { + t.Fatal("VerifyConnection is not configured") + } + if err := cfg.VerifyConnection(connectionState(fixture.clientCert)); err == nil { + t.Fatal("revoked client certificate was accepted") + } +} + +func TestBuildTLSConfigReloadsCRLForEachConnection(t *testing.T) { + fixture := newTLSFixture(t) + now := time.Now() + fixture.writeCRL(t, false, now.Add(-time.Minute), now.Add(time.Hour)) + + cfg, err := buildTLSConfig(fixture.config) + if err != nil { + t.Fatal(err) + } + if cfg.VerifyConnection == nil { + t.Fatal("VerifyConnection is not configured") + } + state := connectionState(fixture.clientCert) + if err := cfg.VerifyConnection(state); err != nil { + t.Fatalf("valid client certificate rejected: %v", err) + } + + fixture.writeCRL(t, true, now.Add(-time.Minute), now.Add(time.Hour)) + if err := cfg.VerifyConnection(state); err == nil { + t.Fatal("updated CRL was not applied") + } +} + +func TestBuildTLSConfigRejectsInvalidOrExpiredCRL(t *testing.T) { + fixture := newTLSFixture(t) + cfg, err := buildTLSConfig(fixture.config) + if err != nil { + t.Fatal(err) + } + if cfg.VerifyConnection == nil { + t.Fatal("VerifyConnection is not configured") + } + + if err := os.WriteFile(fixture.crlPath, []byte("not a CRL"), 0o600); err != nil { + t.Fatal(err) + } + if err := cfg.VerifyConnection(connectionState(fixture.clientCert)); err == nil { + t.Fatal("invalid CRL was accepted") + } + + now := time.Now() + fixture.writeCRL(t, false, now.Add(-2*time.Hour), now.Add(-time.Hour)) + if err := cfg.VerifyConnection(connectionState(fixture.clientCert)); err == nil { + t.Fatal("expired CRL was accepted") + } +} + +func connectionState(client *x509.Certificate) tls.ConnectionState { + return tls.ConnectionState{PeerCertificates: []*x509.Certificate{client}} +} diff --git a/tms/gateway/trpc_go.yaml b/tms/gateway/trpc_go.yaml index bb86c60..c5ee976 100644 --- a/tms/gateway/trpc_go.yaml +++ b/tms/gateway/trpc_go.yaml @@ -33,6 +33,7 @@ custom: cert_path: /data/ocm/ca/server.crt key_path: /data/ocm/ca/server.key ca_cert_path: /data/ocm/ca/ca.crt + crl_path: /data/ocm/ca/crl.pem limits: max_connections: 100 per_ip_limit: 3 -- Gitee