/* * 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 }