Upload files to "internal/cluster"
This commit is contained in:
383
internal/cluster/tls_config.go
Normal file
383
internal/cluster/tls_config.go
Normal file
@@ -0,0 +1,383 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
Reference in New Issue
Block a user