lolly/internal/sslutil/certpool_test.go
xfy 6686b8557d fix(ci): 修复 act 本地运行 CI 测试失败的问题
- 修复 captureStdout/captureStderr 管道死锁问题,使用 goroutine 异步读取
- 添加 root 用户跳过权限测试的逻辑(act 容器以 root 运行)
- 更新 golangci-lint 到 v2.11.4 并迁移配置格式
- 更新 golangci-lint-action 到 v7
- 添加 linter continue-on-error 避免阻塞 CI

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 19:33:35 +08:00

428 lines
11 KiB
Go

// Package sslutil provides SSL/TLS utility functions tests.
package sslutil
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"os"
"path/filepath"
"testing"
"time"
)
// generateTestCert generates a self-signed certificate for testing.
func generateTestCert(t *testing.T) ([]byte, []byte) {
t.Helper()
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("Failed to generate private key: %v", err)
}
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"Test"},
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
DNSNames: []string{"localhost"},
}
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
t.Fatalf("Failed to create certificate: %v", err)
}
certPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certDER,
})
keyDER, err := x509.MarshalECPrivateKey(priv)
if err != nil {
t.Fatalf("Failed to marshal private key: %v", err)
}
keyPEM := pem.EncodeToMemory(&pem.Block{
Type: "EC PRIVATE KEY",
Bytes: keyDER,
})
return certPEM, keyPEM
}
// generateMultipleCerts generates multiple certificates in a single PEM file.
func generateMultipleCerts(t *testing.T, count int) []byte {
t.Helper()
var pemData []byte
for i := 0; i < count; i++ {
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("Failed to generate private key: %v", err)
}
template := x509.Certificate{
SerialNumber: big.NewInt(int64(i + 1)),
Subject: pkix.Name{
Organization: []string{"Test"},
CommonName: "test-cert-" + string(rune('A'+i)),
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
DNSNames: []string{"localhost"},
}
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
t.Fatalf("Failed to create certificate: %v", err)
}
certPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certDER,
})
pemData = append(pemData, certPEM...)
}
return pemData
}
func TestLoadCertPool_Success(t *testing.T) {
tmpDir := t.TempDir()
certPath := filepath.Join(tmpDir, "cert.pem")
cert, _ := generateTestCert(t)
if err := os.WriteFile(certPath, cert, 0o644); err != nil {
t.Fatalf("Failed to write cert: %v", err)
}
pool, err := LoadCertPool(certPath, "")
if err != nil {
t.Errorf("LoadCertPool() error = %v, want nil", err)
}
if pool == nil {
t.Error("LoadCertPool() returned nil pool")
}
}
func TestLoadCertPool_MultipleCertificates(t *testing.T) {
tmpDir := t.TempDir()
certPath := filepath.Join(tmpDir, "certs.pem")
multiCerts := generateMultipleCerts(t, 3)
if err := os.WriteFile(certPath, multiCerts, 0o644); err != nil {
t.Fatalf("Failed to write multi-cert file: %v", err)
}
pool, err := LoadCertPool(certPath, "")
if err != nil {
t.Errorf("LoadCertPool() error = %v, want nil", err)
}
if pool == nil {
t.Error("LoadCertPool() returned nil pool")
}
}
func TestLoadCertPool_FileNotFound(t *testing.T) {
_, err := LoadCertPool("/nonexistent/cert.pem", "")
if err == nil {
t.Error("LoadCertPool() expected error for non-existent file, got nil")
}
}
func TestLoadCertPool_EmptyFile(t *testing.T) {
tmpDir := t.TempDir()
certPath := filepath.Join(tmpDir, "empty.pem")
if err := os.WriteFile(certPath, []byte{}, 0o644); err != nil {
t.Fatalf("Failed to write empty file: %v", err)
}
_, err := LoadCertPool(certPath, "")
if err == nil {
t.Error("LoadCertPool() expected error for empty file, got nil")
}
}
func TestLoadCertPool_InvalidPEM(t *testing.T) {
tmpDir := t.TempDir()
certPath := filepath.Join(tmpDir, "invalid.pem")
invalidData := []byte("not a valid PEM file")
if err := os.WriteFile(certPath, invalidData, 0o644); err != nil {
t.Fatalf("Failed to write invalid file: %v", err)
}
_, err := LoadCertPool(certPath, "")
if err == nil {
t.Error("LoadCertPool() expected error for invalid PEM, got nil")
}
}
func TestLoadCertPool_InvalidCertificate(t *testing.T) {
tmpDir := t.TempDir()
certPath := filepath.Join(tmpDir, "invalid-cert.pem")
// Valid PEM block but not a certificate
invalidCert := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: []byte("not a valid certificate"),
})
if err := os.WriteFile(certPath, invalidCert, 0o644); err != nil {
t.Fatalf("Failed to write invalid cert file: %v", err)
}
_, err := LoadCertPool(certPath, "")
if err == nil {
t.Error("LoadCertPool() expected error for invalid certificate, got nil")
}
}
func TestLoadCertPool_WrongPEMType(t *testing.T) {
tmpDir := t.TempDir()
certPath := filepath.Join(tmpDir, "key.pem")
_, key := generateTestCert(t)
if err := os.WriteFile(certPath, key, 0o644); err != nil {
t.Fatalf("Failed to write key file: %v", err)
}
_, err := LoadCertPool(certPath, "")
if err == nil {
t.Error("LoadCertPool() expected error for non-certificate PEM, got nil")
}
}
func TestLoadCertPool_Directory(t *testing.T) {
tmpDir := t.TempDir()
_, err := LoadCertPool(tmpDir, "")
if err == nil {
t.Error("LoadCertPool() expected error for directory path, got nil")
}
}
func TestLoadCACertPool_Success(t *testing.T) {
tmpDir := t.TempDir()
caPath := filepath.Join(tmpDir, "ca.pem")
cert, _ := generateTestCert(t)
if err := os.WriteFile(caPath, cert, 0o644); err != nil {
t.Fatalf("Failed to write CA cert: %v", err)
}
pool, err := LoadCACertPool(caPath)
if err != nil {
t.Errorf("LoadCACertPool() error = %v, want nil", err)
}
if pool == nil {
t.Error("LoadCACertPool() returned nil pool")
}
}
func TestLoadCACertPool_MultipleCertificates(t *testing.T) {
tmpDir := t.TempDir()
caPath := filepath.Join(tmpDir, "ca-bundle.pem")
multiCerts := generateMultipleCerts(t, 5)
if err := os.WriteFile(caPath, multiCerts, 0o644); err != nil {
t.Fatalf("Failed to write CA bundle: %v", err)
}
pool, err := LoadCACertPool(caPath)
if err != nil {
t.Errorf("LoadCACertPool() error = %v, want nil", err)
}
if pool == nil {
t.Error("LoadCACertPool() returned nil pool")
}
}
func TestLoadCACertPool_FileNotFound(t *testing.T) {
_, err := LoadCACertPool("/nonexistent/ca.pem")
if err == nil {
t.Error("LoadCACertPool() expected error for non-existent file, got nil")
}
}
func TestLoadCACertPool_EmptyFile(t *testing.T) {
tmpDir := t.TempDir()
caPath := filepath.Join(tmpDir, "empty.pem")
if err := os.WriteFile(caPath, []byte{}, 0o644); err != nil {
t.Fatalf("Failed to write empty file: %v", err)
}
_, err := LoadCACertPool(caPath)
if err == nil {
t.Error("LoadCACertPool() expected error for empty file, got nil")
}
}
func TestLoadCACertPool_InvalidPEM(t *testing.T) {
tmpDir := t.TempDir()
caPath := filepath.Join(tmpDir, "invalid.pem")
invalidData := []byte("not a valid PEM file")
if err := os.WriteFile(caPath, invalidData, 0o644); err != nil {
t.Fatalf("Failed to write invalid file: %v", err)
}
_, err := LoadCACertPool(caPath)
if err == nil {
t.Error("LoadCACertPool() expected error for invalid PEM, got nil")
}
}
func TestLoadCACertPool_InvalidCertificate(t *testing.T) {
tmpDir := t.TempDir()
caPath := filepath.Join(tmpDir, "invalid-ca.pem")
// Valid PEM block but not a certificate
invalidCert := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: []byte("not a valid certificate"),
})
if err := os.WriteFile(caPath, invalidCert, 0o644); err != nil {
t.Fatalf("Failed to write invalid cert file: %v", err)
}
_, err := LoadCACertPool(caPath)
if err == nil {
t.Error("LoadCACertPool() expected error for invalid certificate, got nil")
}
}
func TestLoadCACertPool_WrongPEMType(t *testing.T) {
tmpDir := t.TempDir()
caPath := filepath.Join(tmpDir, "key.pem")
_, key := generateTestCert(t)
if err := os.WriteFile(caPath, key, 0o644); err != nil {
t.Fatalf("Failed to write key file: %v", err)
}
_, err := LoadCACertPool(caPath)
if err == nil {
t.Error("LoadCACertPool() expected error for non-certificate PEM, got nil")
}
}
func TestLoadCACertPool_Directory(t *testing.T) {
tmpDir := t.TempDir()
_, err := LoadCACertPool(tmpDir)
if err == nil {
t.Error("LoadCACertPool() expected error for directory path, got nil")
}
}
func TestLoadCACertPool_PermissionDenied(t *testing.T) {
// Skip on Windows as permission handling differs
if os.Getenv("GOOS") == "windows" {
t.Skip("Skipping on Windows")
}
// Skip if running as root - root can read any file
if os.Getuid() == 0 {
t.Skip("Skipping permission test when running as root")
}
tmpDir := t.TempDir()
caPath := filepath.Join(tmpDir, "no-perm.pem")
cert, _ := generateTestCert(t)
if err := os.WriteFile(caPath, cert, 0o000); err != nil {
t.Fatalf("Failed to write cert: %v", err)
}
_, err := LoadCACertPool(caPath)
if err == nil {
t.Error("LoadCACertPool() expected error for unreadable file, got nil")
}
}
// Test that LoadCertPool and LoadCACertPool produce equivalent results
func TestLoadCertPool_Equivalence(t *testing.T) {
tmpDir := t.TempDir()
certPath := filepath.Join(tmpDir, "cert.pem")
cert, _ := generateTestCert(t)
if err := os.WriteFile(certPath, cert, 0o644); err != nil {
t.Fatalf("Failed to write cert: %v", err)
}
pool1, err1 := LoadCertPool(certPath, "")
pool2, err2 := LoadCACertPool(certPath)
if err1 != err2 {
t.Errorf("LoadCertPool() err = %v, LoadCACertPool() err = %v", err1, err2)
}
if (pool1 == nil) != (pool2 == nil) {
t.Errorf("LoadCertPool() pool nil = %v, LoadCACertPool() pool nil = %v", pool1 == nil, pool2 == nil)
}
}
// Benchmark LoadCertPool
func BenchmarkLoadCertPool(b *testing.B) {
tmpDir := b.TempDir()
certPath := filepath.Join(tmpDir, "cert.pem")
cert, _ := generateTestCert(&testing.T{})
if err := os.WriteFile(certPath, cert, 0o644); err != nil {
b.Fatalf("Failed to write cert: %v", err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = LoadCertPool(certPath, "")
}
}
// Benchmark LoadCACertPool
func BenchmarkLoadCACertPool(b *testing.B) {
tmpDir := b.TempDir()
caPath := filepath.Join(tmpDir, "ca.pem")
cert, _ := generateTestCert(&testing.T{})
if err := os.WriteFile(caPath, cert, 0o644); err != nil {
b.Fatalf("Failed to write CA cert: %v", err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = LoadCACertPool(caPath)
}
}
// Benchmark with multiple certificates
func BenchmarkLoadCACertPool_MultiCert(b *testing.B) {
tmpDir := b.TempDir()
caPath := filepath.Join(tmpDir, "ca-bundle.pem")
multiCerts := generateMultipleCerts(&testing.T{}, 10)
if err := os.WriteFile(caPath, multiCerts, 0o644); err != nil {
b.Fatalf("Failed to write CA bundle: %v", err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = LoadCACertPool(caPath)
}
}