259 lines
6.1 KiB
Go
259 lines
6.1 KiB
Go
// Package ssl 提供 mTLS 客户端证书验证支持。
|
||
//
|
||
// 该文件包含客户端证书验证的核心逻辑,包括:
|
||
// - CA 证书池加载和管理
|
||
// - 证书吊销列表 (CRL) 支持
|
||
// - 验证模式配置
|
||
// - 客户端证书信息提取
|
||
//
|
||
// mTLS (Mutual TLS) 提供双向认证,服务器验证客户端证书,
|
||
// 客户端验证服务器证书,适用于高安全场景。
|
||
//
|
||
// 作者:xfy
|
||
package ssl
|
||
|
||
import (
|
||
"crypto/tls"
|
||
"crypto/x509"
|
||
"encoding/pem"
|
||
"errors"
|
||
"fmt"
|
||
"os"
|
||
"time"
|
||
|
||
"rua.plus/lolly/internal/config"
|
||
"rua.plus/lolly/internal/sslutil"
|
||
)
|
||
|
||
// ClientVerifyMode 客户端证书验证模式
|
||
type ClientVerifyMode int
|
||
|
||
const (
|
||
// VerifyOff 不验证客户端证书。
|
||
VerifyOff ClientVerifyMode = iota
|
||
// VerifyOn 强制验证客户端证书。
|
||
VerifyOn
|
||
// VerifyOptional 可选验证(客户端可选择不提供证书)。
|
||
VerifyOptional
|
||
// VerifyOptionalNoCA 可选验证但不验证 CA。
|
||
VerifyOptionalNoCA
|
||
|
||
// verifyModeOff 验证模式字符串常量
|
||
verifyModeOff = "off"
|
||
verifyModeOn = "on"
|
||
verifyModeOptional = "optional"
|
||
verifyModeOptionalNoCA = "optional_no_ca"
|
||
)
|
||
|
||
// ParseVerifyMode 解析验证模式字符串。
|
||
//
|
||
// 参数:
|
||
// - mode: 模式字符串(on/off/optional/optional_no_ca)
|
||
//
|
||
// 返回值:
|
||
// - ClientVerifyMode: 验证模式
|
||
// - error: 无效模式时返回错误
|
||
func ParseVerifyMode(mode string) (ClientVerifyMode, error) {
|
||
switch mode {
|
||
case verifyModeOff, "":
|
||
return VerifyOff, nil
|
||
case verifyModeOn:
|
||
return VerifyOn, nil
|
||
case verifyModeOptional:
|
||
return VerifyOptional, nil
|
||
case verifyModeOptionalNoCA:
|
||
return VerifyOptionalNoCA, nil
|
||
default:
|
||
return VerifyOff, fmt.Errorf("invalid verify mode: %s", mode)
|
||
}
|
||
}
|
||
|
||
// TLSClientAuth 返回对应的 tls.ClientAuthType。
|
||
//
|
||
// 返回值:
|
||
// - tls.ClientAuthType: TLS 客户端认证类型
|
||
func (m ClientVerifyMode) TLSClientAuth() tls.ClientAuthType {
|
||
switch m {
|
||
case VerifyOff:
|
||
return tls.NoClientCert
|
||
case VerifyOn:
|
||
return tls.RequireAndVerifyClientCert
|
||
case VerifyOptional:
|
||
return tls.VerifyClientCertIfGiven
|
||
case VerifyOptionalNoCA:
|
||
return tls.RequestClientCert
|
||
default:
|
||
return tls.NoClientCert
|
||
}
|
||
}
|
||
|
||
// ClientVerifier 客户端证书验证器。
|
||
//
|
||
// 管理客户端证书验证所需的 CA 证书池和 CRL。
|
||
type ClientVerifier struct {
|
||
caPool *x509.CertPool
|
||
crl *x509.RevocationList
|
||
caFile string
|
||
crlFile string
|
||
mode ClientVerifyMode
|
||
verifyDepth int
|
||
}
|
||
|
||
// NewClientVerifier 创建新的客户端证书验证器。
|
||
//
|
||
// 参数:
|
||
// - cfg: 客户端验证配置
|
||
//
|
||
// 返回值:
|
||
// - *ClientVerifier: 验证器实例
|
||
// - error: 配置无效时返回错误
|
||
func NewClientVerifier(cfg config.ClientVerifyConfig) (*ClientVerifier, error) {
|
||
if !cfg.Enabled {
|
||
return &ClientVerifier{
|
||
mode: VerifyOff,
|
||
}, nil
|
||
}
|
||
|
||
mode, err := ParseVerifyMode(cfg.Mode)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
verifier := &ClientVerifier{
|
||
mode: mode,
|
||
verifyDepth: cfg.VerifyDepth,
|
||
caFile: cfg.ClientCA,
|
||
crlFile: cfg.CRL,
|
||
}
|
||
|
||
// 加载 CA 证书池(如果需要验证)
|
||
if mode == VerifyOn || mode == VerifyOptional {
|
||
if cfg.ClientCA == "" {
|
||
return nil, errors.New("client_ca is required when verify is enabled")
|
||
}
|
||
|
||
caPool, err := sslutil.LoadCACertPool(cfg.ClientCA)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to load CA certificate pool: %w", err)
|
||
}
|
||
verifier.caPool = caPool
|
||
}
|
||
|
||
// 加载 CRL(如果配置)
|
||
if cfg.CRL != "" {
|
||
crl, err := LoadCRL(cfg.CRL)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to load CRL: %w", err)
|
||
}
|
||
verifier.crl = crl
|
||
}
|
||
|
||
return verifier, nil
|
||
}
|
||
|
||
// ConfigureTLS 配置 TLS 以启用客户端证书验证。
|
||
//
|
||
// 参数:
|
||
// - tlsCfg: TLS 配置对象
|
||
func (v *ClientVerifier) ConfigureTLS(tlsCfg *tls.Config) {
|
||
if tlsCfg == nil || v.mode == VerifyOff {
|
||
return
|
||
}
|
||
|
||
tlsCfg.ClientAuth = v.mode.TLSClientAuth()
|
||
tlsCfg.ClientCAs = v.caPool
|
||
|
||
// 设置验证深度(通过 VerifyConnection 回调实现)
|
||
if v.verifyDepth > 0 {
|
||
tlsCfg.VerifyConnection = v.verifyConnection
|
||
}
|
||
}
|
||
|
||
// verifyConnection 验证 TLS 连接。
|
||
//
|
||
// 实现额外的验证逻辑,如证书深度检查。
|
||
//
|
||
// 参数:
|
||
// - cs: 连接状态
|
||
//
|
||
// 返回值:
|
||
// - error: 验证失败时返回错误
|
||
func (v *ClientVerifier) verifyConnection(cs tls.ConnectionState) error {
|
||
// 检查 CRL
|
||
if v.crl != nil && len(cs.PeerCertificates) > 0 {
|
||
if err := v.checkCRL(cs.PeerCertificates[0]); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
// 检查证书链深度
|
||
if v.verifyDepth > 0 && len(cs.PeerCertificates) > v.verifyDepth {
|
||
return fmt.Errorf("certificate chain too long: %d > %d", len(cs.PeerCertificates), v.verifyDepth)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// checkCRL 检查证书是否在吊销列表中。
|
||
//
|
||
// 参数:
|
||
// - cert: 要检查的证书
|
||
//
|
||
// 返回值:
|
||
// - error: 证书已吊销时返回错误
|
||
func (v *ClientVerifier) checkCRL(cert *x509.Certificate) error {
|
||
if v.crl == nil || len(v.crl.RevokedCertificateEntries) == 0 {
|
||
return nil
|
||
}
|
||
|
||
for _, revoked := range v.crl.RevokedCertificateEntries {
|
||
if cert.SerialNumber.Cmp(revoked.SerialNumber) == 0 {
|
||
return fmt.Errorf("certificate %s has been revoked", cert.SerialNumber.String())
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// LoadCRL 从文件加载证书吊销列表。
|
||
//
|
||
// 支持 PEM 和 DER 格式的 CRL 文件。
|
||
//
|
||
// 参数:
|
||
// - crlFile: CRL 文件路径
|
||
//
|
||
// 返回值:
|
||
// - *pkix.CertificateList: CRL 对象
|
||
// - error: 加载失败时返回错误
|
||
func LoadCRL(crlFile string) (*x509.RevocationList, error) {
|
||
data, err := os.ReadFile(crlFile)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to read CRL file: %w", err)
|
||
}
|
||
|
||
// 尝试 PEM 解码
|
||
block, _ := pem.Decode(data)
|
||
if block != nil {
|
||
data = block.Bytes
|
||
}
|
||
|
||
crl, err := x509.ParseRevocationList(data)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to parse CRL: %w", err)
|
||
}
|
||
|
||
return crl, nil
|
||
}
|
||
|
||
// ClientCertInfo 客户端证书信息。
|
||
type ClientCertInfo struct {
|
||
NotBefore time.Time
|
||
NotAfter time.Time
|
||
Subject string
|
||
Issuer string
|
||
Serial string
|
||
Fingerprint string
|
||
DNSNames []string
|
||
Email []string
|
||
}
|