Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(keyring): add secp256r1 support to keyring #8862

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 43 additions & 32 deletions crypto/hd/algo.go
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
package hd

import (
bip39 "github.com/cosmos/go-bip39"
"crypto/elliptic"

"github.com/btcsuite/btcd/btcec"

"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256r1"
"github.com/cosmos/cosmos-sdk/crypto/types"
)

// PubKeyType defines an algorithm to derive key-pairs which can be used for cryptographic signing.
type PubKeyType string

// Signature scheme types
const (
// MultiType implies that a pubkey is a multisignature
MultiType = PubKeyType("multi")
// Secp256k1Type uses the Bitcoin secp256k1 ECDSA parameters.
MultiType = PubKeyType("multi")
Secp256k1Type = PubKeyType("secp256k1")
// Ed25519Type represents the Ed25519Type signature system.
// It is currently not supported for end-user keys (wallets/ledgers).
Secp256r1Type = PubKeyType(secp256r1.Name)

Ed25519Type = PubKeyType("ed25519")
// Sr25519Type represents the Sr25519Type signature system.
Sr25519Type = PubKeyType("sr25519")
)

// Secp256 algos use the Bip39 derivation mechanism
var (
// Secp256k1 uses the Bitcoin secp256k1 ECDSA parameters.
Secp256k1 = secp256k1Algo{}
Secp256k1 = ecdsaAlgo{btcec.S256(), Secp256k1Type, genSecp256k1}
Secp256r1 = ecdsaAlgo{elliptic.P256(), Secp256r1Type, genSecp256r1}
)

type DeriveFn func(mnemonic string, bip39Passphrase, hdPath string) ([]byte, error)
Expand All @@ -35,37 +37,46 @@ type WalletGenerator interface {
Generate(bz []byte) types.PrivKey
}

type secp256k1Algo struct {
type ecdsaAlgo struct {
curve elliptic.Curve
name PubKeyType
gen GenerateFn
}

func (s secp256k1Algo) Name() PubKeyType {
return Secp256k1Type
}
// Name returns signature scheme name
func (s ecdsaAlgo) Name() PubKeyType { return s.name }

// Derive derives and returns the secp256k1 private key for the given seed and HD path.
func (s secp256k1Algo) Derive() DeriveFn {
return func(mnemonic string, bip39Passphrase, hdPath string) ([]byte, error) {
seed, err := bip39.NewSeedWithErrorChecking(mnemonic, bip39Passphrase)
if err != nil {
return nil, err
}

masterPriv, ch := ComputeMastersFromSeed(seed)
if len(hdPath) == 0 {
return masterPriv[:], nil
}
derivedKey, err := DerivePrivateKeyForPath(masterPriv, ch, hdPath)

return derivedKey, err
func (s ecdsaAlgo) Derive() DeriveFn {
return s.derive
}

func (s ecdsaAlgo) derive(mnemonic, bip39Passphrase, hdPath string) ([]byte, error) {
m, ch, err := masterPrivKey(mnemonic, bip39Passphrase)
if err != nil {
return nil, err
}
if len(hdPath) == 0 {
return m[:], nil
}
return DeriveECDSAPrivKey(s.curve, m, ch, hdPath)
}

// Generate generates a secp256k1 private key from the given bytes.
func (s secp256k1Algo) Generate() GenerateFn {
return func(bz []byte) types.PrivKey {
var bzArr = make([]byte, secp256k1.PrivKeySize)
copy(bzArr, bz)
func (s ecdsaAlgo) Generate() GenerateFn {
return s.gen
}

func genSecp256k1(bz []byte) types.PrivKey {
var bzArr = make([]byte, secp256k1.PrivKeySize)
copy(bzArr, bz)
return &secp256k1.PrivKey{Key: bzArr}
}

return &secp256k1.PrivKey{Key: bzArr}
func genSecp256r1(bz []byte) types.PrivKey {
key, err := secp256r1.NewPrivKeyFromSecret(bz)
if err != nil {
panic(err)
}
return key
}
7 changes: 6 additions & 1 deletion crypto/hd/fundraiser_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package hd_test

import (
"crypto/elliptic"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"testing"

"github.com/btcsuite/btcd/btcec"
"github.com/stretchr/testify/require"

bip39 "github.com/cosmos/go-bip39"
Expand All @@ -26,6 +28,9 @@ type addrData struct {
Addr string
}

var secp256k1Curve = btcec.S256()
var secp256r1Curve = elliptic.P256()

func TestFullFundraiserPath(t *testing.T) {
require.Equal(t, "m/44'/118'/0'/0/0", hd.NewFundraiserParams(0, 118, 0).String())
}
Expand Down Expand Up @@ -63,7 +68,7 @@ func TestFundraiserCompatibility(t *testing.T) {
t.Logf("ROUND: %d MNEMONIC: %s", i, d.Mnemonic)

master, ch := hd.ComputeMastersFromSeed(seed)
priv, err := hd.DerivePrivateKeyForPath(master, ch, "m/44'/118'/0'/0/0")
priv, err := hd.DeriveECDSAPrivKey(secp256k1Curve, master, ch, "m/44'/118'/0'/0/0")
require.NoError(t, err)

privKey := &secp256k1.PrivKey{Key: priv}
Expand Down
37 changes: 21 additions & 16 deletions crypto/hd/hdpath.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package hd

import (
"crypto/elliptic"
"crypto/hmac"
"crypto/sha512"
"encoding/binary"
Expand All @@ -11,6 +12,7 @@ import (
"strings"

"github.com/btcsuite/btcd/btcec"
bip39 "github.com/cosmos/go-bip39"
)

// BIP44Params wraps BIP 44 params (5 level BIP 32 path).
Expand Down Expand Up @@ -167,6 +169,15 @@ func (p BIP44Params) String() string {
p.AddressIndex)
}

func masterPrivKey(mnemonic string, bip39Passphrase string) (secret [32]byte, chainCode [32]byte, err error) {
var seed []byte
if seed, err = bip39.NewSeedWithErrorChecking(mnemonic, bip39Passphrase); err != nil {
return
}
secret, chainCode = ComputeMastersFromSeed(seed)
return
}

// ComputeMastersFromSeed returns the master secret key's, and chain code.
func ComputeMastersFromSeed(seed []byte) (secret [32]byte, chainCode [32]byte) {
curveIdentifier := []byte("Bitcoin seed")
Expand All @@ -175,9 +186,9 @@ func ComputeMastersFromSeed(seed []byte) (secret [32]byte, chainCode [32]byte) {
return
}

// DerivePrivateKeyForPath derives the private key by following the BIP 32/44 path from privKeyBytes,
// DeriveECDSAPrivKey derives the private key by following the BIP 32/44 path from privKeyBytes,
// using the given chainCode.
func DerivePrivateKeyForPath(privKeyBytes, chainCode [32]byte, path string) ([]byte, error) {
func DeriveECDSAPrivKey(curve elliptic.Curve, privKeyBytes, chainCode [32]byte, path string) ([]byte, error) {
// First step is to trim the right end path separator lest we panic.
// See issue https://github.com/cosmos/cosmos-sdk/issues/8557
path = strings.TrimRightFunc(path, func(r rune) bool { return r == filepath.Separator })
Expand Down Expand Up @@ -210,7 +221,7 @@ func DerivePrivateKeyForPath(privKeyBytes, chainCode [32]byte, path string) ([]b
return []byte{}, fmt.Errorf("invalid BIP 32 path %s: %w", path, err)
}

data, chainCode = derivePrivateKey(data, chainCode, uint32(idx), harden)
data, chainCode = derivePrivateKey(curve, data, chainCode, uint32(idx), harden)
}

derivedKey := make([]byte, 32)
Expand All @@ -228,39 +239,33 @@ func DerivePrivateKeyForPath(privKeyBytes, chainCode [32]byte, path string) ([]b
// It returns the new private key and new chain code.
// For more information on hardened keys see:
// - https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
func derivePrivateKey(privKeyBytes [32]byte, chainCode [32]byte, index uint32, harden bool) ([32]byte, [32]byte) {
func derivePrivateKey(curve elliptic.Curve, privKeyBytes, chainCode [32]byte, index uint32, harden bool) ([32]byte, [32]byte) {
var data []byte

if harden {
index |= 0x80000000

data = append([]byte{byte(0)}, privKeyBytes[:]...)
data = append([]byte{0}, privKeyBytes[:]...)
} else {
// we use btcec instead of tendermint
// this can't return an error:
_, ecPub := btcec.PrivKeyFromBytes(btcec.S256(), privKeyBytes[:])
_, ecPub := btcec.PrivKeyFromBytes(curve, privKeyBytes[:])
pubkeyBytes := ecPub.SerializeCompressed()
data = pubkeyBytes

/* By using btcec, we can remove the dependency on tendermint/crypto/secp256k1
pubkey := secp256k1.PrivKeySecp256k1(privKeyBytes).PubKey()
public := pubkey.(secp256k1.PubKeySecp256k1)
data = public[:]
*/
}

data = append(data, uint32ToBytes(index)...)
data2, chainCode2 := i64(chainCode[:], data)
x := addScalars(privKeyBytes[:], data2[:])

x := addScalars(privKeyBytes[:], data2[:], curve.Params().N)
return x, chainCode2
}

// modular big endian addition
func addScalars(a []byte, b []byte) [32]byte {
func addScalars(a []byte, b []byte, order *big.Int) [32]byte {
aInt := new(big.Int).SetBytes(a)
bInt := new(big.Int).SetBytes(b)
sInt := new(big.Int).Add(aInt, bInt)
x := sInt.Mod(sInt, btcec.S256().N).Bytes()
x := sInt.Mod(sInt, order).Bytes()
x2 := [32]byte{}
copy(x2[32-len(x):], x)

Expand Down
39 changes: 23 additions & 16 deletions crypto/hd/hdpath_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,19 +173,26 @@ func TestDeriveHDPathRange(t *testing.T) {
for _, tt := range tests {
tt := tt
t.Run(tt.path, func(t *testing.T) {
require := require.New(t)
master, ch := hd.ComputeMastersFromSeed(seed)
_, err := hd.DerivePrivateKeyForPath(master, ch, tt.path)
_, err := hd.DeriveECDSAPrivKey(secp256k1Curve, master, ch, tt.path)
checkError(require, err, tt.wantErr)

if tt.wantErr == "" {
require.NoError(t, err, "unexpected error")
} else {
require.Error(t, err, "expected a report of an int overflow")
require.Contains(t, err.Error(), tt.wantErr)
}
_, err = hd.DeriveECDSAPrivKey(secp256r1Curve, master, ch, tt.path)
checkError(require, err, tt.wantErr)
})
}
}

func checkError(require *require.Assertions, err error, wantErr string) {
if wantErr == "" {
require.NoError(err, "unexpected error")
} else {
require.Error(err, "expected a report of an int overflow")
require.Contains(err.Error(), wantErr)
}
}

func ExampleStringifyPathParams() {
path := hd.NewParams(44, 0, 0, false, 0)
fmt.Println(path.String())
Expand All @@ -203,34 +210,34 @@ func ExampleSomeBIP32TestVecs() {
fmt.Println("keys from fundraiser test-vector (cosmos, bitcoin, ether)")
fmt.Println()
// cosmos
priv, err := hd.DerivePrivateKeyForPath(master, ch, types.FullFundraiserPath)
priv, err := hd.DeriveECDSAPrivKey(secp256k1Curve, master, ch, types.FullFundraiserPath)
if err != nil {
fmt.Println("INVALID")
} else {
fmt.Println(hex.EncodeToString(priv[:]))
}
// bitcoin
priv, err = hd.DerivePrivateKeyForPath(master, ch, "44'/0'/0'/0/0")
priv, err = hd.DeriveECDSAPrivKey(secp256k1Curve, master, ch, "44'/0'/0'/0/0")
if err != nil {
fmt.Println("INVALID")
} else {
fmt.Println(hex.EncodeToString(priv[:]))
}
// ether
priv, err = hd.DerivePrivateKeyForPath(master, ch, "44'/60'/0'/0/0")
priv, err = hd.DeriveECDSAPrivKey(secp256k1Curve, master, ch, "44'/60'/0'/0/0")
if err != nil {
fmt.Println("INVALID")
} else {
fmt.Println(hex.EncodeToString(priv[:]))
}
// INVALID
priv, err = hd.DerivePrivateKeyForPath(master, ch, "X/0'/0'/0/0")
priv, err = hd.DeriveECDSAPrivKey(secp256k1Curve, master, ch, "X/0'/0'/0/0")
if err != nil {
fmt.Println("INVALID")
} else {
fmt.Println(hex.EncodeToString(priv[:]))
}
priv, err = hd.DerivePrivateKeyForPath(master, ch, "-44/0'/0'/0/0")
priv, err = hd.DeriveECDSAPrivKey(secp256k1Curve, master, ch, "-44/0'/0'/0/0")
if err != nil {
fmt.Println("INVALID")
} else {
Expand All @@ -245,13 +252,13 @@ func ExampleSomeBIP32TestVecs() {
"advice process birth april short trust crater change bacon monkey medal garment " +
"gorilla ranch hour rival razor call lunar mention taste vacant woman sister")
master, ch = hd.ComputeMastersFromSeed(seed)
priv, _ = hd.DerivePrivateKeyForPath(master, ch, "44'/1'/1'/0/4")
priv, _ = hd.DeriveECDSAPrivKey(secp256k1Curve, master, ch, "44'/1'/1'/0/4")
fmt.Println(hex.EncodeToString(priv[:]))

seed = mnemonicToSeed("idea naive region square margin day captain habit " +
"gun second farm pact pulse someone armed")
master, ch = hd.ComputeMastersFromSeed(seed)
priv, _ = hd.DerivePrivateKeyForPath(master, ch, "44'/0'/0'/0/420")
priv, _ = hd.DeriveECDSAPrivKey(secp256k1Curve, master, ch, "44'/0'/0'/0/420")
fmt.Println(hex.EncodeToString(priv[:]))

fmt.Println()
Expand All @@ -261,7 +268,7 @@ func ExampleSomeBIP32TestVecs() {
// bip32 path: m/0/7
seed = mnemonicToSeed("monitor flock loyal sick object grunt duty ride develop assault harsh history")
master, ch = hd.ComputeMastersFromSeed(seed)
priv, _ = hd.DerivePrivateKeyForPath(master, ch, "0/7")
priv, _ = hd.DeriveECDSAPrivKey(secp256k1Curve, master, ch, "0/7")
fmt.Println(hex.EncodeToString(priv[:]))

// Output: keys from fundraiser test-vector (cosmos, bitcoin, ether)
Expand Down Expand Up @@ -300,7 +307,7 @@ func TestDerivePrivateKeyForPathDoNotCrash(t *testing.T) {
for _, path := range paths {
path := path
t.Run(path, func(t *testing.T) {
hd.DerivePrivateKeyForPath([32]byte{}, [32]byte{}, path)
hd.DeriveECDSAPrivKey(secp256k1Curve, [32]byte{}, [32]byte{}, path)
})
}
}
2 changes: 1 addition & 1 deletion crypto/keyring/keyring.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ type keystore struct {
func newKeystore(kr keyring.Keyring, opts ...Option) keystore {
// Default options for keybase
options := Options{
SupportedAlgos: SigningAlgoList{hd.Secp256k1},
SupportedAlgos: SigningAlgoList{hd.Secp256k1, hd.Secp256r1},
SupportedAlgosLedger: SigningAlgoList{hd.Secp256k1},
}

Expand Down
2 changes: 1 addition & 1 deletion crypto/keyring/signing_algorithms.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func NewSigningAlgoFromString(str string, algoList SigningAlgoList) (SignatureAl
return algo, nil
}
}
return nil, fmt.Errorf("provided algorithm %q is not supported", str)
return nil, fmt.Errorf("provided algorithm %q is not supported, supported algorithms: %v", str, algoList)
}

// SigningAlgoList is a slice of signature algorithms
Expand Down
Loading