206 lines
7.4 KiB
Go
206 lines
7.4 KiB
Go
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)
|
||
}
|
||
}
|