292 lines
9.6 KiB
Go
292 lines
9.6 KiB
Go
package config
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
func sslConfigForReference(ref ManagedSSLReference) SSLConfig {
|
||
return SSLConfig{Mode: ref.Mode, CertificateRef: ref.CertificateRef, PrivateKeyRef: ref.PrivateKeyRef}
|
||
}
|
||
|
||
func countFlatVersions(t *testing.T, root string) int {
|
||
t.Helper()
|
||
entries, err := os.ReadDir(filepath.Join(root, "ssl"))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
count := 0
|
||
for _, entry := range entries {
|
||
publicPath := filepath.Join(root, "ssl", entry.Name())
|
||
privatePath := strings.TrimSuffix(publicPath, ".crt") + ".key"
|
||
if flatSSLPair(root, publicPath, privatePath) {
|
||
if _, err := os.Stat(privatePath); err != nil {
|
||
t.Fatal("证书没有对应私钥", err)
|
||
}
|
||
count++
|
||
}
|
||
}
|
||
return count
|
||
}
|
||
|
||
func countManagedVersions(t *testing.T, root string) int {
|
||
t.Helper()
|
||
count := 0
|
||
for _, mode := range []string{ManagedSSLModeDefault, ManagedSSLModeUpload} {
|
||
path, err := managedSSLRootPathAtRoot(root, mode)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
entries, err := os.ReadDir(path)
|
||
if err != nil && !os.IsNotExist(err) {
|
||
t.Fatal(err)
|
||
}
|
||
for _, entry := range entries {
|
||
if validSSLVersion(entry.Name()) {
|
||
count++
|
||
}
|
||
}
|
||
}
|
||
return count
|
||
}
|
||
|
||
func TestSSLRetentionHandlesExpiredPreviousAndLegacyCurrent(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)
|
||
legacy, err := importManagedSSLAtRoot(root, ManagedSSLModeDefault, key, cert, nil, nil)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var refs []ManagedSSLReference
|
||
for index := 0; index < 4; index++ {
|
||
ref, err := stageManagedSSLAtRoot(root, ManagedSSLModeUpload, key, cert, nil, nil)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
releaseManagedSSL(root, sslConfigForReference(ref))
|
||
refs = append(refs, ref)
|
||
}
|
||
expiredKey, expiredCert := testManagedCertificate(t, "example.com", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), 2048)
|
||
if err := os.WriteFile(refs[2].PrivateKeyPath, expiredKey, 0600); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := os.WriteFile(refs[2].CertificatePath, expiredCert, 0600); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
maintainManagedSSL(root, sslConfigForReference(refs[3]), sslConfigForReference(refs[2]))
|
||
if countManagedVersions(t, root) != 2 {
|
||
t.Fatal("上一份证书过期阻止了清理")
|
||
}
|
||
if _, err := os.Stat(legacy.CertificatePath); !os.IsNotExist(err) {
|
||
t.Fatal("未被引用的旧 current 未清理")
|
||
}
|
||
if _, err := os.Stat(refs[2].CertificatePath); err != nil {
|
||
t.Fatal("清理删除了受保护的上一份证书")
|
||
}
|
||
old := validRuntimeConfig()
|
||
old.SSL.TLS = sslConfigForReference(legacy)
|
||
if err := validateRollbackSSL(root, old); !errors.Is(err, ErrSSLHistoryUnavailable) {
|
||
t.Fatal("历史 current 丢失后仍允许回滚")
|
||
}
|
||
}
|
||
|
||
func TestSSLRetentionProtectsIndependentLease(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)
|
||
var refs []ManagedSSLReference
|
||
for index := 0; index < 4; index++ {
|
||
ref, err := stageManagedSSLAtRoot(root, ManagedSSLModeUpload, key, cert, nil, nil)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
releaseManagedSSL(root, sslConfigForReference(ref))
|
||
refs = append(refs, ref)
|
||
}
|
||
lease, err := openSSLLease(refs[0].PrivateKeyPath)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer lease.Close()
|
||
unlock, err := lockStateFile(lease)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer unlock()
|
||
maintainManagedSSL(root, sslConfigForReference(refs[3]))
|
||
if _, err := os.Stat(refs[0].CertificatePath); err != nil {
|
||
t.Fatal("清理忽略了其他句柄持有的暂存租约")
|
||
}
|
||
if countManagedVersions(t, root) != 2 {
|
||
t.Fatal("暂存和生效证书以外的历史未清理")
|
||
}
|
||
}
|
||
|
||
func TestSSLRetentionKeepsTwoAcrossModesAndProtectsStaged(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)
|
||
var refs []ManagedSSLReference
|
||
for index := 0; index < 4; index++ {
|
||
mode := ManagedSSLModeDefault
|
||
if index%2 == 1 {
|
||
mode = ManagedSSLModeUpload
|
||
}
|
||
ref, err := stageManagedSSLAtRoot(root, mode, key, cert, nil, nil)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
refs = append(refs, ref)
|
||
if index > 0 {
|
||
maintainManagedSSL(root, sslConfigForReference(refs[index-1]))
|
||
if _, err := os.Stat(ref.PrivateKeyPath); err != nil {
|
||
t.Fatal("清理误删尚未提交的续期版本")
|
||
}
|
||
maintainManagedSSL(root, sslConfigForReference(ref), sslConfigForReference(refs[index-1]))
|
||
} else {
|
||
maintainManagedSSL(root, sslConfigForReference(ref))
|
||
}
|
||
want := index + 1
|
||
if want > 2 {
|
||
want = 2
|
||
}
|
||
if actual := countManagedVersions(t, root); actual != want {
|
||
t.Fatalf("跨模式保留总量错误:%d,期望 %d", actual, want)
|
||
}
|
||
}
|
||
old := validRuntimeConfig()
|
||
old.SSL.TLS = sslConfigForReference(refs[0])
|
||
if err := validateRollbackSSL(root, old); !errors.Is(err, ErrSSLHistoryUnavailable) {
|
||
t.Fatalf("旧证书已清理却允许回滚:%v", err)
|
||
}
|
||
// 即使当前证书的目录时间最旧,也必须保留当前证书。
|
||
if err := os.Chtimes(filepath.Dir(refs[2].CertificatePath), time.Unix(1, 0), time.Unix(1, 0)); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
maintainManagedSSL(root, sslConfigForReference(refs[2]))
|
||
if _, err := resolveManagedSSLReferencesAtRoot(root, sslConfigForReference(refs[2])); err != nil {
|
||
t.Fatal("清理删除了旧时间的生效证书")
|
||
}
|
||
}
|
||
|
||
func TestSSLRetentionDoesNotPartiallyDeleteUnknownFiles(t *testing.T) {
|
||
directory := filepath.Join(t.TempDir(), "version-00000000000000000000000000000000")
|
||
if err := os.Mkdir(directory, 0700); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
for _, name := range []string{"private.key", "public.crt", "zzz-user-note.txt"} {
|
||
if err := os.WriteFile(filepath.Join(directory, name), []byte("preserved"), 0600); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
if err := removeManagedSSLDirectory(directory); !errors.Is(err, ErrUnsafeSSLFile) {
|
||
t.Fatal("未知文件没有阻止删除")
|
||
}
|
||
for _, name := range []string{"private.key", "public.crt", "zzz-user-note.txt"} {
|
||
if _, err := os.Stat(filepath.Join(directory, name)); err != nil {
|
||
t.Fatal("预检期间已经删除了部分文件")
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestSSLRetentionRunsOnApplyCancelAndReload(t *testing.T) {
|
||
root, manager, request, _ := customSSLFixture(t)
|
||
var first RuntimeConfig
|
||
for index := 0; index < 4; index++ {
|
||
// 自定义保存不再产生副本;这里显式模拟自动续期的新版本。
|
||
certificate, err := os.ReadFile(filepath.Join(root, "external", "public.crt"))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
key, err := os.ReadFile(filepath.Join(root, "external", "private.key"))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
ref, err := stageFlatSSLAtRoot(root, ManagedSSLModeUpload, key, certificate, nil, nil)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var section SSLSectionConfig
|
||
if err := json.Unmarshal(request.Payload, §ion); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
section.TLS.CertificateRef, section.TLS.PrivateKeyRef = ref.CertificateRef, ref.PrivateKeyRef
|
||
request.Payload, _ = json.Marshal(section)
|
||
request.ExpectedGeneration = manager.Snapshot().Generation
|
||
updated, err := manager.Update(context.Background(), request)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := manager.ApplyPendingAuthorized(context.Background(), updated.Generation, updated.CandidateID, SectionSSL, request.Actor); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if index == 0 {
|
||
first = manager.Snapshot()
|
||
if _, err := manager.transactions.CreateBackup(first); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
maintainManagedSSL(root, manager.Snapshot().SSL.TLS)
|
||
if index > 0 && countFlatVersions(t, root) != 2 {
|
||
t.Fatal("配置应用完成后未自动保留两份")
|
||
}
|
||
}
|
||
if _, err := manager.PreviewRollback(context.Background(), first.Generation, request.Actor); !errors.Is(err, ErrSSLHistoryUnavailable) {
|
||
t.Fatalf("预览允许回滚已经清理的证书:%v", err)
|
||
}
|
||
if _, _, err := manager.rollbackWithState(context.Background(), first.Generation, request.Actor, StepUpConfirmation{Confirmed: true, Method: "ticket"}); !errors.Is(err, ErrSSLHistoryUnavailable) {
|
||
t.Fatalf("实际回滚没有拒绝已清理证书:%v", err)
|
||
}
|
||
request.ExpectedGeneration = manager.Snapshot().Generation
|
||
updated, err := manager.Update(context.Background(), request)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := manager.ManagePending(context.Background(), updated.Generation, updated.CandidateID, request.Actor, StepUpConfirmation{}, true); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if countFlatVersions(t, root) != 2 {
|
||
t.Fatal("取消候选未收敛到两份")
|
||
}
|
||
if err := manager.Load(context.Background()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if countFlatVersions(t, root) != 2 {
|
||
t.Fatal("重新加载后保留数量错误")
|
||
}
|
||
// 确认五分区仍可完整读取,证书删除不能导致摘要被直接修改。
|
||
data, _ := json.Marshal(manager.Snapshot().SSL)
|
||
if len(data) == 0 {
|
||
t.Fatal("配置快照不可读")
|
||
}
|
||
request.ExpectedGeneration = manager.Snapshot().Generation
|
||
updated, err = manager.Update(context.Background(), request)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
manager.releasePendingSSL()
|
||
path, err := resolveManagedSSLPathsAtRoot(root, updated.Candidate.SSL.TLS)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
managedSSLStorage.Lock()
|
||
stillLeased := managedSSLStorage.staged[path.CertificatePath] != nil
|
||
managedSSLStorage.Unlock()
|
||
if stillLeased {
|
||
t.Fatal("管理器停止后仍持有暂存租约")
|
||
}
|
||
}
|