diff --git a/Gopkg.lock b/Gopkg.lock index f52af559..f51e2b22 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,17 +1,23 @@ # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. +[[projects]] + branch = "master" + name = "github.com/brejski/hid" + packages = ["."] + revision = "06112dcfcc50a7e0e4fd06e17f9791e788fdaafc" + [[projects]] branch = "master" name = "github.com/btcsuite/btcd" packages = ["btcec"] - revision = "2be2f12b358dc57d70b8f501b00be450192efbc3" + revision = "86fed781132ac890ee03e906e4ecd5d6fa180c64" [[projects]] branch = "master" name = "github.com/btcsuite/btcutil" packages = ["base58"] - revision = "501929d3d046174c3d39f0ea54ece471aa17238c" + revision = "d4cc87b860166d00d6b5b9e0d3b3d71d6088d4d4" [[projects]] name = "github.com/davecgh/go-spew" @@ -55,7 +61,7 @@ branch = "master" name = "github.com/golang/snappy" packages = ["."] - revision = "553a641470496b2327abcac10b36396bd98e45c9" + revision = "2e65f85255dbc3072edf28d6b5b8efc472979f5a" [[projects]] branch = "master" @@ -113,7 +119,7 @@ "leveldb/table", "leveldb/util" ] - revision = "714f901b98fdb3aa954b4193d8cbd64a28d80cad" + revision = "5d6fca44a948d2be89a9702de7717f0168403d3d" [[projects]] branch = "master" @@ -128,8 +134,8 @@ [[projects]] name = "github.com/tendermint/go-amino" packages = ["."] - revision = "42246108ff925a457fb709475070a03dfd3e2b5c" - version = "0.9.6" + revision = "3c22a7a539411f89a96738fcfa14c1027e24e5ec" + version = "0.9.10" [[projects]] name = "github.com/tendermint/tmlibs" @@ -138,8 +144,8 @@ "db", "log" ] - revision = "2e24b64fc121dcdf1cabceab8dc2f7257675483c" - version = "v0.8.1" + revision = "d970af87248a4e162590300dbb74e102183a417d" + version = "v0.8.3" [[projects]] branch = "master" @@ -147,6 +153,11 @@ packages = ["."] revision = "8e7a99b3e716f36d3b080a9a70f9eb45abe4edcc" +[[projects]] + name = "github.com/zondax/ledger-goclient" + packages = ["."] + revision = "3e2146609cdb97894c064d59e9d00accd8c2b1dd" + [[projects]] branch = "master" name = "golang.org/x/crypto" @@ -161,11 +172,11 @@ "ripemd160", "salsa20/salsa" ] - revision = "b2aa35443fbc700ab74c586ae79b81c171851023" + revision = "5ba7f63082460102a45837dbd1827e10f9479ac0" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "f9ccfa2cadfcbfb43bf729b871a0ad2f8d4f4acb118cd859e6faf9b24842b840" + inputs-digest = "365c3bca75ced49eb0ebcdc5c98fd47b534850684fcc94c16d1bc6a671116395" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 4ccb8c07..10931a1a 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -57,6 +57,10 @@ name = "github.com/tyler-smith/go-bip39" branch = "master" +[[constraint]] + name = "github.com/zondax/ledger-goclient" + revision = "3e2146609cdb97894c064d59e9d00accd8c2b1dd" + [prune] go-tests = true unused-packages = true diff --git a/amino.go b/amino.go index 89636895..2af76543 100644 --- a/amino.go +++ b/amino.go @@ -27,6 +27,8 @@ func RegisterAmino(cdc *amino.Codec) { "tendermint/PrivKeyEd25519", nil) cdc.RegisterConcrete(PrivKeySecp256k1{}, "tendermint/PrivKeySecp256k1", nil) + cdc.RegisterConcrete(PrivKeyLedgerSecp256k1{}, + "tendermint/PrivKeyLedgerSecp256k1", nil) cdc.RegisterInterface((*Signature)(nil), nil) cdc.RegisterConcrete(SignatureEd25519{}, diff --git a/ledger_common.go b/ledger_common.go new file mode 100644 index 00000000..39f15464 --- /dev/null +++ b/ledger_common.go @@ -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 +} diff --git a/ledger_secp256k1.go b/ledger_secp256k1.go new file mode 100644 index 00000000..f485bb41 --- /dev/null +++ b/ledger_secp256k1.go @@ -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 +} diff --git a/ledger_test.go b/ledger_test.go new file mode 100644 index 00000000..dda3d3d3 --- /dev/null +++ b/ledger_test.go @@ -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) +} diff --git a/signature.go b/signature.go index 4f55420c..1ffb45ea 100644 --- a/signature.go +++ b/signature.go @@ -80,3 +80,9 @@ func (sig SignatureSecp256k1) Equals(other Signature) bool { return false } } + +func SignatureSecp256k1FromBytes(data []byte) Signature { + sig := make(SignatureSecp256k1, len(data)) + copy(sig[:], data) + return sig +}