384 lines
11 KiB
Go
384 lines
11 KiB
Go
|
|
/*
|
||
|
|
* 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
|
||
|
|
}
|