new tron gateway
This commit is contained in:
13
api/gateway/tron/internal/keymanager/config.go
Normal file
13
api/gateway/tron/internal/keymanager/config.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package keymanager
|
||||
|
||||
import "github.com/tech/sendico/pkg/model"
|
||||
|
||||
// Driver identifies the key management backend implementation.
|
||||
type Driver string
|
||||
|
||||
const (
|
||||
DriverVault Driver = "vault"
|
||||
)
|
||||
|
||||
// Config represents a configured key manager driver with arbitrary settings.
|
||||
type Config = model.DriverConfig[Driver]
|
||||
26
api/gateway/tron/internal/keymanager/keymanager.go
Normal file
26
api/gateway/tron/internal/keymanager/keymanager.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package keymanager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/fbsobreira/gotron-sdk/pkg/proto/core"
|
||||
)
|
||||
|
||||
// ManagedWalletKey captures information returned after provisioning a managed wallet key.
|
||||
type ManagedWalletKey struct {
|
||||
KeyID string
|
||||
Address string
|
||||
PublicKey string
|
||||
}
|
||||
|
||||
// Manager defines the contract for managing managed wallet keys.
|
||||
type Manager interface {
|
||||
// CreateManagedWalletKey provisions a new managed wallet key for the provided wallet reference and network.
|
||||
CreateManagedWalletKey(ctx context.Context, walletRef string, network string) (*ManagedWalletKey, error)
|
||||
// SignTransaction signs the provided transaction using the identified key material.
|
||||
SignTransaction(ctx context.Context, keyID string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error)
|
||||
// SignTronTransaction signs a native TRON transaction using the identified key material.
|
||||
SignTronTransaction(ctx context.Context, keyID string, tx *core.Transaction) (*core.Transaction, error)
|
||||
}
|
||||
341
api/gateway/tron/internal/keymanager/vault/manager.go
Normal file
341
api/gateway/tron/internal/keymanager/vault/manager.go
Normal file
@@ -0,0 +1,341 @@
|
||||
package vault
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdecdsa "crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
troncore "github.com/fbsobreira/gotron-sdk/pkg/proto/core"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"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 struct {
|
||||
Address string `mapstructure:"address"`
|
||||
TokenEnv string `mapstructure:"token_env"`
|
||||
Namespace string `mapstructure:"namespace"`
|
||||
MountPath string `mapstructure:"mount_path"`
|
||||
KeyPrefix string `mapstructure:"key_prefix"`
|
||||
}
|
||||
|
||||
// Manager implements the keymanager.Manager contract backed by HashiCorp Vault.
|
||||
type Manager struct {
|
||||
logger mlogger.Logger
|
||||
client *api.Client
|
||||
store *api.KVv2
|
||||
keyPrefix string
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
address := strings.TrimSpace(cfg.Address)
|
||||
if address == "" {
|
||||
logger.Error("Vault address missing")
|
||||
return nil, merrors.InvalidArgument("vault key manager: address is required")
|
||||
}
|
||||
tokenEnv := strings.TrimSpace(cfg.TokenEnv)
|
||||
if tokenEnv == "" {
|
||||
logger.Error("Vault token env missing")
|
||||
return nil, merrors.InvalidArgument("vault key manager: token_env is required")
|
||||
}
|
||||
token := strings.TrimSpace(os.Getenv(tokenEnv))
|
||||
if token == "" {
|
||||
logger.Error("Vault token missing; expected Vault Agent to export token", zap.String("env", tokenEnv))
|
||||
return nil, merrors.InvalidArgument("vault key manager: token env " + tokenEnv + " is not set (expected Vault Agent sink to populate it)")
|
||||
}
|
||||
mountPath := strings.Trim(strings.TrimSpace(cfg.MountPath), "/")
|
||||
if mountPath == "" {
|
||||
logger.Error("Vault mount path missing")
|
||||
return nil, merrors.InvalidArgument("vault key manager: mount_path is required")
|
||||
}
|
||||
keyPrefix := strings.Trim(strings.TrimSpace(cfg.KeyPrefix), "/")
|
||||
if keyPrefix == "" {
|
||||
keyPrefix = "gateway/chain/wallets"
|
||||
}
|
||||
|
||||
clientCfg := api.DefaultConfig()
|
||||
clientCfg.Address = address
|
||||
|
||||
client, err := api.NewClient(clientCfg)
|
||||
if err != nil {
|
||||
logger.Error("Failed to create vault client", zap.Error(err))
|
||||
return nil, merrors.Internal("vault key manager: failed to create client: " + err.Error())
|
||||
}
|
||||
client.SetToken(token)
|
||||
if ns := strings.TrimSpace(cfg.Namespace); ns != "" {
|
||||
client.SetNamespace(ns)
|
||||
}
|
||||
|
||||
kv := client.KVv2(mountPath)
|
||||
|
||||
return &Manager{
|
||||
logger: logger.Named("vault"),
|
||||
client: client,
|
||||
store: kv,
|
||||
keyPrefix: keyPrefix,
|
||||
}, 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) {
|
||||
if strings.TrimSpace(walletRef) == "" {
|
||||
m.logger.Warn("WalletRef missing for managed key creation", zap.String("network", network))
|
||||
return nil, merrors.InvalidArgument("vault key manager: walletRef is required")
|
||||
}
|
||||
if strings.TrimSpace(network) == "" {
|
||||
m.logger.Warn("Network missing for managed key creation", zap.String("wallet_ref", walletRef))
|
||||
return nil, merrors.InvalidArgument("vault key manager: network is required")
|
||||
}
|
||||
|
||||
privateKey, err := stdecdsa.GenerateKey(secp256k1.S256(), rand.Reader)
|
||||
if err != nil {
|
||||
m.logger.Warn("Failed to generate managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", network), zap.Error(err))
|
||||
return nil, merrors.Internal("vault key manager: failed to generate key: " + err.Error())
|
||||
}
|
||||
privateKeyBytes := crypto.FromECDSA(privateKey)
|
||||
publicKey := privateKey.PublicKey
|
||||
publicKeyBytes := crypto.FromECDSAPub(&publicKey)
|
||||
publicKeyHex := hex.EncodeToString(publicKeyBytes)
|
||||
address := crypto.PubkeyToAddress(publicKey).Hex()
|
||||
|
||||
err = m.persistKey(ctx, walletRef, network, privateKeyBytes, publicKeyBytes, address)
|
||||
if err != nil {
|
||||
m.logger.Warn("Failed to persist managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", network), zap.Error(err))
|
||||
zeroBytes(privateKeyBytes)
|
||||
zeroBytes(publicKeyBytes)
|
||||
return nil, err
|
||||
}
|
||||
zeroBytes(privateKeyBytes)
|
||||
zeroBytes(publicKeyBytes)
|
||||
|
||||
m.logger.Info("Managed wallet key created",
|
||||
zap.String("wallet_ref", walletRef),
|
||||
zap.String("network", network),
|
||||
zap.String("address", strings.ToLower(address)),
|
||||
)
|
||||
|
||||
return &keymanager.ManagedWalletKey{
|
||||
KeyID: m.buildKeyID(network, walletRef),
|
||||
Address: strings.ToLower(address),
|
||||
PublicKey: publicKeyHex,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) persistKey(ctx context.Context, walletRef, network string, privateKey, publicKey []byte, address string) error {
|
||||
secretPath := m.buildKeyID(network, walletRef)
|
||||
payload := map[string]interface{}{
|
||||
"private_key": hex.EncodeToString(privateKey),
|
||||
"public_key": hex.EncodeToString(publicKey),
|
||||
"address": strings.ToLower(address),
|
||||
"network": strings.ToLower(network),
|
||||
}
|
||||
if _, err := m.store.Put(ctx, secretPath, payload); err != nil {
|
||||
return merrors.Internal("vault key manager: failed to write secret at " + secretPath + ": " + err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) buildKeyID(network, walletRef string) string {
|
||||
net := strings.Trim(strings.ToLower(network), "/")
|
||||
return path.Join(m.keyPrefix, net, walletRef)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if strings.TrimSpace(keyID) == "" {
|
||||
m.logger.Warn("Signing failed: empty key id")
|
||||
return nil, merrors.InvalidArgument("vault key manager: keyID is required")
|
||||
}
|
||||
if tx == nil {
|
||||
m.logger.Warn("Signing failed: nil transaction", zap.String("key_id", keyID))
|
||||
return nil, merrors.InvalidArgument("vault key manager: transaction is nil")
|
||||
}
|
||||
if chainID == nil {
|
||||
m.logger.Warn("Signing failed: nil chain id", zap.String("key_id", keyID))
|
||||
return nil, merrors.InvalidArgument("vault key manager: chainID is nil")
|
||||
}
|
||||
|
||||
material, err := m.loadKey(ctx, keyID)
|
||||
if err != nil {
|
||||
m.logger.Warn("Failed to load key material", 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", zap.String("key_id", keyID), zap.Error(err))
|
||||
return nil, merrors.Internal("vault key manager: invalid key material: " + err.Error())
|
||||
}
|
||||
defer zeroBytes(keyBytes)
|
||||
|
||||
privateKey, err := crypto.ToECDSA(keyBytes)
|
||||
if err != nil {
|
||||
m.logger.Warn("Failed to construct private key", zap.String("key_id", keyID), zap.Error(err))
|
||||
return nil, merrors.Internal("vault key manager: failed to construct private key: " + err.Error())
|
||||
}
|
||||
|
||||
signed, err := types.SignTx(tx, types.LatestSignerForChainID(chainID), privateKey)
|
||||
if err != nil {
|
||||
m.logger.Warn("Failed to sign transaction", zap.String("key_id", keyID), zap.Error(err))
|
||||
return nil, merrors.Internal("vault key manager: failed to sign transaction: " + err.Error())
|
||||
}
|
||||
m.logger.Info("Transaction signed with managed key",
|
||||
zap.String("key_id", keyID),
|
||||
zap.String("network", material.Network),
|
||||
zap.String("tx_hash", signed.Hash().Hex()),
|
||||
)
|
||||
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.loadKey(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
|
||||
}
|
||||
|
||||
type keyMaterial struct {
|
||||
PrivateKey string
|
||||
PublicKey string
|
||||
Address string
|
||||
Network string
|
||||
}
|
||||
|
||||
func (m *Manager) loadKey(ctx context.Context, keyID string) (*keyMaterial, error) {
|
||||
secretPath := strings.Trim(strings.TrimPrefix(keyID, "/"), "/")
|
||||
secret, err := m.store.Get(ctx, secretPath)
|
||||
if err != nil {
|
||||
m.logger.Warn("Failed to read secret", zap.String("path", secretPath), zap.Error(err))
|
||||
return nil, merrors.Internal("vault key manager: failed to read secret at " + secretPath + ": " + err.Error())
|
||||
}
|
||||
if secret == nil || secret.Data == nil {
|
||||
m.logger.Warn("Secret not found", zap.String("path", secretPath))
|
||||
return nil, merrors.NoData("vault key manager: secret " + secretPath + " not found")
|
||||
}
|
||||
|
||||
getString := func(key string) (string, error) {
|
||||
val, ok := secret.Data[key]
|
||||
if !ok {
|
||||
m.logger.Warn("Secret missing field", zap.String("path", secretPath), zap.String("field", key))
|
||||
return "", merrors.Internal("vault key manager: secret " + secretPath + " missing " + key)
|
||||
}
|
||||
str, ok := val.(string)
|
||||
if !ok || strings.TrimSpace(str) == "" {
|
||||
m.logger.Warn("Secret field invalid", zap.String("path", secretPath), zap.String("field", key))
|
||||
return "", merrors.Internal("vault key manager: secret " + secretPath + " invalid " + key)
|
||||
}
|
||||
return str, nil
|
||||
}
|
||||
|
||||
privateKey, err := getString("private_key")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
publicKey, err := getString("public_key")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
address, err := getString("address")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
network, err := getString("network")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &keyMaterial{
|
||||
PrivateKey: privateKey,
|
||||
PublicKey: publicKey,
|
||||
Address: address,
|
||||
Network: network,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func zeroBytes(data []byte) {
|
||||
for i := range data {
|
||||
data[i] = 0
|
||||
}
|
||||
}
|
||||
|
||||
var _ keymanager.Manager = (*Manager)(nil)
|
||||
Reference in New Issue
Block a user