lolly/internal/ssl/session_tickets.go

382 lines
9.7 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 Session Tickets 支持。
//
// 该文件包含 TLS Session Tickets 密钥管理和轮换逻辑,包括:
// - Session Ticket 密钥生成和加载
// - 自动密钥轮换机制
// - 多密钥保留策略(支持旧票据解密)
// - 与 TLS 配置的集成
//
// Session Tickets 允许 TLS 1.3 会话恢复,避免完整握手,显著提升性能。
// 密钥定期轮换增强安全性,同时保留旧密钥确保已发放的票据仍可解密。
//
// 作者xfy
package ssl
import (
"crypto/rand"
"crypto/tls"
"errors"
"fmt"
"os"
"sync"
"time"
"rua.plus/lolly/internal/config"
"rua.plus/lolly/internal/logging"
)
const (
// ticketKeySize Session Ticket 密钥大小(字节)
// TLS 1.3 使用 32 字节的 AES-256-GCM 密钥
ticketKeySize = 32
// defaultRotateInterval 默认密钥轮换间隔
defaultRotateInterval = time.Hour
// defaultRetainKeys 默认保留的密钥数量
// 至少保留 2 个密钥(当前 + 上一个)
defaultRetainKeys = 3
// minRetainKeys 最小保留密钥数量
minRetainKeys = 2
)
// SessionTicketManager Session Ticket 密钥管理器。
//
// 管理 Session Ticket 密钥的生命周期,包括生成、轮换、存储和加载。
// 密钥按时间顺序排列,最新的密钥用于加密,所有密钥都可用于解密。
type SessionTicketManager struct {
rotateTimer *time.Timer
stopCh chan struct{}
keys [][]byte
config config.SessionTicketsConfig
mu sync.RWMutex
started bool
}
// NewSessionTicketManager 创建新的 Session Ticket 管理器。
//
// 根据配置创建管理器,如 key_file 存在则加载现有密钥,
// 否则自动生成新密钥。
//
// 参数:
// - cfg: Session Tickets 配置
//
// 返回值:
// - *SessionTicketManager: 配置好的管理器
// - error: 密钥加载或生成失败时返回错误
func NewSessionTicketManager(cfg config.SessionTicketsConfig) (*SessionTicketManager, error) {
if !cfg.Enabled {
return nil, errors.New("session tickets are disabled")
}
// 使用默认值
rotateInterval := cfg.RotateInterval
if rotateInterval <= 0 {
rotateInterval = defaultRotateInterval
}
retainKeys := cfg.RetainKeys
if retainKeys < minRetainKeys {
retainKeys = defaultRetainKeys
}
manager := &SessionTicketManager{
config: config.SessionTicketsConfig{
Enabled: cfg.Enabled,
KeyFile: cfg.KeyFile,
RotateInterval: rotateInterval,
RetainKeys: retainKeys,
},
keys: make([][]byte, 0, retainKeys),
stopCh: make(chan struct{}),
}
// 尝试加载或生成初始密钥
if cfg.KeyFile != "" {
if err := manager.loadOrGenerateKey(); err != nil {
return nil, fmt.Errorf("failed to initialize session ticket key: %w", err)
}
} else {
// 没有指定密钥文件,生成内存中的密钥
// 警告:服务重启后旧票据将失效,影响前向保密性
logging.Warn().Msg("SessionTickets enabled without KeyFile: session tickets will be invalid after restart, consider configuring KeyFile for persistence")
key, err := generateTicketKey()
if err != nil {
return nil, fmt.Errorf("failed to generate session ticket key: %w", err)
}
manager.keys = append(manager.keys, key)
}
return manager, nil
}
// Start 启动密钥轮换定时器。
//
// 按照配置的 rotate_interval 定期生成新密钥。
// 必须在调用 GetKeys 之前启动。
func (m *SessionTicketManager) Start() {
m.mu.Lock()
if m.started {
m.mu.Unlock()
return
}
m.started = true
m.mu.Unlock()
// 启动轮换定时器
m.scheduleRotation()
}
// Stop 停止密钥轮换定时器。
//
// 停止后不再进行自动密钥轮换,但现有密钥仍然有效。
func (m *SessionTicketManager) Stop() {
m.mu.Lock()
if !m.started {
m.mu.Unlock()
return
}
m.started = false
m.mu.Unlock()
close(m.stopCh)
if m.rotateTimer != nil {
m.rotateTimer.Stop()
}
}
// GetKeys 返回当前所有有效的 Session Ticket 密钥。
//
// 返回的密钥按时间顺序排列,最新的在最后。
// TLS 配置应该使用最新的密钥加密,所有密钥都可以解密。
//
// 返回值:
// - [][]byte: 密钥列表,每个密钥 32 字节
func (m *SessionTicketManager) GetKeys() [][]byte {
m.mu.RLock()
defer m.mu.RUnlock()
// 返回副本以防止外部修改
result := make([][]byte, len(m.keys))
for i, key := range m.keys {
result[i] = make([]byte, len(key))
copy(result[i], key)
}
return result
}
// RotateKey 手动轮换 Session Ticket 密钥。
//
// 生成新密钥并添加到密钥列表,如果超过 retain_keys 数量则移除最旧的密钥。
// 新密钥用于加密新票据,旧密钥仍可用于解密已发放的票据。
//
// 返回值:
// - error: 密钥生成失败时返回错误
func (m *SessionTicketManager) RotateKey() error {
m.mu.Lock()
defer m.mu.Unlock()
// 生成新密钥
newKey, err := generateTicketKey()
if err != nil {
return fmt.Errorf("failed to generate new ticket key: %w", err)
}
// 添加新密钥
m.keys = append(m.keys, newKey)
// 如果超过保留数量,移除最旧的密钥
if len(m.keys) > m.config.RetainKeys {
m.keys = m.keys[len(m.keys)-m.config.RetainKeys:]
}
// 如果有密钥文件,保存所有密钥
if m.config.KeyFile != "" {
if err := m.saveKeys(); err != nil {
logging.Warn().Err(err).Msg("Session Ticket 密钥保存失败")
}
}
return nil
}
// ApplyToTLSConfig 将 Session Ticket 密钥应用到 TLS 配置。
//
// 设置 tls.Config 的 SetSessionTicketKeys 回调,用于动态提供密钥。
//
// 参数:
// - tlsCfg: TLS 配置对象
func (m *SessionTicketManager) ApplyToTLSConfig(tlsCfg *tls.Config) {
if tlsCfg == nil {
return
}
// 设置会话票据密钥
// Go 的 crypto/tls 使用 SetSessionTicketKeys 方法设置密钥
// 需要转换为 [][32]byte 类型
keys := m.GetKeys()
ticketKeys := make([][32]byte, len(keys))
for i, key := range keys {
if len(key) >= 32 {
copy(ticketKeys[i][:], key)
}
}
tlsCfg.SetSessionTicketKeys(ticketKeys)
}
// scheduleRotation 调度密钥轮换。
//
// 使用定时器在指定间隔后执行密钥轮换。
func (m *SessionTicketManager) scheduleRotation() {
if !m.started {
return
}
m.rotateTimer = time.AfterFunc(m.config.RotateInterval, func() {
select {
case <-m.stopCh:
return
default:
_ = m.RotateKey()
m.scheduleRotation()
}
})
}
// loadOrGenerateKey 从文件加载密钥或生成新密钥。
//
// 如果密钥文件存在,加载所有密钥;否则生成新密钥并保存。
//
// 返回值:
// - error: 加载或生成失败时返回错误
func (m *SessionTicketManager) loadOrGenerateKey() error {
// 尝试加载现有密钥
if _, err := os.Stat(m.config.KeyFile); err == nil {
// 文件存在,加载密钥
if err := m.loadKeys(); err != nil {
// 加载失败,生成新密钥
return m.generateAndSaveKey()
}
return nil
}
// 文件不存在,生成新密钥
return m.generateAndSaveKey()
}
// loadKeys 从文件加载所有密钥。
//
// 密钥文件格式:每个密钥 32 字节,连续存储
//
// 返回值:
// - error: 读取或解析失败时返回错误
func (m *SessionTicketManager) loadKeys() error {
data, err := os.ReadFile(m.config.KeyFile)
if err != nil {
return fmt.Errorf("failed to read key file: %w", err)
}
// 解析密钥(每个 32 字节)
if len(data) < ticketKeySize {
return errors.New("key file too small")
}
m.keys = make([][]byte, 0, m.config.RetainKeys)
for i := 0; i+ticketKeySize <= len(data); i += ticketKeySize {
key := make([]byte, ticketKeySize)
copy(key, data[i:i+ticketKeySize])
m.keys = append(m.keys, key)
}
// 如果加载的密钥超过保留数量,只保留最新的
if len(m.keys) > m.config.RetainKeys {
m.keys = m.keys[len(m.keys)-m.config.RetainKeys:]
}
// 确保至少有一个密钥
if len(m.keys) == 0 {
return errors.New("no valid keys found in file")
}
return nil
}
// saveKeys 将所有密钥保存到文件。
//
// 密钥文件格式:每个密钥 32 字节,连续存储
// 文件权限设置为 0600仅所有者可读写
//
// 返回值:
// - error: 写入失败时返回错误
func (m *SessionTicketManager) saveKeys() error {
// 计算总大小
totalSize := len(m.keys) * ticketKeySize
data := make([]byte, 0, totalSize)
for _, key := range m.keys {
data = append(data, key...)
}
// 使用 0600 权限写入文件(敏感数据,限制访问)
if err := os.WriteFile(m.config.KeyFile, data, 0o600); err != nil {
return fmt.Errorf("failed to write key file: %w", err)
}
return nil
}
// generateAndSaveKey 生成新密钥并保存。
//
// 返回值:
// - error: 生成或保存失败时返回错误
func (m *SessionTicketManager) generateAndSaveKey() error {
key, err := generateTicketKey()
if err != nil {
return err
}
m.keys = [][]byte{key}
if m.config.KeyFile != "" {
if err := m.saveKeys(); err != nil {
return err
}
}
return nil
}
// generateTicketKey 生成新的随机 Session Ticket 密钥。
//
// 使用 crypto/rand 生成加密安全的随机密钥。
//
// 返回值:
// - []byte: 32 字节的随机密钥
// - error: 随机数生成失败时返回错误
func generateTicketKey() ([]byte, error) {
key := make([]byte, ticketKeySize)
if _, err := rand.Read(key); err != nil {
return nil, fmt.Errorf("failed to generate random key: %w", err)
}
return key, nil
}
// SessionTicketStatus Session Ticket 密钥状态信息。
//
// 用于监控和调试,显示当前密钥数量和轮换状态。
type SessionTicketStatus struct {
// KeyCount 当前密钥数量
KeyCount int
// RetainKeys 配置的最大保留密钥数
RetainKeys int
// RotateInterval 配置的轮换间隔
RotateInterval time.Duration
// Started 管理器是否已启动
Started bool
}