Skip to content

Commit

Permalink
Merge pull request coinbase#278 from LanfordCai/feature/secp256r1
Browse files Browse the repository at this point in the history
feat(keys): support secp256r1
  • Loading branch information
itstehkman authored Dec 25, 2020
2 parents 23200e5 + bc8d565 commit 11a73ee
Show file tree
Hide file tree
Showing 4 changed files with 338 additions and 0 deletions.
6 changes: 6 additions & 0 deletions keys/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,17 @@ var (
ErrPrivKeyUndecodable = errors.New("could not decode privkey")
ErrPrivKeyLengthInvalid = errors.New("invalid privkey length")
ErrPrivKeyZero = errors.New("privkey cannot be 0")
ErrPubKeyNotOnCurve = errors.New("pubkey is not on the curve")

ErrKeyGenSecp256k1Failed = errors.New(
"keygen: error generating key pair for secp256k1 curve type",
)
ErrKeyGenEdwards25519Failed = errors.New(
"keygen: error generating key pair for edwards25519 curve type",
)
ErrKeyGenSecp256r1Failed = errors.New(
"keygen: error generating key pair for secp256r1 curve type",
)
ErrCurveTypeNotSupported = errors.New("not a supported CurveType")

ErrSignUnsupportedPayloadSignatureType = errors.New(
Expand All @@ -58,7 +62,9 @@ func Err(err error) bool {
ErrPrivKeyUndecodable,
ErrPrivKeyLengthInvalid,
ErrPrivKeyZero,
ErrPubKeyNotOnCurve,
ErrKeyGenSecp256k1Failed,
ErrKeyGenSecp256r1Failed,
ErrKeyGenEdwards25519Failed,
ErrCurveTypeNotSupported,
ErrSignUnsupportedPayloadSignatureType,
Expand Down
48 changes: 48 additions & 0 deletions keys/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@
package keys

import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"encoding/hex"
"fmt"
"math/big"

"github.com/btcsuite/btcd/btcec"

Expand Down Expand Up @@ -88,6 +92,32 @@ func ImportPrivateKey(privKeyHex string, curve types.CurveType) (*KeyPair, error
PublicKey: pubKey,
PrivateKey: rawPrivKey.Seed(),
}
case types.Secp256r1:
crv := elliptic.P256()
x, y := crv.ScalarBaseMult(privKey)

// IsOnCurve will return false for the point at infinity (0, 0)
// See:
// https://github.com/golang/go/blob/3298300ddf45a0792b4d8ea5e05f0fbceec4c9f9/src/crypto/elliptic/elliptic.go#L24
if !crv.IsOnCurve(x, y) {
return nil, ErrPubKeyNotOnCurve
}

rawPubKey := ecdsa.PublicKey{X: x, Y: y, Curve: crv}
rawPrivKey := ecdsa.PrivateKey{
PublicKey: rawPubKey,
D: new(big.Int).SetBytes(privKey),
}

pubKey := &types.PublicKey{
Bytes: elliptic.Marshal(crv, rawPubKey.X, rawPubKey.Y),
CurveType: curve,
}

keyPair = &KeyPair{
PublicKey: pubKey,
PrivateKey: rawPrivKey.D.Bytes(),
}
default:
return nil, fmt.Errorf("%w: %s", ErrCurveTypeNotSupported, curve)
}
Expand Down Expand Up @@ -137,6 +167,22 @@ func GenerateKeypair(curve types.CurveType) (*KeyPair, error) {
PublicKey: pubKey,
PrivateKey: rawPrivKey.Seed(),
}
case types.Secp256r1:
crv := elliptic.P256()
rawPrivKey, err := ecdsa.GenerateKey(crv, rand.Reader)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrKeyGenSecp256r1Failed, err)
}
rawPubKey := rawPrivKey.PublicKey
pubKey := &types.PublicKey{
Bytes: elliptic.Marshal(crv, rawPubKey.X, rawPubKey.Y),
CurveType: curve,
}

keyPair = &KeyPair{
PublicKey: pubKey,
PrivateKey: rawPrivKey.D.Bytes(),
}
default:
return nil, fmt.Errorf("%w: %s", ErrCurveTypeNotSupported, curve)
}
Expand Down Expand Up @@ -171,6 +217,8 @@ func (k *KeyPair) Signer() (Signer, error) {
return &SignerSecp256k1{k}, nil
case types.Edwards25519:
return &SignerEdwards25519{k}, nil
case types.Secp256r1:
return &SignerSecp256r1{k}, nil
default:
return nil, fmt.Errorf("%w: %s", ErrCurveTypeNotSupported, k.PublicKey.CurveType)
}
Expand Down
150 changes: 150 additions & 0 deletions keys/signer_secp256r1.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License

package keys

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

"github.com/coinbase/rosetta-sdk-go/asserter"
"github.com/coinbase/rosetta-sdk-go/types"
)

// SignerSecp256r1 is initialized from a keypair
type SignerSecp256r1 struct {
KeyPair *KeyPair
}

// The Ecdsa signature is the couple (R, S), both R and S are 32 bytes
const (
EcdsaRLen = 32
EcdsaSLen = 32
EcdsaMsgLen = 32
)

// Verify interface compliance at compile time
var _ Signer = (*SignerSecp256r1)(nil)

// PublicKey returns the PublicKey of the signer
func (s *SignerSecp256r1) PublicKey() *types.PublicKey {
return s.KeyPair.PublicKey
}

// Sign arbitrary payloads using a KeyPair with specific sigType.
// Currently, we only support sigType types.Ecdsa for secp256r1 and the signature format is R || S.
func (s *SignerSecp256r1) Sign(
payload *types.SigningPayload,
sigType types.SignatureType,
) (*types.Signature, error) {
if err := s.KeyPair.IsValid(); err != nil {
return nil, err
}

if !(payload.SignatureType == sigType || payload.SignatureType == "") {
return nil, fmt.Errorf(
"%w: %v",
ErrSignUnsupportedPayloadSignatureType,
payload.SignatureType,
)
}

if sigType != types.Ecdsa {
return nil, fmt.Errorf(
"%w: expected %v but got %v",
ErrSignUnsupportedSignatureType,
types.Ecdsa,
sigType,
)
}

crv := elliptic.P256()
x, y := crv.ScalarBaseMult(s.KeyPair.PrivateKey)

// IsOnCurve will return false for the point at infinity (0, 0)
// See:
// https://github.com/golang/go/blob/3298300ddf45a0792b4d8ea5e05f0fbceec4c9f9/src/crypto/elliptic/elliptic.go#L24
if !crv.IsOnCurve(x, y) {
return nil, ErrPubKeyNotOnCurve
}

pubKey := ecdsa.PublicKey{X: x, Y: y, Curve: crv}
privKey := ecdsa.PrivateKey{
PublicKey: pubKey,
D: new(big.Int).SetBytes(s.KeyPair.PrivateKey),
}

sigR, sigS, err := ecdsa.Sign(rand.Reader, &privKey, payload.Bytes)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrSignFailed, err.Error())
}
sig := sigR.Bytes()
sig = append(sig, sigS.Bytes()...)

return &types.Signature{
SigningPayload: payload,
PublicKey: s.KeyPair.PublicKey,
SignatureType: payload.SignatureType,
Bytes: sig,
}, nil
}

// Verify verifies a Signature, by checking the validity of a Signature,
// the SigningPayload, and the PublicKey of the Signature.
func (s *SignerSecp256r1) Verify(signature *types.Signature) error {
if signature.SignatureType != types.Ecdsa {
return fmt.Errorf(
"%w: expected %v but got %v",
ErrVerifyUnsupportedSignatureType,
types.Ecdsa,
signature.SignatureType,
)
}

if err := asserter.Signatures([]*types.Signature{signature}); err != nil {
return err
}

message := signature.SigningPayload.Bytes

if len(message) != EcdsaMsgLen {
return ErrVerifyFailed
}

sig := signature.Bytes

crv := elliptic.P256()
x, y := elliptic.Unmarshal(elliptic.P256(), signature.PublicKey.Bytes)

// IsOnCurve will return false for the point at infinity (0, 0)
// See:
// https://github.com/golang/go/blob/3298300ddf45a0792b4d8ea5e05f0fbceec4c9f9/src/crypto/elliptic/elliptic.go#L24
if !crv.IsOnCurve(x, y) {
return ErrPubKeyNotOnCurve
}
publicKey := ecdsa.PublicKey{X: x, Y: y, Curve: elliptic.P256()}

sigR := new(big.Int).SetBytes(sig[:EcdsaRLen])
sigS := new(big.Int).SetBytes(sig[EcdsaRLen:])

verify := ecdsa.Verify(&publicKey, message, sigR, sigS)

if !verify {
return ErrVerifyFailed
}
return nil
}
134 changes: 134 additions & 0 deletions keys/signer_secp256r1_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License

package keys

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/coinbase/rosetta-sdk-go/types"
)

var signerSecp256r1 Signer

func init() {
keypair, _ := GenerateKeypair(types.Secp256r1)
signerSecp256r1, _ = keypair.Signer()
}

func TestSignSecp256r1(t *testing.T) {
type payloadTest struct {
payload *types.SigningPayload
sigType types.SignatureType
sigLen int
err bool
errMsg error
}

var payloadTests = []payloadTest{
{mockPayload(hash("hello123"), types.Ecdsa), types.Ecdsa, 64, false, nil},
{
mockPayload(hash("hello1234"), types.EcdsaRecovery),
types.EcdsaRecovery,
65,
true,
ErrSignUnsupportedSignatureType,
},
{
mockPayload(hash("hello123"), types.Ed25519),
types.Ed25519,
64,
true,
ErrSignUnsupportedSignatureType,
},
{
mockPayload(hash("hello1234"), types.Schnorr1),
types.Schnorr1,
64, true,
ErrSignUnsupportedSignatureType,
},
}

for _, test := range payloadTests {
signature, err := signerSecp256r1.Sign(test.payload, test.sigType)

if !test.err {
assert.NoError(t, err)
assert.Equal(t, len(signature.Bytes), test.sigLen)
assert.Equal(t, signerSecp256r1.PublicKey(), signature.PublicKey)
} else {
assert.Contains(t, err.Error(), test.errMsg.Error())
}
}
}

func TestVerifySecp256r1(t *testing.T) {
type signatureTest struct {
signature *types.Signature
errMsg error
}

payloadEcdsa := &types.SigningPayload{
AccountIdentifier: &types.AccountIdentifier{Address: "test"},
Bytes: hash("hello"),
SignatureType: types.Ecdsa,
}
testSignatureEcdsa, err := signerSecp256r1.Sign(payloadEcdsa, types.Ecdsa)
assert.NoError(t, err)

simpleBytes := make([]byte, 33)
copy(simpleBytes, "hello")

var signatureTests = []signatureTest{
{mockSecpSignature(
types.Ecdsa,
signerSecp256r1.PublicKey(),
[]byte("hello"),
testSignatureEcdsa.Bytes), ErrVerifyFailed},
{mockSecpSignature(
types.Ed25519,
signerSecp256r1.PublicKey(),
hash("hello"),
simpleBytes), ErrVerifyUnsupportedSignatureType},
{mockSecpSignature(
types.Ecdsa,
signerSecp256r1.PublicKey(),
hash("hello"),
simpleBytes), ErrVerifyFailed},
{mockSecpSignature(
types.EcdsaRecovery,
signerSecp256r1.PublicKey(),
hash("hello"),
simpleBytes), ErrVerifyUnsupportedSignatureType},
{mockSecpSignature(
types.Schnorr1,
signerSecp256r1.PublicKey(),
hash("hello"),
simpleBytes), ErrVerifyUnsupportedSignatureType},
}

for _, test := range signatureTests {
err := signerSecp256r1.Verify(test.signature)
assert.Contains(t, err.Error(), test.errMsg.Error())
}

goodEcdsaSignature := mockSecpSignature(
types.Ecdsa,
signerSecp256r1.PublicKey(),
hash("hello"),
testSignatureEcdsa.Bytes)
assert.Equal(t, nil, signerSecp256r1.Verify(goodEcdsaSignature))
}

0 comments on commit 11a73ee

Please sign in to comment.