Skip to content
Merged
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ linters:
- github.com/decred/dcrd/dcrec/secp256k1
- github.com/oasisprotocol/curve25519-voi/primitives/ed25519
- github.com/cloudflare/circl/sign/mldsa/mldsa65
- golang.org/x/crypto/sha3
- google.golang.org/grpc
- gopkg.in/yaml.v3
test:
Expand Down
5 changes: 3 additions & 2 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,14 @@ type GRPCConfig struct {

// GRPCKey binds a signing key to an id (the SignerService key handle clients
// address). Backend selects the custodian and Algorithm the key type. The
// supported combinations are file/secp256k1, awskms/ed25519, and awskms/secp256k1.
// supported combinations are file/{ed25519,secp256k1eth} and
// awskms/{ed25519,secp256k1,secp256k1eth}.
// PKCS#11 is not yet supported over gRPC. The server performs no caller
// authorization, so every configured key is usable by any connecting client.
type GRPCKey struct {
ID string `yaml:"id"`
Backend Backend `yaml:"backend"` // "file" | "awskms"
Algorithm Algorithm `yaml:"algorithm"` // file: "secp256k1"; awskms: "ed25519" | "secp256k1"
Algorithm Algorithm `yaml:"algorithm"` // file: "ed25519" | "secp256k1eth"; awskms: "ed25519" | "secp256k1" | "secp256k1eth"
KeyID string `yaml:"key_id"` // awskms: KMS id, ARN, or alias/<name>

FileConfig `yaml:",inline"`
Expand Down
4 changes: 2 additions & 2 deletions config/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import (
// supportedPKCS11Algorithms mirrors the algo registry in signing/pkcs11. It is
// duplicated here so config validation does not have to import the cgo-backed
// pkcs11 package. Keep the two in sync when adding a key type.
var supportedPKCS11Algorithms = map[Algorithm]bool{AlgoED25519: true}
var supportedPKCS11Algorithms = map[Algorithm]bool{AlgoED25519: true, AlgoSecp256k1Eth: true}

// supportedAWSKMSAlgorithms mirrors the algo registry in signing/awskms. It is
// duplicated here so config validation does not have to import the awskms
// package. Keep the two in sync when adding a key type.
var supportedAWSKMSAlgorithms = map[Algorithm]bool{AlgoED25519: true, AlgoSecp256k1: true}
var supportedAWSKMSAlgorithms = map[Algorithm]bool{AlgoED25519: true, AlgoSecp256k1: true, AlgoSecp256k1Eth: true}

// Validate resolves defaults and enforces fail-fast invariants. home is the base
// directory used to resolve relative paths and default state files.
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
golang.org/x/crypto v0.52.0
google.golang.org/grpc v1.81.1
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af
gopkg.in/yaml.v3 v3.0.1
Expand Down Expand Up @@ -146,7 +147,6 @@ require (
go.uber.org/mock v0.5.2 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/net v0.55.0 // indirect
Expand Down
10 changes: 7 additions & 3 deletions internal/app/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,15 +246,19 @@ func NewServer(c *config.Config, home string, logger log.Logger) (srv *Server, e
// backend/algorithm.
//
// Only currently supported configurations of the grpc signer:
// - File backend with secp key
// - AWS KMS backend with ed25519 or secp key
// - File backend with ed25519 or secp256k1eth key
// - AWS KMS backend with ed25519, secp256k1, or secp256k1eth key
func newGRPCSigner(home string, k config.GRPCKey) (signing.Signer, error) {
be, algo := k.Backend, k.Algorithm

switch {
case be == config.BackendFile && algo == config.AlgoED25519:
return file.LoadEd25519(k.KeyFile)
case be == config.BackendFile && algo == config.AlgoSecp256k1Eth:
return file.LoadSecp256k1Eth(k.KeyFile)
case be == config.BackendAWSKMS && (algo == config.AlgoED25519 || algo == config.AlgoSecp256k1):
case be == config.BackendAWSKMS &&
(algo == config.AlgoED25519 || algo == config.AlgoSecp256k1 || algo == config.AlgoSecp256k1Eth):

return awskms.Open(context.Background(), awskms.Config{
KeyID: k.KeyID,
Region: k.Region,
Expand Down
23 changes: 20 additions & 3 deletions internal/signer/privkey_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
cometed25519 "github.com/cometbft/cometbft/crypto/ed25519"
cometmldsa "github.com/cometbft/cometbft/crypto/mldsa65"
cometsecp "github.com/cometbft/cometbft/crypto/secp256k1"
cometsecpeth "github.com/cometbft/cometbft/crypto/secp256k1eth"
"golang.org/x/crypto/sha3"

"github.com/cosmos/kms/config"
"github.com/cosmos/kms/signing"
Expand All @@ -33,6 +35,12 @@ func newSignerPrivKey(ctx context.Context, s signing.Signer) (crypto.PrivKey, er
pub = cometed25519.PubKey(s.PubKey())
case config.AlgoSecp256k1:
pub = cometsecp.PubKey(s.PubKey())
case config.AlgoSecp256k1Eth:
epub, err := cometsecpeth.NewPubKeyFromBytes(s.PubKey())
if err != nil {
return nil, fmt.Errorf("signer: secp256k1eth pubkey: %w", err)
}
pub = epub
case config.AlgoMLDSA65:
mpub, err := cometmldsa.NewPubKeyFromBytes(s.PubKey())
if err != nil {
Expand All @@ -45,9 +53,18 @@ func newSignerPrivKey(ctx context.Context, s signing.Signer) (crypto.PrivKey, er
return &signerPrivKey{ctx: ctx, s: s, pub: pub}, nil
}

func (k *signerPrivKey) Sign(msg []byte) ([]byte, error) { return k.s.Sign(k.ctx, msg) }
func (k *signerPrivKey) PubKey() crypto.PubKey { return k.pub }
func (k *signerPrivKey) Type() string { return k.pub.Type() }
// Sign signs the consensus sign-bytes.
// secp256k1eth signers hash with Keccak256 first
func (k *signerPrivKey) Sign(msg []byte) ([]byte, error) {
if k.s.Scheme() == config.AlgoSecp256k1Eth {
h := sha3.NewLegacyKeccak256()
_, _ = h.Write(msg)
msg = h.Sum(nil)
}
return k.s.Sign(k.ctx, msg)
}
func (k *signerPrivKey) PubKey() crypto.PubKey { return k.pub }
func (k *signerPrivKey) Type() string { return k.pub.Type() }

// Bytes is unsupported: remote/HSM keys never expose private material. It returns
// nil rather than panicking because crypto.PrivKey requires the method; it is not
Expand Down
28 changes: 24 additions & 4 deletions internal/signer/privkey_adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ package signer

import (
"context"
"crypto/rand"
"testing"

"github.com/cometbft/cometbft/crypto"
"github.com/cometbft/cometbft/crypto/ed25519"
"github.com/cometbft/cometbft/crypto/mldsa65"
"github.com/cometbft/cometbft/crypto/secp256k1eth"
"github.com/stretchr/testify/require"

"github.com/cosmos/kms/config"
"github.com/cosmos/kms/signing/file"
)

// stubSigner is a minimal ed25519 signing.Signer for tests.
Expand Down Expand Up @@ -60,13 +63,30 @@ func TestAdapterMLDSA65(t *testing.T) {
require.True(t, pk.PubKey().VerifySignature(msg, sig))
}

// ethStub reports the eth scheme, which has no cometbft pubkey type.
type ethStub struct{ stubSigner }
// TestAdapterSecp256k1Eth proves the adapter's Keccak-256 pre-hash matches what
// cometbft secp256k1eth verification applies: a digest-signing eth signer
// wrapped by the adapter must verify against the raw sign-bytes.
func TestAdapterSecp256k1Eth(t *testing.T) {
fs, err := file.GenerateSecp256k1Eth(rand.Reader)
require.NoError(t, err)

pk, err := newSignerPrivKey(context.Background(), fs)
require.NoError(t, err)
require.Equal(t, secp256k1eth.KeyType, pk.Type())

msg := []byte("hello")
sig, err := pk.Sign(msg)
require.NoError(t, err)
require.True(t, pk.PubKey().VerifySignature(msg, sig))
}

// bogusStub reports a scheme with no cometbft pubkey type.
type bogusStub struct{ stubSigner }

func (ethStub) Scheme() config.Algorithm { return config.AlgoSecp256k1Eth }
func (bogusStub) Scheme() config.Algorithm { return config.Algorithm("bogus") }

func TestAdapterRejectsSchemeWithoutCometPubKey(t *testing.T) {
st := ethStub{stubSigner{priv: ed25519.GenPrivKey()}}
st := bogusStub{stubSigner{priv: ed25519.GenPrivKey()}}
_, err := newSignerPrivKey(context.Background(), st)
require.ErrorContains(t, err, "no cometbft pubkey type")
}