Files

384 lines
11 KiB
Go
Raw Permalink Normal View History

2026-06-12 16:20:55 +00:00
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/cluster/tls_config.go
// Назначение: TLS для межузлового общения, валидация сертификатов, ротация ключей
package cluster
import (
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"os"
"sync"
"time"
)
// TLSConfig управляет TLS для межузлового общения
type TLSConfig struct {
Enabled bool `json:"enabled"`
CertFile string `json:"cert_file"`
KeyFile string `json:"key_file"`
CAFile string `json:"ca_file"`
MinVersion string `json:"min_version"`
MutualAuth bool `json:"mutual_auth"`
KeyRotationDays int `json:"key_rotation_days"`
AutoGenerate bool `json:"auto_generate"`
CertCache *tls.Certificate
caCertPool *x509.CertPool
mu sync.RWMutex
lastRotation time.Time
logger LoggerInterface
}
// TLSOptions содержит опции TLS
type TLSOptions struct {
CertFile string
KeyFile string
CAFile string
MinVersion uint16
MutualAuth bool
ServerName string
}
// NewTLSConfig создаёт новую TLS конфигурацию
func NewTLSConfig(enabled bool, certFile, keyFile, caFile, minVersion string, mutualAuth bool, keyRotationDays int, autoGenerate bool, logger LoggerInterface) *TLSConfig {
cfg := &TLSConfig{
Enabled: enabled,
CertFile: certFile,
KeyFile: keyFile,
CAFile: caFile,
MinVersion: minVersion,
MutualAuth: mutualAuth,
KeyRotationDays: keyRotationDays,
AutoGenerate: autoGenerate,
logger: logger,
lastRotation: time.Now(),
}
if enabled {
cfg.loadCertificates()
}
return cfg
}
// loadCertificates загружает сертификаты
func (tc *TLSConfig) loadCertificates() error {
tc.mu.Lock()
defer tc.mu.Unlock()
// Если включена автогенерация и файлы не существуют
if tc.AutoGenerate {
if _, err := os.Stat(tc.CertFile); os.IsNotExist(err) {
if err := tc.generateSelfSignedCert(); err != nil {
return fmt.Errorf("failed to generate self-signed cert: %v", err)
}
}
}
// Загружаем сертификат
cert, err := tls.LoadX509KeyPair(tc.CertFile, tc.KeyFile)
if err != nil {
return fmt.Errorf("failed to load certificate: %v", err)
}
tc.CertCache = &cert
// Загружаем CA если указан
if tc.CAFile != "" {
caCert, err := os.ReadFile(tc.CAFile)
if err != nil {
return fmt.Errorf("failed to read CA file: %v", err)
}
tc.caCertPool = x509.NewCertPool()
if !tc.caCertPool.AppendCertsFromPEM(caCert) {
return fmt.Errorf("failed to parse CA certificate")
}
}
if tc.logger != nil {
tc.logger.Debug(fmt.Sprintf("TLS certificates loaded from %s", tc.CertFile))
}
return nil
}
// generateSelfSignedCert генерирует самоподписанный сертификат
func (tc *TLSConfig) generateSelfSignedCert() error {
// Генерируем приватный ключ
privKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return err
}
// Создаём шаблон сертификата
template := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().Unix()),
Subject: pkix.Name{
Organization: []string{"Futriix Cluster"},
CommonName: "futriix-node",
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
}
// Создаём сертификат
certBytes, err := x509.CreateCertificate(rand.Reader, template, template, &privKey.PublicKey, privKey)
if err != nil {
return err
}
// Сохраняем сертификат
certOut, err := os.Create(tc.CertFile)
if err != nil {
return err
}
defer certOut.Close()
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: certBytes})
// Сохраняем ключ
keyOut, err := os.Create(tc.KeyFile)
if err != nil {
return err
}
defer keyOut.Close()
pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privKey)})
if tc.logger != nil {
tc.logger.Info(fmt.Sprintf("Generated self-signed certificate at %s", tc.CertFile))
}
return nil
}
// GetTLSConfig возвращает tls.Config для использования
func (tc *TLSConfig) GetTLSConfig() (*tls.Config, error) {
if !tc.Enabled {
return nil, nil
}
// Проверяем необходимость ротации ключей
if time.Since(tc.lastRotation).Hours() > float64(tc.KeyRotationDays*24) {
if err := tc.rotateKeys(); err != nil {
if tc.logger != nil {
tc.logger.Warn(fmt.Sprintf("Key rotation failed: %v", err))
}
}
}
tc.mu.RLock()
defer tc.mu.RUnlock()
if tc.CertCache == nil {
if err := tc.loadCertificates(); err != nil {
return nil, err
}
}
minVersion := tc.getMinVersion()
tlsCfg := &tls.Config{
Certificates: []tls.Certificate{*tc.CertCache},
MinVersion: minVersion,
CurvePreferences: []tls.CurveID{
tls.X25519,
tls.CurveP256,
tls.CurveP384,
},
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
},
PreferServerCipherSuites: true,
SessionTicketsDisabled: false,
ClientSessionCache: tls.NewLRUClientSessionCache(100),
}
if tc.MutualAuth && tc.caCertPool != nil {
tlsCfg.ClientAuth = tls.RequireAndVerifyClientCert
tlsCfg.ClientCAs = tc.caCertPool
} else if tc.MutualAuth {
tlsCfg.ClientAuth = tls.RequireAnyClientCert
}
return tlsCfg, nil
}
// getMinVersion преобразует строковую версию в uint16
func (tc *TLSConfig) getMinVersion() uint16 {
switch tc.MinVersion {
case "1.0":
return tls.VersionTLS10
case "1.1":
return tls.VersionTLS11
case "1.2":
return tls.VersionTLS12
case "1.3":
return tls.VersionTLS13
default:
return tls.VersionTLS12
}
}
// rotateKeys выполняет ротацию ключей шифрования
func (tc *TLSConfig) rotateKeys() error {
if !tc.AutoGenerate {
return fmt.Errorf("auto-generation disabled, cannot rotate keys")
}
tc.mu.Lock()
defer tc.mu.Unlock()
// Создаём бэкап старых ключей
backupCert := tc.CertFile + ".backup"
backupKey := tc.KeyFile + ".backup"
if _, err := os.Stat(tc.CertFile); err == nil {
os.Rename(tc.CertFile, backupCert)
os.Rename(tc.KeyFile, backupKey)
}
// Генерируем новые ключи
if err := tc.generateSelfSignedCert(); err != nil {
// Восстанавливаем из бэкапа при ошибке
os.Rename(backupCert, tc.CertFile)
os.Rename(backupKey, tc.KeyFile)
return fmt.Errorf("key rotation failed: %v", err)
}
// Загружаем новый сертификат
cert, err := tls.LoadX509KeyPair(tc.CertFile, tc.KeyFile)
if err != nil {
return err
}
tc.CertCache = &cert
tc.lastRotation = time.Now()
if tc.logger != nil {
tc.logger.Info("TLS keys rotated successfully")
}
return nil
}
// ValidatePeerCertificate валидирует сертификат пира
func (tc *TLSConfig) ValidatePeerCertificate(certificates [][]byte) error {
if !tc.MutualAuth {
return nil
}
if len(certificates) == 0 {
return fmt.Errorf("no client certificate provided")
}
cert, err := x509.ParseCertificate(certificates[0])
if err != nil {
return fmt.Errorf("failed to parse peer certificate: %v", err)
}
if tc.caCertPool != nil {
opts := x509.VerifyOptions{
Roots: tc.caCertPool,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
if _, err := cert.Verify(opts); err != nil {
return fmt.Errorf("peer certificate verification failed: %v", err)
}
}
// Проверяем срок действия
if time.Now().After(cert.NotAfter) {
return fmt.Errorf("peer certificate expired at %v", cert.NotAfter)
}
if time.Now().Before(cert.NotBefore) {
return fmt.Errorf("peer certificate not valid until %v", cert.NotBefore)
}
return nil
}
// IsExpired проверяет, истёк ли сертификат
func (tc *TLSConfig) IsExpired() bool {
tc.mu.RLock()
defer tc.mu.RUnlock()
if tc.CertCache == nil {
return true
}
cert, err := x509.ParseCertificate(tc.CertCache.Certificate[0])
if err != nil {
return true
}
return time.Now().After(cert.NotAfter)
}
// GetExpiryDays возвращает количество дней до истечения
func (tc *TLSConfig) GetExpiryDays() int {
tc.mu.RLock()
defer tc.mu.RUnlock()
if tc.CertCache == nil {
return 0
}
cert, err := x509.ParseCertificate(tc.CertCache.Certificate[0])
if err != nil {
return 0
}
days := int(time.Until(cert.NotAfter).Hours() / 24)
if days < 0 {
return 0
}
return days
}
// GetCertFile возвращает путь к файлу сертификата
func (tc *TLSConfig) GetCertFile() string {
return tc.CertFile
}
// GetKeyFile возвращает путь к файлу ключа
func (tc *TLSConfig) GetKeyFile() string {
return tc.KeyFile
}
// GetCAFile возвращает путь к файлу CA
func (tc *TLSConfig) GetCAFile() string {
return tc.CAFile
}
// IsMutualAuthEnabled возвращает статус взаимной аутентификации
func (tc *TLSConfig) IsMutualAuthEnabled() bool {
return tc.MutualAuth
}
// IsEnabled возвращает статус TLS
func (tc *TLSConfig) IsEnabled() bool {
return tc.Enabled
}