Smail/test/backend/ssl_lifecycle_test.go
smail 9dc861d560
Some checks are pending
Security Checks / security (push) Waiting to run
角色权限收紧前保存证书直接引用与续期热切换改造
2026-09-09 17:42:19 +08:00

256 lines
9.5 KiB
Go

package config
import (
"bytes"
"crypto/tls"
"errors"
"io"
"net"
"os"
"path/filepath"
"testing"
"time"
)
func stageLifecycleCertificate(t *testing.T, root string) ManagedSSLReference {
t.Helper()
key, cert := testManagedCertificate(t, "example.com", time.Now().Add(-time.Hour), time.Now().Add(time.Hour), 2048)
ref, err := stageFlatSSLAtRoot(root, ManagedSSLModeUpload, key, cert, nil, nil)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { releaseManagedSSL(root, sslConfigForReference(ref)) })
return ref
}
func TestFlatSSLRetentionAndExpiryRecovery(t *testing.T) {
root := t.TempDir()
if err := InitializeDeployment(root); err != nil {
t.Fatal(err)
}
first, second, third := stageLifecycleCertificate(t, root), stageLifecycleCertificate(t, root), stageLifecycleCertificate(t, root)
for _, ref := range []ManagedSSLReference{first, second, third} {
releaseManagedSSL(root, sslConfigForReference(ref))
}
previous := sslConfigForReference(second)
previous.PreviousCertificateRef, previous.PreviousPrivateKeyRef = first.CertificateRef, first.PrivateKeyRef
active := sslConfigForReference(third)
active.Management, active.DefaultDirectory, active.CustomDirectory = SSLManagementCustom, "ssl", "ssl"
retainPreviousSSL(root, &active, previous)
manual := filepath.Join(root, "ssl", "manual-note.txt")
if err := os.WriteFile(manual, []byte("preserve"), 0600); err != nil {
t.Fatal(err)
}
maintainManagedSSL(root, active, previous)
if countFlatVersions(t, root) != 2 {
t.Fatal("稳定状态未立即收敛为当前与上一套")
}
current := validRuntimeConfig()
current.Root.Domains = DomainConfig{Primary: "example.com", Web: "example.com", Accepted: []string{"example.com"}}
current.SSL.TLS = active
writeRuntimeFixture(t, root, current)
manager, err := NewConfigManager(root, ConfigLoaderOptions{Validation: RuntimeValidationOptions{HasSuperAdmin: true}})
if err != nil {
t.Fatal(err)
}
if err := manager.clearExpiredPreviousSSL(); err != nil || manager.Snapshot().Generation != current.Generation {
t.Fatal("未到期证书不应清除引用", err)
}
key, cert := testManagedCertificate(t, "example.com", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), 2048)
if err := os.WriteFile(second.CertificatePath, cert, 0600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(second.PrivateKeyPath, key, 0600); err != nil {
t.Fatal(err)
}
maintainManagedSSL(root, active)
if countFlatVersions(t, root) != 2 {
t.Fatal("配置尚未解除过期引用就删除了材料")
}
manager.snapshots = &flakyRuntimeSnapshotStore{current: manager.Snapshot(), failures: 1}
if err := manager.clearExpiredPreviousSSL(); !errors.Is(err, ErrRuntimeSnapshotUnavailable) {
t.Fatalf("发布失败未留下恢复状态:%v", err)
}
if manager.pending == nil || !manager.pending.Committed {
t.Fatal("丢失已提交候选")
}
if err := manager.clearExpiredPreviousSSL(); err != nil {
t.Fatal("下轮维护未恢复发布", err)
}
loaded, err := manager.loader.Load()
if err != nil || loaded.Generation != current.Generation+1 || loaded.SSL.TLS.PreviousCertificateRef != "" {
t.Fatal("磁盘引用或代次不一致", err)
}
maintainManagedSSL(root, manager.Snapshot().SSL.TLS)
if countFlatVersions(t, root) != 1 {
t.Fatal("过期上一套未删除")
}
if _, err := os.Stat(manual); err != nil {
t.Fatal("误删未知文件", err)
}
}
func TestFlatSSLPreservesLeaseAndUnreadableActive(t *testing.T) {
root := t.TempDir()
first, second := stageLifecycleCertificate(t, root), stageLifecycleCertificate(t, root)
maintainManagedSSL(root, sslConfigForReference(second))
if countFlatVersions(t, root) != 2 {
t.Fatal("误删正在暂存的材料")
}
releaseManagedSSL(root, sslConfigForReference(first))
if err := os.WriteFile(second.CertificatePath, []byte("invalid"), 0600); err != nil {
t.Fatal(err)
}
maintainManagedSSL(root, sslConfigForReference(second))
if countFlatVersions(t, root) != 2 {
t.Fatal("当前证书损坏时仍删除了恢复材料")
}
}
func TestFlatSSLPrunePreservesMalformedAndRetriesOrphan(t *testing.T) {
root := t.TempDir()
active := stageLifecycleCertificate(t, root)
orphan, malformed := stageLifecycleCertificate(t, root), stageLifecycleCertificate(t, root)
releaseManagedSSL(root, sslConfigForReference(orphan))
releaseManagedSSL(root, sslConfigForReference(malformed))
if err := os.Remove(orphan.PrivateKeyPath); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(malformed.CertificatePath, []byte("preserve unknown material"), 0600); err != nil {
t.Fatal(err)
}
maintainManagedSSL(root, sslConfigForReference(active))
if _, err := os.Stat(orphan.CertificatePath); !os.IsNotExist(err) {
t.Fatal("删除中断后留下的证书没有清理", err)
}
if _, err := os.Stat(malformed.PrivateKeyPath); err != nil {
t.Fatal("无法确认的材料被误删", err)
}
}
func TestLegacySSLMigrationReusesIdenticalDirectFiles(t *testing.T) {
root := t.TempDir()
if err := InitializeDeployment(root); err != nil {
t.Fatal(err)
}
key, cert := testManagedCertificate(t, "example.com", time.Now().Add(-time.Hour), time.Now().Add(time.Hour), 2048, "*.example.com")
legacy, err := stageManagedSSLAtRoot(root, ManagedSSLModeUpload, key, cert, nil, nil)
if err != nil {
t.Fatal(err)
}
releaseManagedSSL(root, sslConfigForReference(legacy))
for name, data := range map[string][]byte{"example.com.crt": cert, "example.com.key": key} {
if err := os.WriteFile(filepath.Join(root, "ssl", name), data, 0600); err != nil {
t.Fatal(err)
}
}
current := validRuntimeConfig()
current.Root.Domains = DomainConfig{Primary: "example.com", Web: "example.com", Accepted: []string{"example.com"}}
current.SSL.TLS = sslConfigForReference(legacy)
current.SSL.TLS.Management, current.SSL.TLS.DefaultDirectory, current.SSL.TLS.CustomDirectory = SSLManagementCustom, "ssl", "ssl"
writeRuntimeFixture(t, root, current)
if err := MigrateLegacySSLStorage(root); err != nil {
t.Fatal(err)
}
loader := NewConfigLoader(root, ConfigLoaderOptions{Validation: RuntimeValidationOptions{HasSuperAdmin: true}})
loaded, err := loader.Load()
if err != nil || loaded.SSL.TLS.CertificateRef != "ssl/example.com.crt" || loaded.Generation != current.Generation+1 {
t.Fatal("迁移未使用同一原件或事务代次错误", err)
}
if err := MigrateLegacySSLStorage(root); err != nil {
t.Fatal("重复启动失败", err)
}
maintainManagedSSL(root, loaded.SSL.TLS)
if _, err := os.Stat(filepath.Join(root, "ssl", "managed")); !os.IsNotExist(err) {
t.Fatal("迁移完成仍保留旧副本目录", err)
}
if data, err := os.ReadFile(filepath.Join(root, "ssl", "example.com.key")); err != nil || !bytes.Equal(data, key) {
t.Fatal("迁移改变了原始私钥", err)
}
}
func TestLiveSSLSwitchKeepsConnectionAndRestoresPrevious(t *testing.T) {
root := t.TempDir()
oldRoot, oldPublished, oldFlag := ROOT_PATH, publishedConfig.Load(), hasPublishedConfig.Load()
oldManager := CurrentManager()
SetRuntimeManager(nil)
ROOT_PATH = root
t.Cleanup(func() {
ROOT_PATH = oldRoot
publishedConfig.Store(oldPublished)
hasPublishedConfig.Store(oldFlag)
SetRuntimeManager(oldManager)
})
first, second := stageLifecycleCertificate(t, root), stageLifecycleCertificate(t, root)
cfg := &Config{Domain: "example.com", WebDomain: "example.com", SSL: sslConfigForReference(first)}
PublishConfigSnapshot(cfg)
tlsConfig, err := NewRuntimeTLSConfig()
if err != nil {
t.Fatal(err)
}
listener, err := tls.Listen("tcp", "127.0.0.1:0", tlsConfig)
if err != nil {
t.Fatal(err)
}
defer listener.Close()
go func() {
for {
connection, err := listener.Accept()
if err != nil {
return
}
go func(connection net.Conn) {
defer connection.Close()
_ = connection.SetDeadline(time.Now().Add(10 * time.Second))
_, _ = io.Copy(connection, connection)
}(connection)
}
}()
dial := func() *tls.Conn {
t.Helper()
connection, err := tls.DialWithDialer(&net.Dialer{Timeout: 3 * time.Second}, "tcp", listener.Addr().String(), &tls.Config{InsecureSkipVerify: true})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { connection.Close() })
return connection
}
firstClient := dial()
oldSerial := firstClient.ConnectionState().PeerCertificates[0].SerialNumber.String()
cfg.SSL = sslConfigForReference(second)
cfg.SSL.PreviousCertificateRef, cfg.SSL.PreviousPrivateKeyRef = first.CertificateRef, first.PrivateKeyRef
PublishConfigSnapshot(cfg)
if err := ReloadRuntimeTLSCertificate(); err != nil {
t.Fatal(err)
}
if dial().ConnectionState().PeerCertificates[0].SerialNumber.String() == oldSerial {
t.Fatal("新握手仍使用旧证书")
}
_ = firstClient.SetDeadline(time.Now().Add(3 * time.Second))
if _, err := firstClient.Write([]byte("ping")); err != nil {
t.Fatal("换证断开已有连接", err)
}
message := make([]byte, 4)
if _, err := io.ReadFull(firstClient, message); err != nil || string(message) != "ping" {
t.Fatal("已有连接无法继续通信", err)
}
if err := os.WriteFile(second.CertificatePath, []byte("invalid"), 0600); err != nil {
t.Fatal(err)
}
liveTLS.Lock()
liveTLS.certificate = nil
liveTLS.Unlock()
if _, err := NewRuntimeTLSConfig(); err != nil {
t.Fatal("无内存缓存时无法恢复上一套有效证书", err)
}
if dial().ConnectionState().PeerCertificates[0].SerialNumber.String() != oldSerial {
t.Fatal("恢复握手没有使用上一套证书")
}
liveTLS.Lock()
liveTLS.certificate.Leaf.NotAfter = time.Now().Add(-time.Second)
liveTLS.Unlock()
if _, err := NewRuntimeTLSConfig(); err != nil {
t.Fatal("内存证书到期时没有重新尝试有效的上一套", err)
}
}