Skip to content

Commit 5a8777a

Browse files
mergify[bot]zivkovicmilosjulienrbrt
authored
feat: add support for ed25519 tx signature verification (backport #23283) (#23606)
Co-authored-by: Miloš Živković <[email protected]> Co-authored-by: Julien Robert <[email protected]>
1 parent 8b9030f commit 5a8777a

File tree

7 files changed

+99
-31
lines changed

7 files changed

+99
-31
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ Ref: https://keepachangelog.com/en/1.0.0/
4040

4141
Every module contains its own CHANGELOG.md. Please refer to the module you are interested in.
4242

43+
### Features
44+
45+
* (x/auth/ante) [#23283](https://github.com/cosmos/cosmos-sdk/pull/23283) Allow ed25519 transaction signatures.
46+
4347
### Bug Fixes
4448

4549
* (codec) [#23504](https://github.com/cosmos/cosmos-sdk/pull/23504) Provide `*codec.LegacyAmino` alongside `registry.AminoRegistrar` by default in depinject provider.

crypto/keys/ed25519/ed25519.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"io"
99

10+
"filippo.io/edwards25519"
1011
"github.com/cometbft/cometbft/crypto"
1112
"github.com/cometbft/cometbft/crypto/tmhash"
1213
"github.com/hdevalence/ed25519consensus"
@@ -18,7 +19,7 @@ import (
1819
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
1920
)
2021

21-
//-------------------------------------
22+
// -------------------------------------
2223

2324
const (
2425
PrivKeyName = "tendermint/PrivKeyEd25519"
@@ -153,7 +154,7 @@ func GenPrivKeyFromSecret(secret []byte) *PrivKey {
153154
return &PrivKey{Key: ed25519.NewKeyFromSeed(seed)}
154155
}
155156

156-
//-------------------------------------
157+
// -------------------------------------
157158

158159
var (
159160
_ cryptotypes.PubKey = &PubKey{}
@@ -230,3 +231,34 @@ func (pubKey PubKey) MarshalAminoJSON() ([]byte, error) {
230231
func (pubKey *PubKey) UnmarshalAminoJSON(bz []byte) error {
231232
return pubKey.UnmarshalAmino(bz)
232233
}
234+
235+
// identityPoint is the “neutral element” in the ed25519 group, where
236+
// point addition with identityPoint leaves the other point unchanged.
237+
// It corresponds to coordinates (0, 1) in Edwards form and is not a valid public key
238+
var identityPoint = edwards25519.NewIdentityPoint()
239+
240+
// IsOnCurve checks that a 32B ed25519 public key is on the curve.
241+
// The check fails for ed25519 identity points
242+
func (pubKey *PubKey) IsOnCurve() bool {
243+
// Make sure the public key is exactly 32B
244+
if len(pubKey.Key) != ed25519.PublicKeySize {
245+
// Invalid key size
246+
return false
247+
}
248+
249+
// Make sure the public key bytes decodes into an ed25519 point
250+
point, err := new(edwards25519.Point).SetBytes(pubKey.Key)
251+
if err != nil || point == nil {
252+
// Not a valid point on the curve
253+
return false
254+
}
255+
256+
// Make sure the public key is not the identity point (all zeroes)
257+
if point.Equal(identityPoint) == 1 {
258+
// Public key is the identity point (useless)
259+
return false
260+
}
261+
262+
// Public key is a valid point on the ed25519 curve
263+
return true
264+
}

crypto/keys/ed25519/ed25519_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package ed25519_test
22

33
import (
44
stded25519 "crypto/ed25519"
5+
"crypto/rand"
56
"encoding/base64"
67
"testing"
78

9+
"filippo.io/edwards25519"
810
"github.com/cometbft/cometbft/crypto"
911
tmed25519 "github.com/cometbft/cometbft/crypto/ed25519"
1012
"github.com/stretchr/testify/assert"
@@ -254,3 +256,40 @@ func TestMarshalJSON(t *testing.T) {
254256
require.NoError(err)
255257
require.True(pk2.Equals(pk))
256258
}
259+
260+
func TestPubKeyOnCurve(t *testing.T) {
261+
t.Parallel()
262+
263+
t.Run("invalid public key size", func(t *testing.T) {
264+
t.Parallel()
265+
266+
key := &ed25519.PubKey{
267+
Key: make(stded25519.PublicKey, ed25519.PubKeySize+1),
268+
}
269+
270+
assert.False(t, key.IsOnCurve())
271+
})
272+
273+
t.Run("identity point", func(t *testing.T) {
274+
t.Parallel()
275+
276+
key := &ed25519.PubKey{
277+
Key: stded25519.PublicKey(edwards25519.NewIdentityPoint().Bytes()),
278+
}
279+
280+
assert.False(t, key.IsOnCurve())
281+
})
282+
283+
t.Run("valid public key", func(t *testing.T) {
284+
t.Parallel()
285+
286+
publicKey, _, err := stded25519.GenerateKey(rand.Reader)
287+
require.NoError(t, err)
288+
289+
key := &ed25519.PubKey{
290+
Key: publicKey,
291+
}
292+
293+
assert.True(t, key.IsOnCurve())
294+
})
295+
}

docs/learn/beginner/03-accounts.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ The Cosmos SDK supports the following digital key schemes for creating digital s
5353
* `tm-ed25519`, as implemented in the [Cosmos SDK `crypto/keys/ed25519` package](https://github.com/cosmos/cosmos-sdk/blob/v0.52.0-beta.2/crypto/keys/ed25519/ed25519.go). This scheme is supported only for the consensus validation.
5454

5555
| | Address length in bytes | Public key length in bytes | Used for transaction authentication | Used for consensus (cometbft) |
56-
| :----------: | :---------------------: | :------------------------: | :---------------------------------: | :-----------------------------: |
56+
| :----------: |:-----------------------:| :------------------------: |:-----------------------------------:| :-----------------------------: |
5757
| `secp256k1` | 20 | 33 | yes | no |
5858
| `secp256r1` | 32 | 33 | yes | no |
59-
| `tm-ed25519` | -- not used -- | 32 | no | yes |
59+
| `tm-ed25519` | 20 | 32 | yes | yes |
6060

6161
## Addresses
6262

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,10 @@ require (
6464
sigs.k8s.io/yaml v1.4.0
6565
)
6666

67+
require filippo.io/edwards25519 v1.1.0
68+
6769
require (
6870
buf.build/gen/go/cosmos/gogo-proto/protocolbuffers/go v1.36.4-20240130113600-88ef6483f90f.1 // indirect
69-
filippo.io/edwards25519 v1.1.0 // indirect
7071
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
7172
github.com/DataDog/datadog-go v4.8.3+incompatible // indirect
7273
github.com/DataDog/zstd v1.5.6 // indirect

x/auth/ante/sigverify.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ func (svd SigVerificationDecorator) VerifyIsOnCurve(pubKey cryptotypes.PubKey) e
125125
}
126126

127127
switch typedPubKey := pubKey.(type) {
128+
case *ed25519.PubKey:
129+
if !typedPubKey.IsOnCurve() {
130+
return errorsmod.Wrap(sdkerrors.ErrInvalidPubKey, "ed25519 key is not on curve")
131+
}
128132
case *secp256k1.PubKey:
129133
pubKeyObject, err := secp256k1dcrd.ParsePubKey(typedPubKey.Bytes())
130134
if err != nil {
@@ -535,10 +539,7 @@ func DefaultSigVerificationGasConsumer(meter gas.Meter, sig signing.SignatureV2,
535539

536540
switch pubkey := pubkey.(type) {
537541
case *ed25519.PubKey:
538-
if err := meter.Consume(params.SigVerifyCostED25519, "ante verify: ed25519"); err != nil {
539-
return err
540-
}
541-
return errorsmod.Wrap(sdkerrors.ErrInvalidPubKey, "ED25519 public keys are unsupported")
542+
return meter.Consume(params.SigVerifyCostED25519, "ante verify: ed25519")
542543

543544
case *secp256k1.PubKey:
544545
return meter.Consume(params.SigVerifyCostSecp256k1, "ante verify: secp256k1")

x/auth/ante/sigverify_test.go

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"testing"
66

7+
"github.com/stretchr/testify/assert"
78
"github.com/stretchr/testify/require"
89
"go.uber.org/mock/gomock"
910

@@ -74,7 +75,7 @@ func TestConsumeSignatureVerificationGas(t *testing.T) {
7475
args{nil, ed25519.GenPrivKey().PubKey(), params, func(mm *gastestutil.MockMeter) {
7576
mm.EXPECT().Consume(p.SigVerifyCostED25519, "ante verify: ed25519").Times(1)
7677
}},
77-
true,
78+
false,
7879
},
7980
{
8081
"PubKeySecp256k1",
@@ -390,23 +391,21 @@ func TestAnteHandlerChecks(t *testing.T) {
390391
anteHandler := sdk.ChainAnteDecorators(sigVerificationDecorator)
391392

392393
type testCase struct {
393-
name string
394-
privs []cryptotypes.PrivKey
395-
msg sdk.Msg
396-
accNums []uint64
397-
accSeqs []uint64
398-
shouldErr bool
399-
supported bool
394+
name string
395+
privs []cryptotypes.PrivKey
396+
msg sdk.Msg
397+
accNums []uint64
398+
accSeqs []uint64
400399
}
401400

402401
// Secp256r1 keys that are not on curve will fail before even doing any operation i.e when trying to get the pubkey
403402
testCases := []testCase{
404-
{"secp256k1_onCurve", []cryptotypes.PrivKey{priv1}, msgs[0], []uint64{accs[0].GetAccountNumber()}, []uint64{0}, false, true},
405-
{"secp256r1_onCurve", []cryptotypes.PrivKey{priv2}, msgs[1], []uint64{accs[1].GetAccountNumber()}, []uint64{0}, false, true},
406-
{"ed255619", []cryptotypes.PrivKey{priv3}, msgs[2], []uint64{accs[2].GetAccountNumber()}, []uint64{2}, true, false},
403+
{"secp256k1_onCurve", []cryptotypes.PrivKey{priv1}, msgs[0], []uint64{accs[0].GetAccountNumber()}, []uint64{0}},
404+
{"secp256r1_onCurve", []cryptotypes.PrivKey{priv2}, msgs[1], []uint64{accs[1].GetAccountNumber()}, []uint64{0}},
405+
{"ed255619", []cryptotypes.PrivKey{priv3}, msgs[2], []uint64{accs[2].GetAccountNumber()}, []uint64{2}},
407406
}
408407

409-
for i, tc := range testCases {
408+
for _, tc := range testCases {
410409
t.Run(fmt.Sprintf("%s key", tc.name), func(t *testing.T) {
411410
suite.txBuilder = suite.clientCtx.TxConfig.NewTxBuilder() // Create new txBuilder for each test
412411

@@ -423,16 +422,8 @@ func TestAnteHandlerChecks(t *testing.T) {
423422

424423
byteCtx := suite.ctx.WithTxBytes(txBytes)
425424
_, err = anteHandler(byteCtx, tx, true)
426-
if tc.shouldErr {
427-
require.NotNil(t, err, "TestCase %d: %s did not error as expected", i, tc.name)
428-
if tc.supported {
429-
require.ErrorContains(t, err, "not on curve")
430-
} else {
431-
require.ErrorContains(t, err, "unsupported key type")
432-
}
433-
} else {
434-
require.Nil(t, err, "TestCase %d: %s errored unexpectedly. Err: %v", i, tc.name, err)
435-
}
425+
426+
assert.NoError(t, err)
436427
})
437428
}
438429
}

0 commit comments

Comments
 (0)