Smail/backend/services/setup/ssl/user_certificate_test.go
smail f60d3b0880
Some checks are pending
Security Checks / security (push) Waiting to run
整理前后端与测试构建目录并完善配置和安全引导
统一 frontend、backend、test、test-log 和 out 目录,更新构建、部署及测试入口;纳入配置管理兼容与首次安全引导修复;排除本机配置、密钥和测试产物。
2026-09-05 19:19:52 +08:00

194 lines
7.0 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 ssl
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"os"
"path/filepath"
"testing"
"time"
"git.pcux.cn/xuans/Smail/config"
)
func writeTestCertificate(t *testing.T, root string, bits int) (string, string) {
t.Helper()
key, err := rsa.GenerateKey(rand.Reader, bits)
if err != nil {
t.Fatal(err)
}
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "mail.example.com"},
DNSNames: []string{"mail.example.com"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
if err != nil {
t.Fatal(err)
}
directory := filepath.Join(root, "config", "ssl")
if err := os.MkdirAll(directory, 0700); err != nil {
t.Fatal(err)
}
keyPath := filepath.Join(directory, "private.key")
certPath := filepath.Join(directory, "public.crt")
if err := os.WriteFile(keyPath, pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}), 0600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(certPath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0644); err != nil {
t.Fatal(err)
}
return keyPath, certPath
}
func TestIsAbsoluteUserCertificatePathRecognizesWindowsPaths(t *testing.T) {
cases := []string{
`C:\outside\private.key`,
`D:/outside/public.crt`,
`\\server\share\private.key`,
`//server/share/public.crt`,
}
for _, value := range cases {
if !isAbsoluteUserCertificatePath(value) {
t.Errorf("path %q was accepted", value)
}
}
}
func TestIsAbsoluteUserCertificatePathRejectsEmptyAndAcceptsRelative(t *testing.T) {
if isAbsoluteUserCertificatePath("") {
t.Fatal("empty path must not be treated as absolute")
}
if isAbsoluteUserCertificatePath(`config/ssl/upload/private.key`) {
t.Fatal("controlled relative path must not be treated as absolute")
}
}
func TestApplyManagedSSLReferenceDoesNotPersistLegacyPaths(t *testing.T) {
cfg := &config.Config{
SSLPrivateKeyPath: "config/ssl/private.key",
SSLPublicKeyPath: "config/ssl/public.crt",
}
reference := config.ManagedSSLReference{
Mode: config.ManagedSSLModeUpload,
CertificateRef: "config/ssl/managed/upload/current/public.crt",
PrivateKeyRef: "config/ssl/managed/upload/current/private.key",
CertificatePath: `C:\server\config\ssl\managed\upload\current\public.crt`,
PrivateKeyPath: `C:\server\config\ssl\managed\upload\current\private.key`,
}
applyManagedSSLReference(cfg, reference)
if cfg.SSLPrivateKeyPath != "" || cfg.SSLPublicKeyPath != "" {
t.Fatal("新受控 SSL 配置不应保留旧聚合路径")
}
if cfg.SSL.Mode != config.ManagedSSLModeUpload || cfg.SSL.CertificateRef != reference.CertificateRef || cfg.SSL.PrivateKeyRef != reference.PrivateKeyRef {
t.Fatalf("受控 SSL 内部引用写入错误: %+v", cfg.SSL)
}
if cfg.SSL.CustomDirectory != "config/ssl/managed/upload" {
t.Fatalf("自定义受控目录写入错误: %s", cfg.SSL.CustomDirectory)
}
}
func TestNormalizeUserCertificateImportError(t *testing.T) {
tests := []struct {
name string
err error
message string
}{
{name: "certificate mismatch", err: config.ErrSSLCertificateMismatch, message: "SSL 证书与私钥不匹配"},
{name: "domain mismatch", err: fmt.Errorf("%w: web.xuan.com", config.ErrSSLCertificateDomain), message: "SSL 证书与已配置域名不匹配,请检查主域名、Web 域名和附加域名"},
{name: "certificate expired", err: config.ErrSSLCertificateExpired, message: "SSL 证书无效、尚未生效或已经过期"},
{name: "private key permission", err: config.ErrSSLPrivateKeyPermission, message: "SSL 私钥权限过于宽松,请限制为仅 Smail 运行用户可读取"},
{name: "invalid material", err: config.ErrInvalidSSLMaterial, message: "SSL 证书或私钥文件无效"},
{name: "unsafe file", err: config.ErrUnsafeSSLFile, message: "SSL 证书或私钥文件无效"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
message := UserCertificateErrorMessage(normalizeUserCertificateImportError(test.err))
if message != test.message {
t.Fatalf("证书错误提示不匹配: got %q, want %q", message, test.message)
}
})
}
unknown := fmt.Errorf("unknown certificate import error")
if normalized := normalizeUserCertificateImportError(unknown); normalized != unknown {
t.Fatalf("未知错误不应被替换: %v", normalized)
}
}
func TestValidateUserCertificatePaths(t *testing.T) {
oldRoot := config.ROOT_PATH
root := t.TempDir()
config.ROOT_PATH = root + string(os.PathSeparator)
t.Cleanup(func() {
config.ROOT_PATH = oldRoot
})
keyPath, certPath := writeTestCertificate(t, root, 2048)
if _, _, err := validateUserCertificatePaths(keyPath, certPath); err != nil {
t.Fatalf("valid certificate was rejected: %v", err)
}
externalSourceRoot := t.TempDir()
externalKey, externalCert := writeTestCertificate(t, externalSourceRoot, 2048)
if _, _, err := validateUserCertificatePaths(externalKey, externalCert); err != nil {
t.Fatalf("certificate in an arbitrary directory was rejected: %v", err)
}
relativeKey, err := filepath.Rel(root, externalKey)
if err != nil {
t.Fatal(err)
}
relativeCert, err := filepath.Rel(root, externalCert)
if err != nil {
t.Fatal(err)
}
if _, _, err := validateUserCertificatePaths(relativeKey, relativeCert); err != nil {
t.Fatalf("certificate in an arbitrary relative directory was rejected: %v", err)
}
weakKey, weakCert := writeTestCertificate(t, root, 1024)
if _, _, err := validateUserCertificatePaths(weakKey, weakCert); err == nil {
t.Fatal("weak certificate was accepted")
}
invalid := filepath.Join(t.TempDir(), "invalid.key")
if err := os.WriteFile(invalid, []byte("invalid"), 0600); err != nil {
t.Fatal(err)
}
if _, _, err := validateUserCertificatePaths(invalid, certPath); err == nil {
t.Fatal("invalid certificate data was accepted")
}
}
func TestValidateUserCertificatePathsAcceptsParentSymlink(t *testing.T) {
oldRoot := config.ROOT_PATH
root := t.TempDir()
config.ROOT_PATH = root + string(os.PathSeparator)
t.Cleanup(func() { config.ROOT_PATH = oldRoot })
sslDirectory := filepath.Join(root, "config", "ssl")
if err := os.MkdirAll(sslDirectory, 0700); err != nil {
t.Fatal(err)
}
outsideRoot := t.TempDir()
outsideKey, outsideCert := writeTestCertificate(t, outsideRoot, 2048)
linkPath := filepath.Join(sslDirectory, "linked")
if err := os.Symlink(filepath.Dir(outsideKey), linkPath); err != nil {
t.Skipf("symlink creation is unavailable: %v", err)
}
linkedKey := filepath.Join(linkPath, filepath.Base(outsideKey))
linkedCert := filepath.Join(linkPath, filepath.Base(outsideCert))
if _, _, err := validateUserCertificatePaths(linkedKey, linkedCert); err != nil {
t.Fatalf("certificate paths through a parent symlink were rejected: %v", err)
}
}