-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Expand file tree
/
Copy pathledger_secp256k1.go
More file actions
409 lines (346 loc) · 13.7 KB
/
Copy pathledger_secp256k1.go
File metadata and controls
409 lines (346 loc) · 13.7 KB
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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
package ledger
import (
"errors"
"fmt"
"math/big"
"os"
secp "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
"github.com/cosmos/cosmos-sdk/crypto/hd"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
"github.com/cosmos/cosmos-sdk/crypto/types"
)
// options stores the Ledger Options that can be used to customize Ledger usage
var options Options
// AppName defines the Ledger app used for signing. Cosmos SDK uses the Cosmos app
const AppName = "Cosmos"
type (
// discoverLedgerFn defines a Ledger discovery function that returns a
// connected device or an error upon failure. It allows a method to avoid CGO
// dependencies when Ledger support is potentially not enabled.
discoverLedgerFn func() (SECP256K1, error)
// createPubkeyFn supports returning different public key types that implement
// types.PubKey
createPubkeyFn func([]byte) types.PubKey
// SECP256K1 reflects an interface a Ledger API must implement for SECP256K1
SECP256K1 interface {
Close() error
// GetPublicKeySECP256K1 returns an uncompressed pubkey
GetPublicKeySECP256K1([]uint32) ([]byte, error)
// GetAddressPubKeySECP256K1 returns a compressed pubkey and bech32 address (requires user confirmation)
GetAddressPubKeySECP256K1([]uint32, string) ([]byte, string, error)
// SignSECP256K1 signs a message (requires user confirmation)
// The last byte denotes the SIGN_MODE to be used by Ledger: 0 for
// LEGACY_AMINO_JSON, 1 for TEXTUAL. It corresponds to the P2 value
// in https://github.com/cosmos/ledger-cosmos/blob/main/docs/APDUSPEC.md
SignSECP256K1([]uint32, []byte, byte) ([]byte, error)
}
// Options hosts customization options to account for differences in Ledger
// signing and usage across chains.
Options struct {
discoverLedger discoverLedgerFn
createPubkey createPubkeyFn
appName string
skipDERConversion bool
}
// 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 types.PubKey
Path hd.BIP44Params
}
)
// Initialize the default options values for the Cosmos Ledger
func initOptionsDefault() {
options.createPubkey = func(key []byte) types.PubKey {
return &secp256k1.PubKey{Key: key}
}
options.appName = AppName
options.skipDERConversion = false
}
// SetDiscoverLedger sets the discoverLedger function to use a different Ledger derivation
func SetDiscoverLedger(fn discoverLedgerFn) {
options.discoverLedger = fn
}
// SetCreatePubkey sets the createPubkey function to use a different public key
func SetCreatePubkey(fn createPubkeyFn) {
options.createPubkey = fn
}
// SetAppName sets the Ledger app name to use a different app name
func SetAppName(appName string) {
options.appName = appName
}
// SetSkipDERConversion sets the DER Conversion requirement to true (false by default)
func SetSkipDERConversion() {
options.skipDERConversion = true
}
// SetDERConversion configures whether DER signature conversion should be enabled.
// When enabled (true), signatures returned from the Ledger device are converted
// from DER format to BER format, which is the standard behavior for Cosmos SDK chains.
// When disabled (false), raw signatures are used without conversion, which is
// typically required for Ethereum/EVM-compatible chains.
//
// Parameters:
// - enabled: true to enable DER conversion (Cosmos chains), false to disable (Ethereum chains)
//
// Example usage for different coin types in a key management CLI:
//
// switch coinType {
// case 60:
// // Ethereum/EVM chains - disable DER conversion for raw signatures
// cosmosLedger.SetDiscoverLedger(func() (cosmosLedger.SECP256K1, error) {
// return evmkeyring.LedgerDerivation()
// })
// cosmosLedger.SetCreatePubkey(func(key []byte) cryptotypes.PubKey {
// return evmkeyring.CreatePubkey(key)
// })
// cosmosLedger.SetAppName(evmkeyring.AppName)
// cosmosLedger.SetDERConversion(false) // Disable DER conversion for Ethereum
// case 118:
// // Cosmos SDK chains - enable DER conversion for signature compatibility
// cosmosLedger.SetDiscoverLedger(func() (cosmosLedger.SECP256K1, error) {
// device, err := ledger.FindLedgerCosmosUserApp()
// if err != nil {
// return nil, err
// }
// return device, nil
// })
// cosmosLedger.SetCreatePubkey(func(key []byte) cryptotypes.PubKey {
// return &secp256k1.PubKey{Key: key}
// })
// cosmosLedger.SetAppName(cosmosLedger.AppName)
// cosmosLedger.SetDERConversion(true) // Enable DER conversion for Cosmos
// default:
// return fmt.Errorf(
// "unsupported coin type %d for Ledger. Supported coin types: 60 (Ethereum app), 118 (Cosmos app)", coinType,
// )
// }
func SetDERConversion(enabled bool) {
options.skipDERConversion = !enabled
}
// NewPrivKeySecp256k1Unsafe will generate a new key and store the public key for later use.
//
// This function is marked as unsafe as it will retrieve a pubkey without user verification.
// It can only be used to verify a pubkey but never to create new accounts/keys. In that case,
// please refer to NewPrivKeySecp256k1
func NewPrivKeySecp256k1Unsafe(path hd.BIP44Params) (types.LedgerPrivKeyAminoJSON, error) {
device, err := getDevice()
if err != nil {
return nil, err
}
defer warnIfErrors(device.Close)
pubKey, err := getPubKeyUnsafe(device, path)
if err != nil {
return nil, err
}
return PrivKeyLedgerSecp256k1{pubKey, path}, nil
}
// NewPrivKeySecp256k1 will generate a new key and store the public key for later use.
// The request will require user confirmation and will show account and index in the device
func NewPrivKeySecp256k1(path hd.BIP44Params, hrp string) (types.LedgerPrivKey, string, error) {
device, err := getDevice()
if err != nil {
return nil, "", fmt.Errorf("failed to retrieve device: %w", err)
}
defer warnIfErrors(device.Close)
pubKey, addr, err := getPubKeyAddrSafe(device, path, hrp)
if err != nil {
return nil, "", fmt.Errorf("failed to recover pubkey: %w", err)
}
return PrivKeyLedgerSecp256k1{pubKey, path}, addr, nil
}
// PubKey returns the cached public key.
func (pkl PrivKeyLedgerSecp256k1) PubKey() types.PubKey {
return pkl.CachedPubKey
}
// Sign returns a secp256k1 signature for the corresponding message.
func (pkl PrivKeyLedgerSecp256k1) Sign(message []byte) ([]byte, error) {
device, err := getDevice()
if err != nil {
return nil, err
}
defer warnIfErrors(device.Close)
return sign(device, pkl, message, 1)
}
// SignLedgerAminoJSON returns a secp256k1 signature for the corresponding message using
// SIGN_MODE_LEGACY_AMINO_JSON.
func (pkl PrivKeyLedgerSecp256k1) SignLedgerAminoJSON(message []byte) ([]byte, error) {
device, err := getDevice()
if err != nil {
return nil, err
}
defer warnIfErrors(device.Close)
return sign(device, pkl, message, 0)
}
// ShowAddress triggers a ledger device to show the corresponding address.
func ShowAddress(path hd.BIP44Params, expectedPubKey types.PubKey, accountAddressPrefix string) error {
device, err := getDevice()
if err != nil {
return err
}
defer warnIfErrors(device.Close)
pubKey, err := getPubKeyUnsafe(device, path)
if err != nil {
return err
}
if !pubKey.Equals(expectedPubKey) {
return fmt.Errorf("the key's pubkey does not match with the one retrieved from Ledger. Check that the HD path and device are the correct ones")
}
pubKey2, _, err := getPubKeyAddrSafe(device, path, accountAddressPrefix)
if err != nil {
return err
}
if !pubKey2.Equals(expectedPubKey) {
return fmt.Errorf("the key's pubkey does not match with the one retrieved from Ledger. Check that the HD path and device are the correct ones")
}
return nil
}
// ValidateKey allows us to verify the sanity of a public key after loading it
// from disk.
func (pkl PrivKeyLedgerSecp256k1) ValidateKey() error {
device, err := getDevice()
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.MustMarshal(pkl)
}
// Equals implements the PrivKey interface. It makes sure two private keys
// refer to the same public key.
func (pkl PrivKeyLedgerSecp256k1) Equals(other types.LedgerPrivKey) bool {
if otherKey, ok := other.(PrivKeyLedgerSecp256k1); ok {
return pkl.CachedPubKey.Equals(otherKey.CachedPubKey)
}
return false
}
func (pkl PrivKeyLedgerSecp256k1) Type() string { return "PrivKeyLedgerSecp256k1" }
// 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 := ecdsa.ParseDERSignature(signatureDER)
if err != nil {
return nil, err
}
sigStr := sigDER.Serialize()
// The format of a DER encoded signature is as follows:
// 0x30 <total length> 0x02 <length of R> <R> 0x02 <length of S> <S>
r, s := new(big.Int), new(big.Int)
r.SetBytes(sigStr[4 : 4+sigStr[3]])
s.SetBytes(sigStr[4+sigStr[3]+2:])
sModNScalar := new(secp.ModNScalar)
sModNScalar.SetByteSlice(s.Bytes())
// based on https://github.com/tendermint/btcd/blob/ec996c5/btcec/signature.go#L33-L50
if sModNScalar.IsOverHalfOrder() {
s = new(big.Int).Sub(secp.S256().N, s) //nolint:staticcheck // TODO: migrate off deprecated elliptic.Curve (SA1019)
}
sigBytes := make([]byte, 64)
// 0 pad the byte arrays from the left if they aren't big enough.
copy(sigBytes[32-len(r.Bytes()):32], r.Bytes())
copy(sigBytes[64-len(s.Bytes()):64], s.Bytes())
return sigBytes, nil
}
func getDevice() (SECP256K1, error) {
if options.discoverLedger == nil {
return nil, errors.New("no Ledger discovery function defined")
}
device, err := options.discoverLedger()
if err != nil {
return nil, fmt.Errorf("ledger nano S: %w", err)
}
return device, nil
}
func validateKey(device SECP256K1, pkl PrivKeyLedgerSecp256k1) error {
pub, err := getPubKeyUnsafe(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.
//
// Last byte P2 is 0 for LEGACY_AMINO_JSON, and 1 for TEXTUAL.
func sign(device SECP256K1, pkl PrivKeyLedgerSecp256k1, msg []byte, p2 byte) ([]byte, error) {
err := validateKey(device, pkl)
if err != nil {
return nil, err
}
sig, err := device.SignSECP256K1(pkl.Path.DerivationPath(), msg, p2)
if err != nil {
return nil, err
}
if options.skipDERConversion {
return sig, nil
}
return convertDERtoBER(sig)
}
// getPubKeyUnsafe reads the pubkey from a ledger device
//
// This function is marked as unsafe as it will retrieve a pubkey without user verification
// It can only be used to verify a pubkey but never to create new accounts/keys. In that case,
// please refer to getPubKeyAddrSafe
//
// 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 getPubKeyUnsafe(device SECP256K1, path hd.BIP44Params) (types.PubKey, error) {
publicKey, err := device.GetPublicKeySECP256K1(path.DerivationPath())
if err != nil {
return nil, fmt.Errorf("please open the %v app on the Ledger device - error: %w", options.appName, err)
}
// re-serialize in the 33-byte compressed format
cmp, err := secp.ParsePubKey(publicKey)
if err != nil {
return nil, fmt.Errorf("error parsing public key: %w", err)
}
compressedPublicKey := make([]byte, secp256k1.PubKeySize)
copy(compressedPublicKey, cmp.SerializeCompressed())
return options.createPubkey(compressedPublicKey), nil
}
// getPubKeyAddrSafe reads the pubkey and the address from a ledger device.
// This function is marked as Safe as it will require user confirmation and
// account and index will be shown in the device.
//
// 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 getPubKeyAddrSafe(device SECP256K1, path hd.BIP44Params, hrp string) (types.PubKey, string, error) {
publicKey, addr, err := device.GetAddressPubKeySECP256K1(path.DerivationPath(), hrp)
if err != nil {
// Check special case if user is trying to use an index > 100
if path.AddressIndex > 100 {
return nil, "", fmt.Errorf("%w: cannot derive paths where index > 100: %s "+
"This is a security measure to avoid very hard to find derivation paths introduced by a possible attacker. "+
"You can disable this by setting expert mode in your ledger device. Do this at your own risk", err, path)
}
return nil, "", fmt.Errorf("%w: address rejected for path %s", err, path)
}
// re-serialize in the 33-byte compressed format
cmp, err := secp.ParsePubKey(publicKey)
if err != nil {
return nil, "", fmt.Errorf("error parsing public key: %w", err)
}
compressedPublicKey := make([]byte, secp256k1.PubKeySize)
copy(compressedPublicKey, cmp.SerializeCompressed())
return options.createPubkey(compressedPublicKey), addr, nil
}