forked from cosmos/cosmos-sdk
/
ledger_secp256k1.go
215 lines (177 loc) · 6.18 KB
/
ledger_secp256k1.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
package crypto
import (
"fmt"
"os"
"github.com/btcsuite/btcd/btcec"
"github.com/cosmos/cosmos-sdk/crypto/keys/hd"
"github.com/cosmos/cosmos-sdk/types"
"github.com/pkg/errors"
tmbtcec "github.com/tendermint/btcd/btcec"
tmcrypto "github.com/tendermint/tendermint/crypto"
tmsecp256k1 "github.com/tendermint/tendermint/crypto/secp256k1"
)
var (
// discoverLedger defines a function to be invoked at runtime for discovering
// a connected Ledger device.
discoverLedger discoverLedgerFn
)
type (
// discoverLedgerFn defines a Ledger discovery function that returns a
// connected device or an error upon failure. Its allows a method to avoid CGO
// dependencies when Ledger support is potentially not enabled.
discoverLedgerFn func() (LedgerSECP256K1, error)
// LedgerSECP256K1 reflects an interface a Ledger API must implement for
// the SECP256K1 scheme.
LedgerSECP256K1 interface {
Close() error
GetPublicKeySECP256K1([]uint32) ([]byte, error)
SignSECP256K1([]uint32, []byte) ([]byte, error)
ShowAddressSECP256K1([]uint32, string) error
}
// PrivKeyLedgerSecp256k1 implements PrivKey, calling the ledger nano we
// cache the PubKey from the first call to use it later.
PrivKeyLedgerSecp256k1 struct {
// CachedPubKey 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 tmcrypto.PubKey
Path hd.BIP44Params
}
)
// NewPrivKeyLedgerSecp256k1 will generate a new key and store the public key
// for later use.
func NewPrivKeyLedgerSecp256k1(path hd.BIP44Params) (tmcrypto.PrivKey, error) {
device, err := getLedgerDevice()
if err != nil {
return nil, err
}
defer warnIfErrors(device.Close)
pubKey, err := getPubKey(device, path)
if err != nil {
return nil, err
}
return PrivKeyLedgerSecp256k1{pubKey, path}, nil
}
// LedgerShowAddress triggers a ledger device to show the corresponding address.
func LedgerShowAddress(path hd.BIP44Params, expectedPubKey tmcrypto.PubKey) error {
device, err := getLedgerDevice()
if err != nil {
return err
}
defer warnIfErrors(device.Close)
pubKey, err := getPubKey(device, path)
if err != nil {
return err
}
if pubKey != expectedPubKey {
return fmt.Errorf("pubkey does not match, Check this is the same device")
}
return device.ShowAddressSECP256K1(path.DerivationPath(), types.Bech32PrefixAccAddr)
}
// PubKey returns the cached public key.
func (pkl PrivKeyLedgerSecp256k1) PubKey() tmcrypto.PubKey {
return pkl.CachedPubKey
}
// Sign returns a secp256k1 signature for the corresponding message
func (pkl PrivKeyLedgerSecp256k1) Sign(message []byte) ([]byte, error) {
device, err := getLedgerDevice()
if err != nil {
return nil, err
}
defer warnIfErrors(device.Close)
return sign(device, pkl, message)
}
// ValidateKey allows us to verify the sanity of a public key after loading it
// from disk.
func (pkl PrivKeyLedgerSecp256k1) ValidateKey() error {
device, err := getLedgerDevice()
if err != nil {
return err
}
defer warnIfErrors(device.Close)
return validateKey(device, pkl)
}
// AssertIsPrivKeyInner implements the PrivKey interface. It performs a no-op.
func (pkl *PrivKeyLedgerSecp256k1) AssertIsPrivKeyInner() {}
// Bytes implements the PrivKey interface. It stores the cached public key so
// we can verify the same key when we reconnect to a ledger.
func (pkl PrivKeyLedgerSecp256k1) Bytes() []byte {
return cdc.MustMarshalBinaryBare(pkl)
}
// Equals implements the PrivKey interface. It makes sure two private keys
// refer to the same public key.
func (pkl PrivKeyLedgerSecp256k1) Equals(other tmcrypto.PrivKey) bool {
if otherKey, ok := other.(PrivKeyLedgerSecp256k1); ok {
return pkl.CachedPubKey.Equals(otherKey.CachedPubKey)
}
return false
}
// warnIfErrors wraps a function and writes a warning to stderr. This is required
// to avoid ignoring errors when defer is used. Using defer may result in linter warnings.
func warnIfErrors(f func() error) {
if err := f(); err != nil {
_, _ = fmt.Fprint(os.Stderr, "received error when closing ledger connection", err)
}
}
func convertDERtoBER(signatureDER []byte) ([]byte, error) {
sigDER, err := btcec.ParseDERSignature(signatureDER[:], btcec.S256())
if err != nil {
return nil, err
}
sigBER := tmbtcec.Signature{R: sigDER.R, S: sigDER.S}
return sigBER.Serialize(), nil
}
func getLedgerDevice() (LedgerSECP256K1, error) {
if discoverLedger == nil {
return nil, errors.New("no Ledger discovery function defined")
}
device, err := discoverLedger()
if err != nil {
return nil, errors.Wrap(err, "ledger nano S")
}
return device, nil
}
func validateKey(device LedgerSECP256K1, pkl PrivKeyLedgerSecp256k1) error {
pub, err := getPubKey(device, pkl.Path)
if err != nil {
return err
}
// verify this matches cached address
if !pub.Equals(pkl.CachedPubKey) {
return fmt.Errorf("cached key does not match retrieved key")
}
return nil
}
// 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 private key is held in memory
// for a while before use.
func sign(device LedgerSECP256K1, pkl PrivKeyLedgerSecp256k1, msg []byte) ([]byte, error) {
err := validateKey(device, pkl)
if err != nil {
return nil, err
}
sig, err := device.SignSECP256K1(pkl.Path.DerivationPath(), msg)
if err != nil {
return nil, err
}
return convertDERtoBER(sig)
}
// getPubKey reads the pubkey 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 getPubKey(device LedgerSECP256K1, path hd.BIP44Params) (tmcrypto.PubKey, error) {
publicKey, err := device.GetPublicKeySECP256K1(path.DerivationPath())
if err != nil {
return nil, fmt.Errorf("please open Cosmos app on the Ledger device - error: %v", err)
}
// re-serialize in the 33-byte compressed format
cmp, err := btcec.ParsePubKey(publicKey[:], btcec.S256())
if err != nil {
return nil, fmt.Errorf("error parsing public key: %v", err)
}
var compressedPublicKey tmsecp256k1.PubKeySecp256k1
copy(compressedPublicKey[:], cmp.SerializeCompressed())
return compressedPublicKey, nil
}