forked from cosmos/cosmos-sdk
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request cosmos#85 from tendermint/cwgoes/ledger-integration
Ledger integration
- Loading branch information
Showing
7 changed files
with
268 additions
and
10 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package crypto | ||
|
||
import ( | ||
ledger "github.com/zondax/ledger-goclient" | ||
) | ||
|
||
var device *ledger.Ledger | ||
|
||
// Ledger derivation path | ||
type DerivationPath = []uint32 | ||
|
||
// getLedger gets a copy of the device, and caches it | ||
func getLedger() (*ledger.Ledger, error) { | ||
var err error | ||
if device == nil { | ||
device, err = ledger.FindLedger() | ||
} | ||
return device, err | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
package crypto | ||
|
||
import ( | ||
"fmt" | ||
|
||
secp256k1 "github.com/btcsuite/btcd/btcec" | ||
ledger "github.com/zondax/ledger-goclient" | ||
) | ||
|
||
func pubkeyLedgerSecp256k1(device *ledger.Ledger, path DerivationPath) (pub PubKey, err error) { | ||
key, err := device.GetPublicKeySECP256K1(path) | ||
if err != nil { | ||
return nil, fmt.Errorf("error fetching public key: %v", err) | ||
} | ||
var p PubKeySecp256k1 | ||
// Reserialize in the 33-byte compressed format | ||
cmp, err := secp256k1.ParsePubKey(key[:], secp256k1.S256()) | ||
copy(p[:], cmp.SerializeCompressed()) | ||
pub = p | ||
return | ||
} | ||
|
||
func signLedgerSecp256k1(device *ledger.Ledger, path DerivationPath, msg []byte) (sig Signature, err error) { | ||
bsig, err := device.SignSECP256K1(path, msg) | ||
if err != nil { | ||
return sig, err | ||
} | ||
sig = SignatureSecp256k1FromBytes(bsig) | ||
return | ||
} | ||
|
||
// PrivKeyLedgerSecp256k1 implements PrivKey, calling the ledger nano | ||
// we cache the PubKey from the first call to use it later | ||
type PrivKeyLedgerSecp256k1 struct { | ||
// PubKey should be private, but we want to encode it via go-amino | ||
// so we can view the address later, even without having the ledger | ||
// attached | ||
CachedPubKey PubKey | ||
Path DerivationPath | ||
} | ||
|
||
// NewPrivKeyLedgerSecp256k1 will generate a new key and store the | ||
// public key for later use. | ||
func NewPrivKeyLedgerSecp256k1(path DerivationPath) (PrivKey, error) { | ||
var pk PrivKeyLedgerSecp256k1 | ||
pk.Path = path | ||
// getPubKey will cache the pubkey for later use, | ||
// this allows us to return an error early if the ledger | ||
// is not plugged in | ||
_, err := pk.getPubKey() | ||
return &pk, err | ||
} | ||
|
||
// ValidateKey allows us to verify the sanity of a key | ||
// after loading it from disk | ||
func (pk PrivKeyLedgerSecp256k1) ValidateKey() error { | ||
// getPubKey will return an error if the ledger is not | ||
// properly set up... | ||
pub, err := pk.forceGetPubKey() | ||
if err != nil { | ||
return err | ||
} | ||
// verify this matches cached address | ||
if !pub.Equals(pk.CachedPubKey) { | ||
return fmt.Errorf("cached key does not match retrieved key") | ||
} | ||
return nil | ||
} | ||
|
||
// AssertIsPrivKeyInner fulfils PrivKey Interface | ||
func (pk *PrivKeyLedgerSecp256k1) AssertIsPrivKeyInner() {} | ||
|
||
// Bytes fulfils PrivKey Interface - but it stores the cached pubkey so we can verify | ||
// the same key when we reconnect to a ledger | ||
func (pk PrivKeyLedgerSecp256k1) Bytes() []byte { | ||
bin, err := cdc.MarshalBinaryBare(pk) | ||
if err != nil { | ||
panic(err) | ||
} | ||
return bin | ||
} | ||
|
||
// Sign calls the ledger and stores the PubKey for future use | ||
// | ||
// Communication is checked on NewPrivKeyLedger and PrivKeyFromBytes, | ||
// returning an error, so this should only trigger if the privkey is held | ||
// in memory for a while before use. | ||
func (pk PrivKeyLedgerSecp256k1) Sign(msg []byte) Signature { | ||
// oh, I wish there was better error handling | ||
dev, err := getLedger() | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
sig, err := signLedgerSecp256k1(dev, pk.Path, msg) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
pub, err := pubkeyLedgerSecp256k1(dev, pk.Path) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
// if we have no pubkey yet, store it for future queries | ||
if pk.CachedPubKey == nil { | ||
pk.CachedPubKey = pub | ||
} else if !pk.CachedPubKey.Equals(pub) { | ||
panic("stored key does not match signing key") | ||
} | ||
return sig | ||
} | ||
|
||
// PubKey returns the stored PubKey | ||
func (pk PrivKeyLedgerSecp256k1) PubKey() PubKey { | ||
key, err := pk.getPubKey() | ||
if err != nil { | ||
panic(err) | ||
} | ||
return key | ||
} | ||
|
||
// getPubKey reads the pubkey from cache or from the ledger itself | ||
// since this involves IO, it may return an error, which is not exposed | ||
// in the PubKey interface, so this function allows better error handling | ||
func (pk PrivKeyLedgerSecp256k1) getPubKey() (key PubKey, err error) { | ||
// if we have no pubkey, set it | ||
if pk.CachedPubKey == nil { | ||
pk.CachedPubKey, err = pk.forceGetPubKey() | ||
} | ||
return pk.CachedPubKey, err | ||
} | ||
|
||
// forceGetPubKey is like getPubKey but ignores any cached key | ||
// and ensures we get it from the ledger itself. | ||
func (pk PrivKeyLedgerSecp256k1) forceGetPubKey() (key PubKey, err error) { | ||
dev, err := getLedger() | ||
if err != nil { | ||
return key, fmt.Errorf("cannot connect to Ledger device - error: %v", err) | ||
} | ||
key, err = pubkeyLedgerSecp256k1(dev, pk.Path) | ||
if err != nil { | ||
return key, fmt.Errorf("please open Cosmos app on the Ledger device - error: %v", err) | ||
} | ||
return key, err | ||
} | ||
|
||
// Equals fulfils PrivKey Interface - makes sure both keys refer to the | ||
// same | ||
func (pk PrivKeyLedgerSecp256k1) Equals(other PrivKey) bool { | ||
if ledger, ok := other.(*PrivKeyLedgerSecp256k1); ok { | ||
return pk.CachedPubKey.Equals(ledger.CachedPubKey) | ||
} | ||
return false | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
package crypto | ||
|
||
import ( | ||
"os" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestRealLedgerSecp256k1(t *testing.T) { | ||
|
||
if os.Getenv("WITH_LEDGER") == "" { | ||
t.Skip("Set WITH_LEDGER to run code on real ledger") | ||
} | ||
msg := []byte("kuhehfeohg") | ||
|
||
path := DerivationPath{44, 60, 0, 0, 0} | ||
|
||
priv, err := NewPrivKeyLedgerSecp256k1(path) | ||
require.Nil(t, err, "%+v", err) | ||
pub := priv.PubKey() | ||
sig := priv.Sign(msg) | ||
|
||
valid := pub.VerifyBytes(msg, sig) | ||
assert.True(t, valid) | ||
|
||
// now, let's serialize the key and make sure it still works | ||
bs := priv.Bytes() | ||
priv2, err := PrivKeyFromBytes(bs) | ||
require.Nil(t, err, "%+v", err) | ||
|
||
// make sure we get the same pubkey when we load from disk | ||
pub2 := priv2.PubKey() | ||
require.Equal(t, pub, pub2) | ||
|
||
// signing with the loaded key should match the original pubkey | ||
sig = priv2.Sign(msg) | ||
valid = pub.VerifyBytes(msg, sig) | ||
assert.True(t, valid) | ||
|
||
// make sure pubkeys serialize properly as well | ||
bs = pub.Bytes() | ||
bpub, err := PubKeyFromBytes(bs) | ||
require.NoError(t, err) | ||
assert.Equal(t, pub, bpub) | ||
} | ||
|
||
// TestRealLedgerErrorHandling calls. These tests assume | ||
// the ledger is not plugged in.... | ||
func TestRealLedgerErrorHandling(t *testing.T) { | ||
if os.Getenv("WITH_LEDGER") != "" { | ||
t.Skip("Skipping on WITH_LEDGER as it tests unplugged cases") | ||
} | ||
|
||
// first, try to generate a key, must return an error | ||
// (no panic) | ||
path := DerivationPath{44, 60, 0, 0, 0} | ||
_, err := NewPrivKeyLedgerSecp256k1(path) | ||
require.Error(t, err) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters