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