350 lines
9.4 KiB
Go
350 lines
9.4 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/auth.go
|
||
// Назначение: Аутентификация и авторизация между узлами кластера
|
||
|
||
package cluster
|
||
|
||
import (
|
||
"crypto"
|
||
"crypto/rand"
|
||
"crypto/rsa"
|
||
"crypto/sha256"
|
||
"crypto/x509"
|
||
"encoding/base64"
|
||
"encoding/json"
|
||
"encoding/pem"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
// NodeAuthConfig конфигурация аутентификации узлов
|
||
type NodeAuthConfig struct {
|
||
Enabled bool `json:"enabled"`
|
||
TokenTTL time.Duration `json:"token_ttl"`
|
||
PrivateKeyPath string `json:"private_key_path"`
|
||
PublicKeyPath string `json:"public_key_path"`
|
||
AllowedNodes []string `json:"allowed_nodes"`
|
||
RequireMTLS bool `json:"require_mtls"`
|
||
}
|
||
|
||
// NodeAuthToken представляет токен аутентификации узла
|
||
type NodeAuthToken struct {
|
||
NodeID string `json:"node_id"`
|
||
IssuedAt int64 `json:"issued_at"`
|
||
ExpiresAt int64 `json:"expires_at"`
|
||
Signature string `json:"signature"`
|
||
}
|
||
|
||
// NodeAuthenticator управляет аутентификацией между узлами
|
||
type NodeAuthenticator struct {
|
||
config *NodeAuthConfig
|
||
privateKey *rsa.PrivateKey
|
||
publicKey *rsa.PublicKey
|
||
tokens sync.Map
|
||
mu sync.RWMutex
|
||
logger LoggerInterface
|
||
}
|
||
|
||
// NewNodeAuthenticator создаёт новый аутентификатор узлов
|
||
func NewNodeAuthenticator(config *NodeAuthConfig, logger LoggerInterface) (*NodeAuthenticator, error) {
|
||
if config == nil {
|
||
config = &NodeAuthConfig{
|
||
Enabled: true,
|
||
TokenTTL: 24 * time.Hour,
|
||
AllowedNodes: make([]string, 0),
|
||
RequireMTLS: false,
|
||
}
|
||
}
|
||
|
||
na := &NodeAuthenticator{
|
||
config: config,
|
||
logger: logger,
|
||
}
|
||
|
||
if config.Enabled {
|
||
if err := na.loadKeys(); err != nil {
|
||
if err := na.generateKeys(); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
}
|
||
|
||
return na, nil
|
||
}
|
||
|
||
// generateKeys генерирует RSA ключи для аутентификации
|
||
func (na *NodeAuthenticator) generateKeys() error {
|
||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
na.privateKey = privateKey
|
||
na.publicKey = &privateKey.PublicKey
|
||
|
||
// Сохраняем ключи
|
||
if err := na.saveKeys(); err != nil {
|
||
return err
|
||
}
|
||
|
||
if na.logger != nil {
|
||
na.logger.Info("Generated new RSA keys for node authentication")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// loadKeys загружает RSA ключи с диска
|
||
func (na *NodeAuthenticator) loadKeys() error {
|
||
// Загружаем приватный ключ
|
||
privData, err := os.ReadFile(na.config.PrivateKeyPath)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
block, _ := pem.Decode(privData)
|
||
if block == nil {
|
||
return fmt.Errorf("failed to decode private key")
|
||
}
|
||
|
||
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
var ok bool
|
||
na.privateKey, ok = privateKey.(*rsa.PrivateKey)
|
||
if !ok {
|
||
return fmt.Errorf("invalid private key type")
|
||
}
|
||
|
||
// Загружаем публичный ключ
|
||
pubData, err := os.ReadFile(na.config.PublicKeyPath)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
pubBlock, _ := pem.Decode(pubData)
|
||
if pubBlock == nil {
|
||
return fmt.Errorf("failed to decode public key")
|
||
}
|
||
|
||
publicKey, err := x509.ParsePKIXPublicKey(pubBlock.Bytes)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
na.publicKey, ok = publicKey.(*rsa.PublicKey)
|
||
if !ok {
|
||
return fmt.Errorf("invalid public key type")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// saveKeys сохраняет RSA ключи на диск
|
||
func (na *NodeAuthenticator) saveKeys() error {
|
||
if na.privateKey == nil {
|
||
return fmt.Errorf("no private key to save")
|
||
}
|
||
|
||
// Сохраняем приватный ключ
|
||
privBytes, err := x509.MarshalPKCS8PrivateKey(na.privateKey)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
privPEM := pem.EncodeToMemory(&pem.Block{
|
||
Type: "PRIVATE KEY",
|
||
Bytes: privBytes,
|
||
})
|
||
|
||
if err := os.MkdirAll(filepath.Dir(na.config.PrivateKeyPath), 0700); err != nil {
|
||
return err
|
||
}
|
||
|
||
if err := os.WriteFile(na.config.PrivateKeyPath, privPEM, 0600); err != nil {
|
||
return err
|
||
}
|
||
|
||
// Сохраняем публичный ключ
|
||
pubBytes, err := x509.MarshalPKIXPublicKey(na.publicKey)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
pubPEM := pem.EncodeToMemory(&pem.Block{
|
||
Type: "PUBLIC KEY",
|
||
Bytes: pubBytes,
|
||
})
|
||
|
||
if err := os.WriteFile(na.config.PublicKeyPath, pubPEM, 0644); err != nil {
|
||
return err
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// GenerateToken генерирует токен для узла
|
||
func (na *NodeAuthenticator) GenerateToken(nodeID string) (string, error) {
|
||
if !na.config.Enabled {
|
||
return "", nil
|
||
}
|
||
|
||
now := time.Now().UnixMilli()
|
||
token := &NodeAuthToken{
|
||
NodeID: nodeID,
|
||
IssuedAt: now,
|
||
ExpiresAt: now + na.config.TokenTTL.Milliseconds(),
|
||
}
|
||
|
||
// Подписываем токен
|
||
data, err := json.Marshal(token)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
hash := sha256.Sum256(data)
|
||
signature, err := rsa.SignPKCS1v15(rand.Reader, na.privateKey, crypto.SHA256, hash[:])
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
token.Signature = base64.StdEncoding.EncodeToString(signature)
|
||
|
||
// Сохраняем токен
|
||
tokenData, err := json.Marshal(token)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
tokenString := base64.StdEncoding.EncodeToString(tokenData)
|
||
na.tokens.Store(nodeID, token)
|
||
|
||
return tokenString, nil
|
||
}
|
||
|
||
// VerifyToken проверяет токен узла
|
||
func (na *NodeAuthenticator) VerifyToken(tokenString string) (string, error) {
|
||
if !na.config.Enabled {
|
||
return "", nil
|
||
}
|
||
|
||
data, err := base64.StdEncoding.DecodeString(tokenString)
|
||
if err != nil {
|
||
return "", fmt.Errorf("invalid token encoding: %v", err)
|
||
}
|
||
|
||
var token NodeAuthToken
|
||
if err := json.Unmarshal(data, &token); err != nil {
|
||
return "", fmt.Errorf("invalid token format: %v", err)
|
||
}
|
||
|
||
// Проверяем срок действия
|
||
now := time.Now().UnixMilli()
|
||
if token.ExpiresAt < now {
|
||
return "", fmt.Errorf("token expired")
|
||
}
|
||
|
||
// Проверяем подпись
|
||
sigData, err := base64.StdEncoding.DecodeString(token.Signature)
|
||
if err != nil {
|
||
return "", fmt.Errorf("invalid signature: %v", err)
|
||
}
|
||
|
||
tokenCopy := token
|
||
tokenCopy.Signature = ""
|
||
dataWithoutSig, _ := json.Marshal(tokenCopy)
|
||
hash := sha256.Sum256(dataWithoutSig)
|
||
|
||
if err := rsa.VerifyPKCS1v15(na.publicKey, crypto.SHA256, hash[:], sigData); err != nil {
|
||
return "", fmt.Errorf("invalid signature: %v", err)
|
||
}
|
||
|
||
// Проверяем, разрешён ли узел
|
||
if len(na.config.AllowedNodes) > 0 {
|
||
allowed := false
|
||
for _, node := range na.config.AllowedNodes {
|
||
if node == token.NodeID {
|
||
allowed = true
|
||
break
|
||
}
|
||
}
|
||
if !allowed {
|
||
return "", fmt.Errorf("node %s not allowed", token.NodeID)
|
||
}
|
||
}
|
||
|
||
return token.NodeID, nil
|
||
}
|
||
|
||
// AuthenticateRequest аутентифицирует запрос
|
||
func (na *NodeAuthenticator) AuthenticateRequest(data []byte, signature string) bool {
|
||
if !na.config.Enabled {
|
||
return true
|
||
}
|
||
|
||
sigData, err := base64.StdEncoding.DecodeString(signature)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
|
||
hash := sha256.Sum256(data)
|
||
err = rsa.VerifyPKCS1v15(na.publicKey, crypto.SHA256, hash[:], sigData)
|
||
return err == nil
|
||
}
|
||
|
||
// SignRequest подписывает запрос
|
||
func (na *NodeAuthenticator) SignRequest(data []byte) (string, error) {
|
||
if !na.config.Enabled {
|
||
return "", nil
|
||
}
|
||
|
||
hash := sha256.Sum256(data)
|
||
signature, err := rsa.SignPKCS1v15(rand.Reader, na.privateKey, crypto.SHA256, hash[:])
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
return base64.StdEncoding.EncodeToString(signature), nil
|
||
}
|
||
|
||
// AddAllowedNode добавляет разрешённый узел
|
||
func (na *NodeAuthenticator) AddAllowedNode(nodeID string) {
|
||
na.mu.Lock()
|
||
defer na.mu.Unlock()
|
||
|
||
for _, n := range na.config.AllowedNodes {
|
||
if n == nodeID {
|
||
return
|
||
}
|
||
}
|
||
na.config.AllowedNodes = append(na.config.AllowedNodes, nodeID)
|
||
}
|
||
|
||
// RemoveAllowedNode удаляет разрешённый узел
|
||
func (na *NodeAuthenticator) RemoveAllowedNode(nodeID string) {
|
||
na.mu.Lock()
|
||
defer na.mu.Unlock()
|
||
|
||
newList := make([]string, 0)
|
||
for _, n := range na.config.AllowedNodes {
|
||
if n != nodeID {
|
||
newList = append(newList, n)
|
||
}
|
||
}
|
||
na.config.AllowedNodes = newList
|
||
}
|