Smail/backend/services/setup/ssl/user_certificate_test.go
smail 1b2ba5a6a1
Some checks are pending
Security Checks / security (push) Waiting to run
修复投递结果恢复并隔离远程验收资源
2026-09-17 11:21:49 +08:00

206 lines
7.4 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`,
`C:private.key`,
`\\server\share\private.key`,
`//server/share/public.crt`,
}
for _, value := range cases {
if !isAbsoluteUserCertificatePath(value) {
t.Errorf("path %q was not recognized", 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 TestValidateUserCertificatePathsRejectsNonNativeAbsolutePath(t *testing.T) {
for _, path := range []string{`C:private.key`, `C:\outside\private.key`, `\\server\share\private.key`} {
if filepath.IsAbs(path) {
continue
}
if _, _, err := validateUserCertificatePaths(path, path); err == nil || err.Error() != "SSL 路径必须是当前服务器支持的完整绝对路径" {
t.Fatalf("non-native path %q: %v", path, err)
}
}
}
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 != "ssl" {
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)
}
}