-
Notifications
You must be signed in to change notification settings - Fork 0
/
ledger.go
264 lines (243 loc) · 7.44 KB
/
ledger.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
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
// Copyright (C) 2022, Ava Labs, Inc. All rights reserved.
// Copyright (C) 2022, Coinflect, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package ledger
import (
"bytes"
"errors"
"fmt"
"strings"
"github.com/coinflect/coinflectchain/ids"
"github.com/coinflect/coinflectchain/utils/hashing"
ledger_go "github.com/zondax/ledger-go"
)
var _ Ledger = &ledger{}
// Ledger interface for the ledger wrapper
type Ledger interface {
Version() (version string, commit string, name string, err error)
Address(displayHRP string, addressIndex uint32) (ids.ShortID, error)
Addresses(addressIndices []uint32) ([]ids.ShortID, error)
SignHash(hash []byte, addressIndices []uint32) ([][]byte, error)
Disconnect() error
}
const (
CLA = 0x80
INSVersion = 0x00
INSPromptPublicKey = 0x02
INSPromptExtPublicKey = 0x03
INSSignHash = 0x04
)
var (
ErrLedgerNotConnected = errors.New("ledger is not connected")
ErrCoinflectAppNotExecuting = errors.New("ledger is not executing coinflect app")
ErrLedgerIsBlocked = errors.New("ledger is blocked")
ErrRejectedSignature = errors.New("hash sign operation rejected by ledger user")
ErrRejectedKeyProvide = errors.New("public key provide operation rejected by ledger user")
)
// NOTE: The current path prefix assumes we are using account 0 and don't have
// any change addresses
var pathPrefix = []uint32{44, 9000, 0, 0}
// ledger is a wrapper around the low-level Ledger Device interface that
// provides Coinflect-specific access.
type ledger struct {
device ledger_go.LedgerDevice
pk []byte
chainCode []byte
}
// New attempts to connect to a Ledger on the device over HID.
func New() (Ledger, error) {
admin := ledger_go.NewLedgerAdmin()
// connect to the first (0-index) HID USB device that satisfies the ledger-go library criteria
device, err := admin.Connect(0)
if err != nil {
return nil, mapLedgerConnectionErrors(err)
}
return &ledger{
device: device,
}, nil
}
func (l *ledger) collectSignatures(addressIndices []uint32) ([][]byte, error) {
results := make([][]byte, len(addressIndices))
for i := 0; i < len(addressIndices); i++ {
suffix := []uint32{addressIndices[i]}
p1 := 0x01
if i == len(addressIndices)-1 {
p1 = 0x81
}
data, err := bip32bytes(suffix, 0)
if err != nil {
return nil, err
}
msgSig := []byte{
CLA,
INSSignHash,
byte(p1),
0x0,
}
msgSig = append(msgSig, byte(len(data)))
msgSig = append(msgSig, data...)
sig, err := l.device.Exchange(msgSig)
if err != nil {
err = mapLedgerConnectionErrors(err)
if strings.Contains(err.Error(), "[APDU_CODE_CONDITIONS_NOT_SATISFIED] Conditions of use not satisfied") {
err = ErrRejectedSignature
}
return nil, err
}
results[i] = sig
}
return results, nil
}
// Disconnect attempts to disconnect from a previously connected Ledger.
func (l *ledger) Disconnect() error {
return l.device.Close()
}
// Version returns information about the Coinflect Ledger app. If a different
// app is open, this will return an error.
func (l *ledger) Version() (version string, commit string, name string, err error) {
msgVersion := []byte{
CLA,
INSVersion,
0x0,
0x0,
0x0,
}
rawVersion, err := l.device.Exchange(msgVersion)
if err != nil {
err = mapLedgerConnectionErrors(err)
return
}
version = fmt.Sprintf("%d.%d.%d", rawVersion[0], rawVersion[1], rawVersion[2])
rem := bytes.Split(rawVersion[3:], []byte{0x0})
commit = fmt.Sprintf("%x", rem[0])
name = string(rem[1])
return
}
// Address returns an Coinflect address as ids.ShortID, ledger ask confirmation showing
// addresss formatted with [displayHRP] (note [displayHRP] length is restricted to 4)
//
// On the P/X-Chain, accounts are derived on the path m/44'/9000'/0'/0/n
// (where n is the address index).
func (l *ledger) Address(displayHRP string, addressIndex uint32) (ids.ShortID, error) {
if len(displayHRP) != 4 {
return ids.ShortEmpty, fmt.Errorf("expected displayHRP len of 4, got %d", len(displayHRP))
}
msgPK := []byte{
CLA,
INSPromptPublicKey,
0x4,
0x0,
}
pathBytes, err := bip32bytes(append(pathPrefix, addressIndex), 3)
if err != nil {
return ids.ShortEmpty, err
}
data := append([]byte(displayHRP), pathBytes...)
msgPK = append(msgPK, byte(len(data)))
msgPK = append(msgPK, data...)
rawAddress, err := l.device.Exchange(msgPK)
if err != nil {
err = mapLedgerConnectionErrors(err)
if strings.Contains(err.Error(), "[APDU_CODE_CONDITIONS_NOT_SATISFIED] Conditions of use not satisfied") {
err = ErrRejectedKeyProvide
}
return ids.ShortEmpty, err
}
return ids.ToShortID(rawAddress)
}
func (l *ledger) getExtendedPublicKey() ([]byte, []byte, error) {
msgEPK := []byte{
CLA,
INSPromptExtPublicKey,
0x0,
0x0,
}
pathBytes, err := bip32bytes(pathPrefix, 3)
if err != nil {
return nil, nil, err
}
msgEPK = append(msgEPK, byte(len(pathBytes)))
msgEPK = append(msgEPK, pathBytes...)
response, err := l.device.Exchange(msgEPK)
if err != nil {
err = mapLedgerConnectionErrors(err)
if strings.Contains(err.Error(), "[APDU_CODE_CONDITIONS_NOT_SATISFIED] Conditions of use not satisfied") {
err = ErrRejectedKeyProvide
}
return nil, nil, err
}
pkLen := response[0]
chainCodeOffset := 2 + pkLen
chainCodeLength := response[1+pkLen]
return response[1 : 1+pkLen], response[chainCodeOffset : chainCodeOffset+chainCodeLength], nil
}
// Addresses returns the ledger addresses associated to the given [addressIndices], as []ids.ShortID
//
// On the P/X-Chain, accounts are derived on the path m/44'/9000'/0'/0/n
// (where n is the address index).
func (l *ledger) Addresses(addressIndices []uint32) ([]ids.ShortID, error) {
if len(l.pk) == 0 {
pk, chainCode, err := l.getExtendedPublicKey()
if err != nil {
return nil, err
}
l.pk = pk
l.chainCode = chainCode
}
addrs := make([]ids.ShortID, len(addressIndices))
for i, addrIndex := range addressIndices {
k, err := NewChild(l.pk, l.chainCode, addrIndex)
if err != nil {
return nil, err
}
shortAddr, err := ids.ToShortID(hashing.PubkeyBytesToAddress(k))
if err != nil {
return nil, err
}
addrs[i] = shortAddr
}
return addrs, nil
}
// SignHash attempts to sign the [hash] with the provided path [addresses].
// [addressIndices] are appened to the [pathPrefix] (m/44'/9000'/0'/0).
func (l *ledger) SignHash(hash []byte, addressIndices []uint32) ([][]byte, error) {
msgHash := []byte{
CLA,
INSSignHash,
0x0,
0x0,
}
pathBytes, err := bip32bytes(pathPrefix, 3)
if err != nil {
return nil, err
}
data := []byte{byte(len(addressIndices))}
data = append(data, hash...)
data = append(data, pathBytes...)
msgHash = append(msgHash, byte(len(data)))
msgHash = append(msgHash, data...)
resp, err := l.device.Exchange(msgHash)
if err != nil {
err = mapLedgerConnectionErrors(err)
if strings.Contains(err.Error(), "[APDU_CODE_CONDITIONS_NOT_SATISFIED] Conditions of use not satisfied") {
err = ErrRejectedSignature
}
return nil, err
}
if !bytes.Equal(resp, hash) {
return nil, fmt.Errorf("returned hash %x does not match requested %x", resp, hash)
}
return l.collectSignatures(addressIndices)
}
func mapLedgerConnectionErrors(err error) error {
if strings.Contains(err.Error(), "LedgerHID device") && strings.Contains(err.Error(), "not found") {
return ErrLedgerNotConnected
}
if strings.Contains(err.Error(), "Error code: 6e01") {
return ErrCoinflectAppNotExecuting
}
if strings.Contains(err.Error(), "Error code: 6b0c") {
return ErrLedgerIsBlocked
}
return err
}