Scert/backend/internal/setup/dns_verification_test.go

430 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package setup
import (
"context"
"encoding/json"
"errors"
"strings"
"sync"
"testing"
"time"
)
type fakeDNSTester struct {
err error
}
func TestDNSVerificationStoresOnlyCurrentAliyunCredentials(t *testing.T) {
store := &memorySecretWriter{}
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
input := DNSVerificationInput{ProviderID: "aliyun", TestDomain: "www.example.com", Credentials: map[string]string{
"access_key_id": "test-id", "access_key_secret": "test-secret",
"managed_zones": "example.com", "allowed_domains": "www.example.com",
}}
first, err := manager.Verify(t.Context(), input)
if err != nil {
t.Fatal(err)
}
if _, ok := store.values[first.CredentialRef]["managed_zones"]; ok {
t.Fatal("legacy managed zone must not be persisted as a credential")
}
if _, ok := store.values[first.CredentialRef]["allowed_domains"]; ok {
t.Fatal("legacy allowed domain must not be persisted as a credential")
}
input.Credentials["access_key_secret"] = "new-secret"
second, err := manager.Verify(t.Context(), input)
if err != nil {
t.Fatal(err)
}
if first.CredentialRef == second.CredentialRef || store.values[first.CredentialRef]["access_key_secret"] != "test-secret" {
t.Fatal("credential changes must not overwrite an already verified reference")
}
if err := manager.Check(first.VerificationToken, "aliyun", second.CredentialRef, input.TestDomain); err == nil {
t.Fatal("old token must not authorize a new credential reference")
}
}
func (f fakeDNSTester) Verify(context.Context, string, string, map[string]string) (DNSVerificationReport, error) {
return DNSVerificationReport{}, f.err
}
type memorySecretWriter struct {
mu sync.Mutex
values map[string]map[string]string
deleted []string
putErr error
deleteErr error
}
func (m *memorySecretWriter) Put(_ context.Context, reference string, value map[string]string) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.values == nil {
m.values = map[string]map[string]string{}
}
copy := make(map[string]string, len(value))
for key, item := range value {
copy[key] = item
}
m.values[reference] = copy
return m.putErr
}
func (m *memorySecretWriter) Delete(_ context.Context, reference string) error {
m.mu.Lock()
defer m.mu.Unlock()
m.deleted = append(m.deleted, reference)
delete(m.values, reference)
return m.deleteErr
}
func TestDNSVerificationManagerIssuesOneTimeToken(t *testing.T) {
store := &memorySecretWriter{}
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
result, err := manager.Verify(t.Context(), DNSVerificationInput{
ProviderID: "cloudflare", TestDomain: "example.com",
Credentials: map[string]string{"api_token": "secret"},
})
if err != nil {
t.Fatalf("DNS 在线验证失败:%v", err)
}
if result.CredentialRef == "" || result.VerificationToken == "" {
t.Fatalf("DNS 验证结果不完整:%+v", result)
}
if len(store.values) != 1 {
t.Fatalf("DNS 凭据没有写入敏感信息存储:%v", store.values)
}
if err := manager.Check(result.VerificationToken, "cloudflare", result.CredentialRef, "example.com"); err != nil {
t.Fatalf("检查 DNS 验证令牌失败:%v", err)
}
if err := manager.Check(result.VerificationToken, "cloudflare", result.CredentialRef, "example.com"); err != nil {
t.Fatalf("检查 DNS 验证令牌不应消费令牌:%v", err)
}
if err := manager.Consume(result.VerificationToken, "cloudflare", result.CredentialRef, "example.com"); err != nil {
t.Fatalf("消费 DNS 验证令牌失败:%v", err)
}
if err := manager.Check(result.VerificationToken, "cloudflare", result.CredentialRef, "example.com"); err == nil {
t.Fatal("已消费的 DNS 验证令牌不应继续通过检查")
}
if err := manager.Consume(result.VerificationToken, "cloudflare", result.CredentialRef, "example.com"); err == nil {
t.Fatal("DNS 验证令牌只能使用一次")
}
}
func TestDNSVerificationManagerRejectsMismatchedConfiguration(t *testing.T) {
manager := NewDNSVerificationManager(fakeDNSTester{}, &memorySecretWriter{})
result, err := manager.Verify(t.Context(), DNSVerificationInput{
ProviderID: "cloudflare", TestDomain: "example.com",
Credentials: map[string]string{"api_token": "secret"},
})
if err != nil {
t.Fatalf("DNS 在线验证失败:%v", err)
}
if err := manager.Consume(result.VerificationToken, "cloudflare", result.CredentialRef, "other.example.com"); err == nil {
t.Fatal("DNS 验证结果不应允许绑定到其他域名")
}
}
func TestDNSVerificationDiscardOnlyRemovesSelectedToken(t *testing.T) {
store := &memorySecretWriter{}
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
first, err := manager.Verify(t.Context(), DNSVerificationInput{
ProviderID: "aliyun", TestDomain: "first.example.com",
Credentials: map[string]string{"access_key_id": "first-id", "access_key_secret": "first-secret"},
})
if err != nil {
t.Fatal(err)
}
second, err := manager.Verify(t.Context(), DNSVerificationInput{
ProviderID: "aliyun", TestDomain: "second.example.com",
Credentials: map[string]string{"access_key_id": "second-id", "access_key_secret": "second-secret"},
})
if err != nil {
t.Fatal(err)
}
if err := manager.Discard(first.VerificationToken); err != nil {
t.Fatalf("精确回收 DNS 验证令牌失败:%v", err)
}
if err := manager.Check(first.VerificationToken, "aliyun", first.CredentialRef, "first.example.com"); err == nil {
t.Fatal("已回收的 DNS 验证令牌仍然有效")
}
if err := manager.Check(second.VerificationToken, "aliyun", second.CredentialRef, "second.example.com"); err != nil {
t.Fatalf("精确回收影响了其他 DNS 验证令牌:%v", err)
}
store.mu.Lock()
_, firstExists := store.values[first.CredentialRef]
_, secondExists := store.values[second.CredentialRef]
store.mu.Unlock()
if firstExists || !secondExists {
t.Fatalf("DNS 凭据回收范围错误:first=%v second=%v", firstExists, secondExists)
}
if err := manager.Discard(first.VerificationToken); err == nil {
t.Fatal("DNS 验证令牌被重复回收")
}
}
func TestDNSVerificationDiscardAllRemovesPendingCredentials(t *testing.T) {
store := &memorySecretWriter{}
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
results := make([]DNSVerificationResult, 0, 2)
for _, testDomain := range []string{"first.example.com", "second.example.com"} {
result, err := manager.Verify(t.Context(), DNSVerificationInput{
ProviderID: "aliyun", TestDomain: testDomain,
Credentials: map[string]string{"access_key_id": "test-id", "access_key_secret": "test-secret"},
})
if err != nil {
t.Fatal(err)
}
results = append(results, result)
}
manager.DiscardAll()
for _, result := range results {
if err := manager.Check(result.VerificationToken, "aliyun", result.CredentialRef, "first.example.com"); err == nil {
t.Fatal("DiscardAll 后 DNS 验证令牌仍然有效")
}
}
store.mu.Lock()
remaining := len(store.values)
store.mu.Unlock()
if remaining != 0 {
t.Fatalf("DiscardAll 后仍有 DNS 临时凭据:%d", remaining)
}
}
func TestDNSVerificationDiscardAllExceptProtectsFormalCredential(t *testing.T) {
store := &memorySecretWriter{}
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
protected, err := manager.Verify(t.Context(), DNSVerificationInput{
ProviderID: "aliyun", TestDomain: "formal.example.com",
Credentials: map[string]string{"access_key_id": "formal-id", "access_key_secret": "formal-secret"},
})
if err != nil {
t.Fatal(err)
}
pending, err := manager.Verify(t.Context(), DNSVerificationInput{
ProviderID: "aliyun", TestDomain: "pending.example.com",
Credentials: map[string]string{"access_key_id": "pending-id", "access_key_secret": "pending-secret"},
})
if err != nil {
t.Fatal(err)
}
manager.DiscardAllExcept(" " + protected.CredentialRef + " ")
if _, err := manager.Resolve(protected.VerificationToken); err == nil {
t.Fatal("批量清理后受保护凭据对应的验证令牌仍然有效")
}
if _, err := manager.Resolve(pending.VerificationToken); err == nil {
t.Fatal("批量清理后待处理凭据对应的验证令牌仍然有效")
}
store.mu.Lock()
_, protectedExists := store.values[protected.CredentialRef]
_, pendingExists := store.values[pending.CredentialRef]
store.mu.Unlock()
if !protectedExists || pendingExists {
t.Fatalf("DNS 凭据保护范围错误:protected=%v pending=%v", protectedExists, pendingExists)
}
}
func TestDNSVerificationManagerRejectsExpiredToken(t *testing.T) {
store := &memorySecretWriter{}
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
now := time.Date(2026, 8, 26, 8, 0, 0, 0, time.UTC)
manager.now = func() time.Time { return now }
result, err := manager.Verify(t.Context(), DNSVerificationInput{
ProviderID: "cloudflare", TestDomain: "example.com",
Credentials: map[string]string{"api_token": "secret"},
})
if err != nil {
t.Fatalf("DNS 在线验证失败:%v", err)
}
now = now.Add(dnsVerificationLifetime + time.Second)
if err := manager.Consume(result.VerificationToken, "cloudflare", result.CredentialRef, "example.com"); err == nil {
t.Fatal("过期 DNS 验证令牌应被拒绝")
}
if len(store.deleted) != 1 || store.deleted[0] != result.CredentialRef {
t.Fatalf("expired session did not delete temporary credential: %v", store.deleted)
}
}
func TestDNSVerificationManagerDeletesCredentialWhenPutReportsFailure(t *testing.T) {
store := &memorySecretWriter{putErr: errors.New("write failed")}
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
_, err := manager.Verify(t.Context(), DNSVerificationInput{
ProviderID: "cloudflare", TestDomain: "example.com",
Credentials: map[string]string{"api_token": "secret"},
})
if err == nil {
t.Fatal("Put failure unexpectedly succeeded")
}
if len(store.deleted) != 1 {
t.Fatalf("Put failure did not trigger credential cleanup: %v", store.deleted)
}
if len(store.values) != 0 {
t.Fatalf("Put failure left temporary credentials stored: %v", store.values)
}
}
type countingDNSTester struct {
mu sync.Mutex
calls int
block <-chan struct{}
}
func (f *countingDNSTester) Verify(_ context.Context, _ string, _ string, _ map[string]string) (DNSVerificationReport, error) {
f.mu.Lock()
f.calls++
f.mu.Unlock()
if f.block != nil {
<-f.block
}
return DNSVerificationReport{}, nil
}
func (f *countingDNSTester) CallCount() int {
f.mu.Lock()
defer f.mu.Unlock()
return f.calls
}
func TestDNSVerificationManagerRejectsCapacityBeforeTestingOrStoring(t *testing.T) {
tester := &countingDNSTester{}
store := &memorySecretWriter{}
manager := NewDNSVerificationManager(tester, store)
manager.sessions = make(map[string]dnsVerificationSession, dnsVerificationSessionCapacity)
expiresAt := time.Now().UTC().Add(time.Hour)
for index := 0; index < dnsVerificationSessionCapacity; index++ {
manager.sessions[string(rune(index))] = dnsVerificationSession{CredentialRef: "secret://existing", ExpiresAt: expiresAt}
}
_, err := manager.Verify(t.Context(), DNSVerificationInput{
ProviderID: "cloudflare", TestDomain: "example.com", Credentials: map[string]string{"api_token": "secret"},
})
if err == nil || tester.CallCount() != 0 || len(store.values) != 0 {
t.Fatalf("capacity rejection must precede testing and storage: err=%v calls=%d values=%v", err, tester.CallCount(), store.values)
}
}
func TestDNSVerificationManagerRejectsInFlightCapacityBeforeTestingOrStoring(t *testing.T) {
tester := &countingDNSTester{}
store := &memorySecretWriter{}
manager := NewDNSVerificationManager(tester, store)
manager.inFlight = dnsVerificationInFlightCapacity
_, err := manager.Verify(t.Context(), DNSVerificationInput{
ProviderID: "cloudflare", TestDomain: "example.com", Credentials: map[string]string{"api_token": "secret"},
})
if err == nil || tester.CallCount() != 0 || len(store.values) != 0 {
t.Fatalf("in-flight rejection must precede testing and storage: err=%v calls=%d values=%v", err, tester.CallCount(), store.values)
}
}
func TestDNSVerificationManagerConsumesTokenOnlyOnceUnderConcurrency(t *testing.T) {
manager := NewDNSVerificationManager(fakeDNSTester{}, &memorySecretWriter{})
result, err := manager.Verify(t.Context(), DNSVerificationInput{
ProviderID: "cloudflare", TestDomain: "example.com", Credentials: map[string]string{"api_token": "secret"},
})
if err != nil {
t.Fatal(err)
}
start := make(chan struct{})
results := make(chan error, 2)
for range 2 {
go func() {
<-start
results <- manager.Consume(result.VerificationToken, "cloudflare", result.CredentialRef, "example.com")
}()
}
close(start)
successes := 0
for range 2 {
if <-results == nil {
successes++
}
}
if successes != 1 {
t.Fatalf("concurrent Consume successes = %d; want 1", successes)
}
}
func TestDNSVerificationManagerDoesNotStoreFailedCredential(t *testing.T) {
store := &memorySecretWriter{}
manager := NewDNSVerificationManager(fakeDNSTester{err: errors.New("权限不足")}, store)
_, err := manager.Verify(t.Context(), DNSVerificationInput{
ProviderID: "cloudflare", TestDomain: "example.com",
Credentials: map[string]string{"api_token": "secret"},
})
if err == nil {
t.Fatal("DNS 测试失败时应返回错误")
}
if len(store.values) != 0 {
t.Fatal("DNS 测试失败时不应保存凭据")
}
}
type recordingDNSTester struct {
credentials map[string]string
}
func (f *recordingDNSTester) Verify(_ context.Context, _ string, _ string, credentials map[string]string) (DNSVerificationReport, error) {
f.credentials = credentials
return DNSVerificationReport{}, nil
}
func TestDNSVerificationManagerNormalizesCredentialCopyBeforeTestingAndStoring(t *testing.T) {
store := &memorySecretWriter{}
tester := &recordingDNSTester{}
manager := NewDNSVerificationManager(tester, store)
inputCredentials := map[string]string{" api_token ": " secret-token "}
result, err := manager.Verify(t.Context(), DNSVerificationInput{
ProviderID: "cloudflare", TestDomain: "example.com", Credentials: inputCredentials,
})
if err != nil {
t.Fatalf("DNS 在线验证失败:%v", err)
}
if tester.credentials["api_token"] != "secret-token" {
t.Fatalf("传给测试器的凭据未规范化:%v", tester.credentials)
}
if _, ok := tester.credentials[" api_token "]; ok {
t.Fatal("测试器不应收到带首尾空格的凭据键")
}
stored := store.values[result.CredentialRef]
if stored["api_token"] != "secret-token" {
t.Fatalf("保存的凭据未规范化:%v", stored)
}
if inputCredentials[" api_token "] != " secret-token " {
t.Fatal("规范化过程不应修改调用方的原始凭据 Map")
}
}
func TestDNSVerificationResolveIsServerOnlyAndDoesNotConsumeToken(t *testing.T) {
store := &memorySecretWriter{}
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
result, err := manager.Verify(t.Context(), DNSVerificationInput{
ProviderID: "aliyun", TestDomain: "www.example.com",
Credentials: map[string]string{"access_key_id": "test-id", "access_key_secret": "test-secret"},
})
if err != nil {
t.Fatal(err)
}
resolved, err := manager.Resolve(result.VerificationToken)
if err != nil {
t.Fatalf("按令牌解析 DNS 验证结果失败:%v", err)
}
if resolved.ProviderID != "aliyun" || resolved.TestDomain != "www.example.com" ||
resolved.CredentialRef != result.CredentialRef || resolved.ExpiresAt != result.ExpiresAt {
t.Fatalf("解析的 DNS 验证状态不完整:%+v", resolved)
}
encoded, err := json.Marshal(resolved)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(encoded), result.CredentialRef) || strings.Contains(string(encoded), resolved.TestDomain) {
t.Fatalf("服务端 DNS 验证状态可被意外序列化:%s", encoded)
}
if err := manager.Consume(result.VerificationToken, resolved.ProviderID, resolved.CredentialRef, resolved.TestDomain); err != nil {
t.Fatalf("Resolve 不应消费 DNS 验证令牌:%v", err)
}
if _, err := manager.Resolve(result.VerificationToken); err == nil {
t.Fatal("已消费的 DNS 验证令牌仍可被解析")
}
}