diff --git a/.golangci.yml b/.golangci.yml index d32f84a..d3a0de6 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -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: diff --git a/config/config.go b/config/config.go index ab2e370..5578f10 100644 --- a/config/config.go +++ b/config/config.go @@ -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/ FileConfig `yaml:",inline"` diff --git a/config/validate.go b/config/validate.go index 9de6f4b..37fa12e 100644 --- a/config/validate.go +++ b/config/validate.go @@ -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. diff --git a/go.mod b/go.mod index c5a9eab..8fb21cb 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 diff --git a/internal/app/build.go b/internal/app/build.go index 55dd4cb..3bbc81c 100644 --- a/internal/app/build.go +++ b/internal/app/build.go @@ -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, diff --git a/internal/signer/privkey_adapter.go b/internal/signer/privkey_adapter.go index e55c881..5b46479 100644 --- a/internal/signer/privkey_adapter.go +++ b/internal/signer/privkey_adapter.go @@ -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" @@ -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 { @@ -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 diff --git a/internal/signer/privkey_adapter_test.go b/internal/signer/privkey_adapter_test.go index 33c1758..62f6d3c 100644 --- a/internal/signer/privkey_adapter_test.go +++ b/internal/signer/privkey_adapter_test.go @@ -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. @@ -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") }