Smail/server/config/ssl_storage_test.go

229 lines
9.9 KiB
Go

package config
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"math/big"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
)
func TestImportManagedSSLUsesInternalReferencesAndAtomicCurrent(t *testing.T) {
oldRoot := ROOT_PATH
ROOT_PATH = t.TempDir()
t.Cleanup(func() { ROOT_PATH = oldRoot })
privateKeyPEM, certificatePEM := testManagedCertificate(t, "mail.example.com", time.Now().Add(-time.Hour), time.Now().Add(24*time.Hour), 2048)
first, err := ImportManagedSSL(ManagedSSLModeDefault, privateKeyPEM, certificatePEM, nil, []string{"mail.example.com"})
if err != nil {
t.Fatal(err)
}
if filepath.IsAbs(first.CertificateRef) || filepath.IsAbs(first.PrivateKeyRef) {
t.Fatal("SSL 内部引用不应为绝对路径")
}
if first.CertificateRef != "config/ssl/managed/default/current/public.crt" || first.PrivateKeyRef != "config/ssl/managed/default/current/private.key" {
t.Fatalf("内部引用不符合预期: %+v", first)
}
resolved, err := ResolveManagedSSLReferences(SSLConfig{Mode: ManagedSSLModeDefault, DefaultDirectory: "config/ssl", CertificateRef: first.CertificateRef, PrivateKeyRef: first.PrivateKeyRef})
if err != nil {
t.Fatal(err)
}
if resolved.CertificatePath != first.CertificatePath || resolved.PrivateKeyPath != first.PrivateKeyPath {
t.Fatal("解析后的受控路径与导入结果不一致")
}
secondKey, secondCertificate := testManagedCertificate(t, "mail.example.com", time.Now().Add(-time.Hour), time.Now().Add(24*time.Hour), 2048)
second, err := ImportManagedSSL(ManagedSSLModeDefault, secondKey, secondCertificate, nil, []string{"mail.example.com"})
if err != nil {
t.Fatal(err)
}
if second.CertificatePath != first.CertificatePath {
t.Fatal("current 引用路径应保持稳定")
}
currentData, err := os.ReadFile(second.CertificatePath)
if err != nil {
t.Fatal(err)
}
if string(currentData) == string(certificatePEM) {
t.Fatal("current 目录没有切换到新证书")
}
previousMatches := false
entries, err := os.ReadDir(filepath.Dir(filepath.Dir(second.CertificatePath)))
if err != nil {
t.Fatal(err)
}
for _, entry := range entries {
if !strings.HasPrefix(entry.Name(), ".previous-") {
continue
}
previousData, readErr := os.ReadFile(filepath.Join(filepath.Dir(filepath.Dir(second.CertificatePath)), entry.Name(), "public.crt"))
if readErr == nil && string(previousData) == string(certificatePEM) {
previousMatches = true
}
}
if !previousMatches {
t.Fatal("更新后未保留旧证书目录")
}
}
func TestResolveRuntimeSSLPathsUsesManagedReferences(t *testing.T) {
oldRoot := ROOT_PATH
ROOT_PATH = t.TempDir()
t.Cleanup(func() { ROOT_PATH = oldRoot })
privateKeyPEM, certificatePEM := testManagedCertificate(t, "mail.example.com", time.Now().Add(-time.Hour), time.Now().Add(24*time.Hour), 2048)
reference, err := ImportManagedSSL(ManagedSSLModeDefault, privateKeyPEM, certificatePEM, nil, []string{"mail.example.com"})
if err != nil {
t.Fatal(err)
}
cfg := &Config{SSL: SSLConfig{Mode: ManagedSSLModeDefault, CertificateRef: reference.CertificateRef, PrivateKeyRef: reference.PrivateKeyRef}}
certificatePath, privateKeyPath, err := ResolveRuntimeSSLPaths(cfg)
if err != nil {
t.Fatal(err)
}
if certificatePath != reference.CertificatePath || privateKeyPath != reference.PrivateKeyPath {
t.Fatalf("resolved paths = %q, %q", certificatePath, privateKeyPath)
}
if _, err := LoadRuntimeTLSCertificate(cfg); err != nil {
t.Fatal(err)
}
}
func TestResolveRuntimeSSLPathsRejectsPartialManagedConfiguration(t *testing.T) {
_, _, err := ResolveRuntimeSSLPaths(&Config{SSL: SSLConfig{Mode: ManagedSSLModeDefault, CertificateRef: "config/ssl/managed/default/current/public.crt"}})
if !errors.Is(err, ErrInvalidSSLReference) {
t.Fatalf("error = %v, want %v", err, ErrInvalidSSLReference)
}
}
func TestResolveRuntimeSSLPathsValidatesLegacyFiles(t *testing.T) {
oldRoot := ROOT_PATH
ROOT_PATH = t.TempDir()
t.Cleanup(func() { ROOT_PATH = oldRoot })
privateKeyPEM, certificatePEM := testManagedCertificate(t, "mail.example.com", time.Now().Add(-time.Hour), time.Now().Add(24*time.Hour), 2048)
directory := filepath.Join(t.TempDir(), "legacy-ssl")
if err := os.MkdirAll(directory, 0700); err != nil {
t.Fatal(err)
}
privateKeyPath := filepath.Join(directory, "private.key")
certificatePath := filepath.Join(directory, "public.crt")
if err := os.WriteFile(privateKeyPath, privateKeyPEM, 0600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(certificatePath, certificatePEM, 0600); err != nil {
t.Fatal(err)
}
resolvedCertificate, resolvedPrivate, err := ResolveRuntimeSSLPaths(&Config{SSLPublicKeyPath: certificatePath, SSLPrivateKeyPath: privateKeyPath})
if err != nil {
t.Fatal(err)
}
if resolvedCertificate != certificatePath || resolvedPrivate != privateKeyPath {
t.Fatalf("resolved paths = %q, %q", resolvedCertificate, resolvedPrivate)
}
}
func TestResolveManagedSSLReferencesRejectsUnsafeReferences(t *testing.T) {
oldRoot := ROOT_PATH
ROOT_PATH = t.TempDir()
t.Cleanup(func() { ROOT_PATH = oldRoot })
privateKeyPEM, certificatePEM := testManagedCertificate(t, "mail.example.com", time.Now().Add(-time.Hour), time.Now().Add(24*time.Hour), 2048)
reference, err := ImportManagedSSL(ManagedSSLModeUpload, privateKeyPEM, certificatePEM, nil, nil)
if err != nil {
t.Fatal(err)
}
unsafe := []SSLConfig{
{Mode: ManagedSSLModeUpload, CertificateRef: filepath.Join(filepath.Dir(reference.CertificateRef), "..", "public.crt"), PrivateKeyRef: reference.PrivateKeyRef},
{Mode: ManagedSSLModeUpload, CertificateRef: filepath.Join(t.TempDir(), "public.crt"), PrivateKeyRef: reference.PrivateKeyRef},
{Mode: ManagedSSLModeUpload, CertificateRef: reference.CertificateRef, PrivateKeyRef: reference.CertificateRef},
{Mode: ManagedSSLModeUpload, CertificateRef: `C:\outside\public.crt`, PrivateKeyRef: reference.PrivateKeyRef},
{Mode: ManagedSSLModeUpload, CertificateRef: `\\server\share\public.crt`, PrivateKeyRef: reference.PrivateKeyRef},
}
for index, candidate := range unsafe {
if _, err := ResolveManagedSSLReferences(candidate); !errors.Is(err, ErrInvalidSSLReference) {
t.Fatalf("用例 %d error = %v, want %v", index, err, ErrInvalidSSLReference)
}
}
}
func TestResolveManagedSSLReferencesRejectsSymlinkAndBroadPrivatePermissions(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("当前测试依赖 Unix 文件权限和符号链接")
}
oldRoot := ROOT_PATH
ROOT_PATH = t.TempDir()
t.Cleanup(func() { ROOT_PATH = oldRoot })
privateKeyPEM, certificatePEM := testManagedCertificate(t, "mail.example.com", time.Now().Add(-time.Hour), time.Now().Add(24*time.Hour), 2048)
reference, err := ImportManagedSSL(ManagedSSLModeDefault, privateKeyPEM, certificatePEM, nil, nil)
if err != nil {
t.Fatal(err)
}
if err := os.Chmod(reference.PrivateKeyPath, 0640); err != nil {
t.Fatal(err)
}
if _, err := ResolveManagedSSLReferences(SSLConfig{Mode: ManagedSSLModeDefault, CertificateRef: reference.CertificateRef, PrivateKeyRef: reference.PrivateKeyRef}); !errors.Is(err, ErrSSLPrivateKeyPermission) {
t.Fatalf("私钥权限 error = %v, want %v", err, ErrSSLPrivateKeyPermission)
}
if err := os.Chmod(reference.PrivateKeyPath, 0600); err != nil {
t.Fatal(err)
}
outside := filepath.Join(t.TempDir(), "outside.crt")
if err := os.WriteFile(outside, certificatePEM, 0600); err != nil {
t.Fatal(err)
}
if err := os.Remove(reference.CertificatePath); err != nil {
t.Fatal(err)
}
if err := os.Symlink(outside, reference.CertificatePath); err != nil {
t.Skipf("当前环境不支持符号链接测试: %v", err)
}
if _, err := ResolveManagedSSLReferences(SSLConfig{Mode: ManagedSSLModeDefault, CertificateRef: reference.CertificateRef, PrivateKeyRef: reference.PrivateKeyRef}); !errors.Is(err, ErrUnsafeSSLFile) {
t.Fatalf("符号链接 error = %v, want %v", err, ErrUnsafeSSLFile)
}
}
func TestValidateSSLMaterialRejectsDomainExpiryAndWeakKeys(t *testing.T) {
privateKeyPEM, certificatePEM := testManagedCertificate(t, "mail.example.com", time.Now().Add(-time.Hour), time.Now().Add(24*time.Hour), 2048)
if err := ValidateSSLMaterial(privateKeyPEM, certificatePEM, nil, []string{"other.example.com"}); !errors.Is(err, ErrSSLCertificateDomain) {
t.Fatalf("域名不匹配 error = %v, want %v", err, ErrSSLCertificateDomain)
}
expiredKey, expiredCertificate := testManagedCertificate(t, "mail.example.com", time.Now().Add(-48*time.Hour), time.Now().Add(-time.Hour), 2048)
if err := ValidateSSLMaterial(expiredKey, expiredCertificate, nil, nil); !errors.Is(err, ErrSSLCertificateExpired) {
t.Fatalf("过期证书 error = %v, want %v", err, ErrSSLCertificateExpired)
}
weakKey, weakCertificate := testManagedCertificate(t, "mail.example.com", time.Now().Add(-time.Hour), time.Now().Add(24*time.Hour), 1024)
if err := ValidateSSLMaterial(weakKey, weakCertificate, nil, nil); !errors.Is(err, ErrInvalidSSLMaterial) {
t.Fatalf("弱密钥 error = %v, want %v", err, ErrInvalidSSLMaterial)
}
}
func testManagedCertificate(t *testing.T, domain string, notBefore, notAfter time.Time, bits int) ([]byte, []byte) {
t.Helper()
key, err := rsa.GenerateKey(rand.Reader, bits)
if err != nil {
t.Fatal(err)
}
template := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().UnixNano()),
Subject: pkix.Name{CommonName: domain},
DNSNames: []string{domain},
NotBefore: notBefore,
NotAfter: notAfter,
BasicConstraintsValid: true,
IsCA: true,
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
if err != nil {
t.Fatal(err)
}
return pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}), pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
}