Files
sendico/api/gateway/tron/internal/keymanager/vault/manager.go
2026-02-28 10:10:26 +01:00

154 lines
5.2 KiB
Go

package vault
import (
"context"
"crypto/sha256"
"encoding/hex"
"math/big"
"strings"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
"github.com/ethereum/go-ethereum/core/types"
troncore "github.com/fbsobreira/gotron-sdk/pkg/proto/core"
"github.com/tech/sendico/pkg/vault/managedkey"
"go.uber.org/zap"
"google.golang.org/protobuf/proto"
"github.com/tech/sendico/gateway/tron/internal/keymanager"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
)
// Config describes how to connect to Vault for managed wallet keys.
type Config = managedkey.Config
// Manager implements the keymanager.Manager contract backed by HashiCorp Vault.
type Manager struct {
logger mlogger.Logger
keys managedkey.Service
}
// New constructs a Vault-backed key manager.
func New(logger mlogger.Logger, cfg Config) (*Manager, error) {
if logger == nil {
return nil, merrors.InvalidArgument("vault key manager: logger is required")
}
keys, err := managedkey.New(managedkey.Options{
Logger: logger,
Config: managedkey.Config(cfg),
Component: "vault key manager",
DefaultKeyPrefix: "gateway/tron/wallets",
})
if err != nil {
logger.Error("Failed to initialise vault key manager", zap.Error(err))
return nil, err
}
return &Manager{
logger: logger.Named("vault"),
keys: keys,
}, nil
}
// CreateManagedWalletKey creates a new managed wallet key and stores it in Vault.
func (m *Manager) CreateManagedWalletKey(ctx context.Context, walletRef string, network string) (*keymanager.ManagedWalletKey, error) {
created, err := m.keys.CreateManagedWalletKey(ctx, walletRef, network)
if err != nil {
m.logger.Warn("Failed to create managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", network), zap.Error(err))
return nil, err
}
return &keymanager.ManagedWalletKey{
KeyID: created.KeyID,
Address: created.Address,
PublicKey: created.PublicKey,
}, nil
}
// SignTransaction loads the key material from Vault and signs the transaction.
func (m *Manager) SignTransaction(ctx context.Context, keyID string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
signed, err := m.keys.SignEVMTransaction(ctx, keyID, tx, chainID)
if err != nil {
m.logger.Warn("Failed to sign transaction", zap.String("key_id", keyID), zap.Error(err))
return nil, err
}
return signed, nil
}
// SignTronTransaction signs a native TRON transaction using the identified key material.
func (m *Manager) SignTronTransaction(ctx context.Context, keyID string, tx *troncore.Transaction) (*troncore.Transaction, error) {
if strings.TrimSpace(keyID) == "" {
m.logger.Warn("TRON signing failed: empty key id")
return nil, merrors.InvalidArgument("vault key manager: keyID is required")
}
if tx == nil {
m.logger.Warn("TRON signing failed: nil transaction", zap.String("key_id", keyID))
return nil, merrors.InvalidArgument("vault key manager: transaction is nil")
}
if tx.GetRawData() == nil {
m.logger.Warn("TRON signing failed: nil raw data", zap.String("key_id", keyID))
return nil, merrors.InvalidArgument("vault key manager: transaction raw_data is nil")
}
material, err := m.keys.LoadKeyMaterial(ctx, keyID)
if err != nil {
m.logger.Warn("Failed to load key material for TRON signing", zap.String("key_id", keyID), zap.Error(err))
return nil, err
}
keyBytes, err := hex.DecodeString(material.PrivateKey)
if err != nil {
m.logger.Warn("Invalid key material for TRON signing", zap.String("key_id", keyID), zap.Error(err))
return nil, merrors.Internal("vault key manager: invalid key material: " + err.Error())
}
defer zeroBytes(keyBytes)
// Marshal the raw_data to bytes for hashing
rawBytes, err := proto.Marshal(tx.GetRawData())
if err != nil {
m.logger.Warn("Failed to marshal TRON transaction raw_data", zap.String("key_id", keyID), zap.Error(err))
return nil, merrors.Internal("vault key manager: failed to marshal transaction: " + err.Error())
}
// SHA256 hash of the raw_data
hash := sha256.Sum256(rawBytes)
// Create secp256k1 private key
privKey := secp256k1.PrivKeyFromBytes(keyBytes)
// Sign using compact signature format (65 bytes: r[32] || s[32] || recovery_id[1])
signature := ecdsa.SignCompact(privKey, hash[:], false)
// TRON expects signature in format: r[32] || s[32] || v[1]
// SignCompact returns: recovery_id[1] || r[32] || s[32]
// We need to rearrange to: r[32] || s[32] || recovery_id[1]
if len(signature) != 65 {
m.logger.Warn("Unexpected signature length", zap.String("key_id", keyID), zap.Int("length", len(signature)))
return nil, merrors.Internal("vault key manager: unexpected signature length")
}
tronSig := make([]byte, 65)
copy(tronSig[0:32], signature[1:33]) // r
copy(tronSig[32:64], signature[33:65]) // s
tronSig[64] = signature[0] // recovery id (v)
// Append signature to transaction
tx.Signature = append(tx.Signature, tronSig)
m.logger.Info("TRON transaction signed with managed key",
zap.String("key_id", keyID),
zap.String("network", material.Network),
)
return tx, nil
}
func zeroBytes(data []byte) {
for i := range data {
data[i] = 0
}
}
var _ keymanager.Manager = (*Manager)(nil)