lolly/internal/ssl/ssl.go

417 lines
10 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 提供 SSL/TLS 支持。
//
// 该文件包含 TLS 配置管理的核心逻辑,包括:
// - 安全的 TLS 默认配置(仅 TLSv1.2 和 TLSv1.3
// - 证书加载和管理
// - SNI服务器名称指示支持
// - OCSP Stapling 支持
//
// 主要用途:
//
// 用于管理 HTTPS 服务器的 TLS 配置,支持多证书虚拟主机。
//
// 安全默认值:
// - TLS 版本:仅启用 TLSv1.2 和 TLSv1.3
// - TLSv1.0 和 TLSv1.1 被强制禁用(不安全)
// - 安全加密套件,支持前向保密
// - 配置 TLS 时自动启用 HTTP/2
//
// 使用示例:
//
// cfg := &config.SSLConfig{
// Cert: "/path/to/cert.pem",
// Key: "/path/to/key.pem",
// Protocols: []string{"TLSv1.2", "TLSv1.3"},
// }
//
// manager, err := ssl.NewTLSManager(cfg)
// if err != nil {
// log.Fatal(err)
// }
//
// // 配合 fasthttp 使用
// server := &fasthttp.Server{
// TLSConfig: manager.GetTLSConfig(),
// }
//
// 作者xfy
package ssl
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"os"
"sync"
"rua.plus/lolly/internal/config"
"rua.plus/lolly/internal/logging"
"rua.plus/lolly/internal/sslutil"
)
// TLSManager TLS 配置管理器。
//
// 管理单个或多个证书的 TLS 配置,支持 SNI服务器名称指示
// 用于多证书虚拟主机,以及 OCSP Stapling 用于证书状态验证。
type TLSManager struct {
// configs TLS 配置映射,按服务器名称索引
configs map[string]*tls.Config
// defaultCfg 默认配置,用于 fallback
defaultCfg *tls.Config
// ocspManager OCSP Stapling 管理器
ocspManager *OCSPManager
// sessionTicketMgr Session Ticket 管理器
sessionTicketMgr *SessionTicketManager
// clientVerifier 客户端证书验证器
clientVerifier *ClientVerifier
// certificates 解析后的证书映射,用于 OCSP
certificates map[string]*x509.Certificate
// issuers 颁发者证书映射,用于 OCSP
issuers map[string]*x509.Certificate
// mu 保护并发访问的读写锁
mu sync.RWMutex
}
// NewTLSManager 创建新的 TLS 配置管理器。
//
// 对于单服务器模式,传入单个 SSLConfig。
//
// 参数:
// - cfg: SSL 配置,包含证书路径和 TLS 设置
//
// 返回值:
// - *TLSManager: 配置好的 TLS 管理器
// - error: 证书加载失败或配置无效时返回错误
func NewTLSManager(cfg *config.SSLConfig) (*TLSManager, error) {
if cfg == nil {
return nil, errors.New("ssl config is nil")
}
if cfg.Cert == "" || cfg.Key == "" {
return nil, errors.New("certificate and key paths are required")
}
// 加载证书
cert, err := loadCertificate(cfg.Cert, cfg.Key, cfg.CertChain)
if err != nil {
return nil, fmt.Errorf("failed to load certificate: %w", err)
}
// 创建 TLS 配置,使用安全默认值
tlsCfg := &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12, // 强制 TLS 1.2 最低版本
MaxVersion: tls.VersionTLS13,
NextProtos: []string{"h2", "http/1.1"}, // 启用 HTTP/2 ALPN 支持
}
// 应用 TLS 1.2 的加密套件
if len(cfg.Ciphers) > 0 {
ciphers, err := sslutil.ParseCipherSuites(cfg.Ciphers)
if err != nil {
return nil, fmt.Errorf("invalid cipher suites: %w", err)
}
tlsCfg.CipherSuites = ciphers
} else {
// 使用安全的默认加密套件
tlsCfg.CipherSuites = sslutil.DefaultCipherSuites()
}
// 解析 TLS 协议版本
if len(cfg.Protocols) > 0 {
minVer, maxVer, err := sslutil.ParseTLSVersions(cfg.Protocols)
if err != nil {
return nil, fmt.Errorf("invalid TLS protocols: %w", err)
}
tlsCfg.MinVersion = minVer
tlsCfg.MaxVersion = maxVer
}
manager := &TLSManager{
configs: make(map[string]*tls.Config),
certificates: make(map[string]*x509.Certificate),
issuers: make(map[string]*x509.Certificate),
}
// 初始化 Session Tickets如果启用
if cfg.SessionTickets.Enabled {
sessionTicketMgr, err := NewSessionTicketManager(cfg.SessionTickets)
if err != nil {
logging.Warn().Err(err).Msg("Session Ticket 初始化失败TLS 性能可能降级")
} else {
manager.sessionTicketMgr = sessionTicketMgr
// 应用 Session Tickets 到 TLS 配置
sessionTicketMgr.ApplyToTLSConfig(tlsCfg)
sessionTicketMgr.Start()
}
}
// 初始化 OCSP Stapling如果启用
if cfg.OCSPStapling {
ocspMgr := NewOCSPManager(DefaultOCSPConfig())
manager.ocspManager = ocspMgr
// 解析证书用于 OCSP
if len(cert.Certificate) > 0 {
parsedCert, err := x509.ParseCertificate(cert.Certificate[0])
if err == nil && len(parsedCert.OCSPServer) > 0 {
// 存储证书用于 OCSP 查询
serial := parsedCert.SerialNumber.String()
manager.certificates[serial] = parsedCert
// 尝试从证书链解析颁发者证书
if len(cert.Certificate) > 1 {
issuerCert, err := x509.ParseCertificate(cert.Certificate[1])
if err == nil {
manager.issuers[serial] = issuerCert
if err := ocspMgr.RegisterCertificate(parsedCert, issuerCert); err != nil {
logging.Warn().Err(err).Msg("OCSP Stapling 注册失败")
}
}
}
// 设置 GetConfigForClient 回调用于 OCSP Stapling
tlsCfg.GetConfigForClient = manager.getConfigForClientWithOCSP
}
}
ocspMgr.Start()
}
// 初始化客户端证书验证(如果启用)
if cfg.ClientVerify.Enabled {
clientVerifier, err := NewClientVerifier(cfg.ClientVerify)
if err != nil {
logging.Warn().Err(err).Msg("客户端证书验证配置失败")
} else {
manager.clientVerifier = clientVerifier
clientVerifier.ConfigureTLS(tlsCfg)
}
}
// 设置为默认配置
manager.defaultCfg = tlsCfg
return manager, nil
}
// GetTLSConfig 返回默认的 TLS 配置。
//
// 用于单服务器模式。
//
// 返回值:
// - *tls.Config: TLS 配置对象
func (m *TLSManager) GetTLSConfig() *tls.Config {
m.mu.RLock()
defer m.mu.RUnlock()
return m.defaultCfg
}
// Close 停止 OCSP 管理器和 Session Ticket 管理器并释放资源。
func (m *TLSManager) Close() {
if m.ocspManager != nil {
m.ocspManager.Stop()
}
if m.sessionTicketMgr != nil {
m.sessionTicketMgr.Stop()
}
}
// getConfigForClientWithOCSP 返回启用 OCSP Stapling 的 TLS 配置。
//
// 该回调在每次 TLS 握手时调用,附加最新的 OCSP 响应。
//
// 参数:
// - hello: 客户端 Hello 信息
//
// 返回值:
// - *tls.Config: 带有 OCSP 响应的 TLS 配置
// - error: 配置错误
func (m *TLSManager) getConfigForClientWithOCSP(hello *tls.ClientHelloInfo) (*tls.Config, error) {
m.mu.RLock()
defer m.mu.RUnlock()
// 获取基础配置
var baseCfg *tls.Config
if hello.ServerName != "" {
if cfg, ok := m.configs[hello.ServerName]; ok {
baseCfg = cfg
}
}
if baseCfg == nil {
baseCfg = m.defaultCfg
}
// 无 OCSP 管理器或无证书时,返回基础配置
if m.ocspManager == nil || len(baseCfg.Certificates) == 0 {
return baseCfg, nil
}
// 创建配置副本并附加 OCSP 响应
cfgCopy := baseCfg.Clone()
// 将 OCSP 响应附加到证书
cert := &cfgCopy.Certificates[0]
if len(cert.Certificate) > 0 {
// 解析叶子证书以获取序列号
leafCert, err := x509.ParseCertificate(cert.Certificate[0])
if err == nil {
serial := leafCert.SerialNumber.String()
ocspResp := m.ocspManager.GetOCSPResponse(serial)
if ocspResp != nil {
// 将 OCSP 响应附加到证书
cert.OCSPStaple = ocspResp
}
}
}
return cfgCopy, nil
}
// OCSPStatusInfo OCSP 响应状态信息。
type OCSPStatusInfo struct {
Serial string // 证书序列号
Subject string // 证书主题 CN
Status OCSPStatus // OCSP 响应状态
HasResponse bool // 是否有可用响应
}
// loadCertificate 从给定路径加载 TLS 证书。
//
// 如果提供了证书链路径,则合并证书链。
//
// 参数:
// - certPath: 证书文件路径
// - keyPath: 私钥文件路径
// - certChainPath: 证书链文件路径(可选)
//
// 返回值:
// - tls.Certificate: 加载的证书
// - error: 加载失败时返回错误
func loadCertificate(certPath, keyPath, certChainPath string) (tls.Certificate, error) {
// 加载主证书
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return tls.Certificate{}, err
}
// 合并证书链(如果提供)
if certChainPath != "" {
chainData, err := os.ReadFile(certChainPath)
if err != nil {
return tls.Certificate{}, fmt.Errorf("failed to read certificate chain: %w", err)
}
// 将证书链追加到证书(每个证书作为独立的 [][]byte 条目)
certs := parsePEMChain(chainData)
cert.Certificate = append(cert.Certificate, certs...)
}
return cert, nil
}
// parsePEMChain 解析 PEM 编码的证书链数据。
//
// 返回 ASN.1 DER 编码的证书切片。
//
// 参数:
// - data: PEM 编码的数据
//
// 返回值:
// - [][]byte: DER 编码的证书列表
func parsePEMChain(data []byte) [][]byte {
var certs [][]byte
var block []byte
rest := data
for {
block, rest = extractPEMBlock(rest)
if block == nil {
break
}
if len(block) > 0 {
certs = append(certs, block)
}
}
return certs
}
// extractPEMBlock 从数据中提取单个 PEM 块。
//
// 返回 DER 编码的块和剩余数据。
//
// 参数:
// - data: PEM 数据
//
// 返回值:
// - []byte: DER 编码的块
// - []byte: 剩余数据
func extractPEMBlock(data []byte) ([]byte, []byte) {
startMarker := []byte("-----BEGIN CERTIFICATE-----")
endMarker := []byte("-----END CERTIFICATE-----")
start := findMarker(data, startMarker)
if start == -1 {
return nil, nil
}
end := findMarker(data[start:], endMarker)
if end == -1 {
return nil, nil
}
// 提取并解码 PEM 块
blockData := data[start : start+end+len(endMarker)]
rest := data[start+end+len(endMarker):]
// 注意:此处为简化实现,直接返回原始 PEM 块数据
// 生产环境建议使用 encoding/pem 进行完整解码
return blockData, rest
}
// findMarker 在数据中查找标记位置。
//
// 参数:
// - data: 待搜索的数据
// - marker: 要查找的标记
//
// 返回值:
// - int: 标记位置,未找到返回 -1
func findMarker(data []byte, marker []byte) int {
for i := 0; i <= len(data)-len(marker); i++ {
if matchMarker(data[i:], marker) {
return i
}
}
return -1
}
// matchMarker 检查数据是否以指定标记开头。
//
// 参数:
// - data: 待检查的数据
// - marker: 要匹配的标记
//
// 返回值:
// - bool: 匹配返回 true
func matchMarker(data []byte, marker []byte) bool {
if len(data) < len(marker) {
return false
}
for i := range marker {
if data[i] != marker[i] {
return false
}
}
return true
}