Skip to content

Commit

Permalink
crypto: add secp256r1 (#8559)
Browse files Browse the repository at this point in the history
* Optimize secp256k1 hashing

* Add ADR-028 related functions

* Update ed25519

* fix errors/handle

* fix build

* fix build

* Add tests and update function names

* wip

* Use LengthPrefix for composed addresses

* add tests for NewComposed

* add module hash function

* fix append

* rollback ed25519 ADR-28 update

* rollback ed25519 ADR-28 test

* Adding Module tests and convert tests to test suite

* convert store_key_test.go to test suite

* rollback test check comment

* any.pb.go update

* generated proto files

* wip

* renames

* wip2

* add String method to PBBytes

* wip3

* add pubkey tests

* adding cryptotypes.PrivKey methods

* re-enable test

* fix equals test

* fix ecdsa object receiver

* add ProtoMarshaler implementation and tests

* move code to init and add interface registry

* add bytes tests

* merge Unmarshal with UnmarshalAmino

* implement ProtoMarshaler to ecdsaSK

* remove bytes.go

* add private key marshaling tests

* break tests into 2 suites

* add signature tests

* remove TODO

* remove bytes.proto

* adding changelog

* Update CHANGELOG.md

* Update crypto/keys/ecdsa/ecdsa_privkey.go

* Update crypto/keys/ecdsa/ecdsa_pubkey.go

* comments: add dot (.) at the end

* update comments

* update commented code

* rename files

* remove Amino methods

* use 2 spaces in protocgen.sh

* rollback changes in protocgen.sh

* add MessageName

* rework ecdsa proto structure

* move ecdsa to internal package

* add secp256r1 proto

* refactore proto definition for secp256r1

* fix err check

* update comments

* create const for fieldSize+1

* simplify the PubKey.String test

* Apply suggestions from code review

Co-authored-by: Jonathan Gimeno <jgimeno@gmail.com>

* Update doc comments: SDK Interface -> sdk.Interface

* rename init.go to doc.go

* Add PubKey.Type() test

* Revert "Update doc comments: SDK Interface -> sdk.Interface"

This reverts commit 01f2b4f5efcd79a452483bcda152db54a8fbfee2.

* Use cryptotypes.Address instead of tmcrypto

* Revert "Use cryptotypes.Address instead of tmcrypto"

This reverts commit 15b866ae67bdb7ca4872f4089fcab19f9e2e3608.
This issue will be solved in cosmos/cosmos-sdk#8775

* add link to ANSI X9.62

* move init.go -> doc.go

* use proto.MessageName()

Co-authored-by: Alessio Treglia <alessio@tendermint.com>
Co-authored-by: Jonathan Gimeno <jgimeno@gmail.com>
  • Loading branch information
3 people authored and daeMOn63 committed Aug 20, 2021
1 parent 63d507f commit 78a7172
Show file tree
Hide file tree
Showing 16 changed files with 1,269 additions and 9 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ Ref: https://keepachangelog.com/en/1.0.0/

* [\#8363](https://github.com/cosmos/cosmos-sdk/issues/8363) Addresses no longer have a fixed 20-byte length. From the SDK modules' point of view, any 1-255 bytes-long byte array is a valid address.

## Features

* [\#8559](https://github.com/cosmos/cosmos-sdk/pull/8559) Adding Protobuf compatible secp256r1 ECDSA signatures.

### Client Breaking Changes

* [\#8363](https://github.com/cosmos/cosmos-sdk/pull/8363) Addresses no longer have a fixed 20-byte length. From the SDK modules' point of view, any 1-255 bytes-long byte array is a valid address.
Expand Down
5 changes: 1 addition & 4 deletions codec/types/any.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 8 additions & 5 deletions crypto/codec/proto.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ import (
"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
"github.com/cosmos/cosmos-sdk/crypto/keys/multisig"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256r1"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
)

// RegisterInterfaces registers the sdk.Tx interface.
func RegisterInterfaces(registry codectypes.InterfaceRegistry) {
registry.RegisterInterface("cosmos.crypto.PubKey", (*cryptotypes.PubKey)(nil))
registry.RegisterImplementations((*cryptotypes.PubKey)(nil), &ed25519.PubKey{})
registry.RegisterImplementations((*cryptotypes.PubKey)(nil), &secp256k1.PubKey{})
registry.RegisterImplementations((*cryptotypes.PubKey)(nil), &multisig.LegacyAminoPubKey{})
registry.RegisterImplementations((*cryptotypes.PubKey)(nil), &bls12381.PubKey{})
var pk *cryptotypes.PubKey
registry.RegisterInterface("cosmos.crypto.PubKey", pk)
registry.RegisterImplementations(pk, &ed25519.PubKey{})
registry.RegisterImplementations(pk, &secp256k1.PubKey{})
registry.RegisterImplementations(pk, &multisig.LegacyAminoPubKey{})
registry.RegisterImplementations(pk, &bls12381.PubKey{})
secp256r1.RegisterInterfaces(registry)
}
3 changes: 3 additions & 0 deletions crypto/keys/internal/ecdsa/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package ECDSA implements Cosmos-SDK compatible ECDSA public and private key. The keys
// can be serialized.
package ecdsa
69 changes: 69 additions & 0 deletions crypto/keys/internal/ecdsa/privkey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package ecdsa

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"fmt"
"math/big"
)

// GenPrivKey generates a new secp256r1 private key. It uses operating system randomness.
func GenPrivKey(curve elliptic.Curve) (PrivKey, error) {
key, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
return PrivKey{}, err
}
return PrivKey{*key}, nil
}

type PrivKey struct {
ecdsa.PrivateKey
}

// PubKey returns ECDSA public key associated with this private key.
func (sk *PrivKey) PubKey() PubKey {
return PubKey{sk.PublicKey, nil}
}

// Bytes serialize the private key using big-endian.
func (sk *PrivKey) Bytes() []byte {
if sk == nil {
return nil
}
fieldSize := (sk.Curve.Params().BitSize + 7) / 8
bz := make([]byte, fieldSize)
sk.D.FillBytes(bz)
return bz
}

// Sign hashes and signs the message usign ECDSA. Implements SDK PrivKey interface.
func (sk *PrivKey) Sign(msg []byte) ([]byte, error) {
digest := sha256.Sum256(msg)
return sk.PrivateKey.Sign(rand.Reader, digest[:], nil)
}

// String returns a string representation of the public key based on the curveName.
func (sk *PrivKey) String(name string) string {
return name + "{-}"
}

// MarshalTo implements proto.Marshaler interface.
func (sk *PrivKey) MarshalTo(dAtA []byte) (int, error) {
bz := sk.Bytes()
copy(dAtA, bz)
return len(bz), nil
}

// Unmarshal implements proto.Marshaler interface.
func (sk *PrivKey) Unmarshal(bz []byte, curve elliptic.Curve, expectedSize int) error {
if len(bz) != expectedSize {
return fmt.Errorf("wrong ECDSA SK bytes, expecting %d bytes", expectedSize)
}

sk.Curve = curve
sk.D = new(big.Int).SetBytes(bz)
sk.X, sk.Y = curve.ScalarBaseMult(bz)
return nil
}
66 changes: 66 additions & 0 deletions crypto/keys/internal/ecdsa/privkey_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package ecdsa

import (
"testing"

"github.com/tendermint/tendermint/crypto"

"github.com/stretchr/testify/suite"
)

func TestSKSuite(t *testing.T) {
suite.Run(t, new(SKSuite))
}

type SKSuite struct{ CommonSuite }

func (suite *SKSuite) TestString() {
const prefix = "abc"
suite.Require().Equal(prefix+"{-}", suite.sk.String(prefix))
}

func (suite *SKSuite) TestPubKey() {
pk := suite.sk.PubKey()
suite.True(suite.sk.PublicKey.Equal(&pk.PublicKey))
}

func (suite *SKSuite) Bytes() {
bz := suite.sk.Bytes()
suite.Len(bz, 32)
var sk *PrivKey
suite.Nil(sk.Bytes())
}

func (suite *SKSuite) TestMarshal() {
require := suite.Require()
const size = 32

var buffer = make([]byte, size)
suite.sk.MarshalTo(buffer)

var sk = new(PrivKey)
err := sk.Unmarshal(buffer, secp256r1, size)
require.NoError(err)
require.True(sk.Equal(&suite.sk.PrivateKey))
}

func (suite *SKSuite) TestSign() {
require := suite.Require()

msg := crypto.CRandBytes(1000)
sig, err := suite.sk.Sign(msg)
require.NoError(err)
sigCpy := make([]byte, len(sig))
copy(sigCpy, sig)
require.True(suite.pk.VerifySignature(msg, sigCpy))

// Mutate the signature
for i := range sig {
sigCpy[i] ^= byte(i + 1)
require.False(suite.pk.VerifySignature(msg, sigCpy))
}

// Mutate the message
msg[1] ^= byte(2)
require.False(suite.pk.VerifySignature(msg, sig))
}
83 changes: 83 additions & 0 deletions crypto/keys/internal/ecdsa/pubkey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package ecdsa

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/sha256"
"encoding/asn1"
"fmt"
"math/big"

tmcrypto "github.com/tendermint/tendermint/crypto"

"github.com/cosmos/cosmos-sdk/types/address"
"github.com/cosmos/cosmos-sdk/types/errors"
)

// signature holds the r and s values of an ECDSA signature.
type signature struct {
R, S *big.Int
}

type PubKey struct {
ecdsa.PublicKey

// cache
address tmcrypto.Address
}

// Address creates an ADR-28 address for ECDSA keys. protoName is a concrete proto structure id.
func (pk *PubKey) Address(protoName string) tmcrypto.Address {
if pk.address == nil {
pk.address = address.Hash(protoName, pk.Bytes())
}
return pk.address
}

// Bytes returns the byte representation of the public key using a compressed form
// specified in section 4.3.6 of ANSI X9.62 with first byte being the curve type.
func (pk *PubKey) Bytes() []byte {
if pk == nil {
return nil
}
return elliptic.MarshalCompressed(pk.Curve, pk.X, pk.Y)
}

// VerifySignature checks if sig is a valid ECDSA signature for msg.
func (pk *PubKey) VerifySignature(msg []byte, sig []byte) bool {
s := new(signature)
if _, err := asn1.Unmarshal(sig, s); err != nil || s == nil {
return false
}

h := sha256.Sum256(msg)
return ecdsa.Verify(&pk.PublicKey, h[:], s.R, s.S)
}

// String returns a string representation of the public key based on the curveName.
func (pk *PubKey) String(curveName string) string {
return fmt.Sprintf("%s{%X}", curveName, pk.Bytes())
}

// **** Proto Marshaler ****

// MarshalTo implements proto.Marshaler interface.
func (pk *PubKey) MarshalTo(dAtA []byte) (int, error) {
bz := pk.Bytes()
copy(dAtA, bz)
return len(bz), nil
}

// Unmarshal implements proto.Marshaler interface.
func (pk *PubKey) Unmarshal(bz []byte, curve elliptic.Curve, expectedSize int) error {
if len(bz) != expectedSize {
return errors.Wrapf(errors.ErrInvalidPubKey, "wrong ECDSA PK bytes, expecting %d bytes, got %d", expectedSize, len(bz))
}
cpk := ecdsa.PublicKey{Curve: curve}
cpk.X, cpk.Y = elliptic.UnmarshalCompressed(curve, bz)
if cpk.X == nil || cpk.Y == nil {
return errors.Wrapf(errors.ErrInvalidPubKey, "wrong ECDSA PK bytes, unknown curve type: %d", bz[0])
}
pk.PublicKey = cpk
return nil
}
69 changes: 69 additions & 0 deletions crypto/keys/internal/ecdsa/pubkey_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package ecdsa

import (
"crypto/elliptic"
"encoding/hex"
"testing"

"github.com/stretchr/testify/suite"
)

var secp256r1 = elliptic.P256()

func GenSecp256r1() (PrivKey, error) {
return GenPrivKey(secp256r1)
}

func TestPKSuite(t *testing.T) {
suite.Run(t, new(PKSuite))
}

type CommonSuite struct {
suite.Suite
pk PubKey
sk PrivKey
}

func (suite *CommonSuite) SetupSuite() {
sk, err := GenSecp256r1()
suite.Require().NoError(err)
suite.sk = sk
suite.pk = sk.PubKey()
}

type PKSuite struct{ CommonSuite }

func (suite *PKSuite) TestString() {
assert := suite.Assert()
require := suite.Require()

prefix := "abc"
pkStr := suite.pk.String(prefix)
assert.Equal(prefix+"{", pkStr[:len(prefix)+1])
assert.EqualValues('}', pkStr[len(pkStr)-1])

bz, err := hex.DecodeString(pkStr[len(prefix)+1 : len(pkStr)-1])
require.NoError(err)
assert.EqualValues(suite.pk.Bytes(), bz)
}

func (suite *PKSuite) TestBytes() {
require := suite.Require()
var pk *PubKey
require.Nil(pk.Bytes())
}

func (suite *PKSuite) TestMarshal() {
require := suite.Require()
const size = 33 // secp256r1 size

var buffer = make([]byte, size)
n, err := suite.pk.MarshalTo(buffer)
require.NoError(err)
require.Equal(size, n)

var pk = new(PubKey)
err = pk.Unmarshal(buffer, secp256r1, size)
require.NoError(err)
require.True(pk.PublicKey.Equal(&suite.pk.PublicKey))
}
35 changes: 35 additions & 0 deletions crypto/keys/secp256r1/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Package secp256r1 implements Cosmos-SDK compatible ECDSA public and private key. The keys
// can be protobuf serialized and packed in Any.
package secp256r1

import (
"crypto/elliptic"
"fmt"

codectypes "github.com/cosmos/cosmos-sdk/codec/types"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
)

const (
// fieldSize is the curve domain size.
fieldSize = 32
pubKeySize = fieldSize + 1

name = "secp256r1"
)

var secp256r1 elliptic.Curve

func init() {
secp256r1 = elliptic.P256()
// pubKeySize is ceil of field bit size + 1 for the sign
expected := (secp256r1.Params().BitSize + 7) / 8
if expected != fieldSize {
panic(fmt.Sprintf("Wrong secp256r1 curve fieldSize=%d, expecting=%d", fieldSize, expected))
}
}

// RegisterInterfaces adds secp256r1 PubKey to pubkey registry
func RegisterInterfaces(registry codectypes.InterfaceRegistry) {
registry.RegisterImplementations((*cryptotypes.PubKey)(nil), &PubKey{})
}
Loading

0 comments on commit 78a7172

Please sign in to comment.