github.com/Finschia/finschia-sdk@v0.48.1/crypto/ledger/ledger_secp256k1.go (about) 1 package ledger 2 3 import ( 4 "fmt" 5 "os" 6 7 "github.com/btcsuite/btcd/btcec" 8 "github.com/pkg/errors" 9 10 tmbtcec "github.com/tendermint/btcd/btcec" 11 12 "github.com/Finschia/finschia-sdk/crypto/hd" 13 "github.com/Finschia/finschia-sdk/crypto/keys/secp256k1" 14 "github.com/Finschia/finschia-sdk/crypto/types" 15 ) 16 17 // discoverLedger defines a function to be invoked at runtime for discovering 18 // a connected Ledger device. 19 var discoverLedger discoverLedgerFn 20 21 type ( 22 // discoverLedgerFn defines a Ledger discovery function that returns a 23 // connected device or an error upon failure. Its allows a method to avoid CGO 24 // dependencies when Ledger support is potentially not enabled. 25 discoverLedgerFn func() (SECP256K1, error) 26 27 // SECP256K1 reflects an interface a Ledger API must implement for SECP256K1 28 SECP256K1 interface { 29 Close() error 30 // Returns an uncompressed pubkey 31 GetPublicKeySECP256K1([]uint32) ([]byte, error) 32 // Returns a compressed pubkey and bech32 address (requires user confirmation) 33 GetAddressPubKeySECP256K1([]uint32, string) ([]byte, string, error) 34 // Signs a message (requires user confirmation) 35 // The last byte denotes the SIGN_MODE to be used by Ledger: 0 for 36 // LEGACY_AMINO_JSON, 1 for TEXTUAL. It corresponds to the P2 value 37 // in https://github.com/cosmos/ledger-cosmos/blob/main/docs/APDUSPEC.md 38 SignSECP256K1([]uint32, []byte, byte) ([]byte, error) 39 } 40 41 // PrivKeyLedgerSecp256k1 implements PrivKey, calling the ledger nano we 42 // cache the PubKey from the first call to use it later. 43 PrivKeyLedgerSecp256k1 struct { 44 // CachedPubKey should be private, but we want to encode it via 45 // go-amino so we can view the address later, even without having the 46 // ledger attached. 47 CachedPubKey types.PubKey 48 Path hd.BIP44Params 49 } 50 ) 51 52 // NewPrivKeySecp256k1Unsafe will generate a new key and store the public key for later use. 53 // 54 // This function is marked as unsafe as it will retrieve a pubkey without user verification. 55 // It can only be used to verify a pubkey but never to create new accounts/keys. In that case, 56 // please refer to NewPrivKeySecp256k1 57 func NewPrivKeySecp256k1Unsafe(path hd.BIP44Params) (types.LedgerPrivKey, error) { 58 device, err := getDevice() 59 if err != nil { 60 return nil, err 61 } 62 defer warnIfErrors(device.Close) 63 64 pubKey, err := getPubKeyUnsafe(device, path) 65 if err != nil { 66 return nil, err 67 } 68 69 return PrivKeyLedgerSecp256k1{pubKey, path}, nil 70 } 71 72 // NewPrivKeySecp256k1 will generate a new key and store the public key for later use. 73 // The request will require user confirmation and will show account and index in the device 74 func NewPrivKeySecp256k1(path hd.BIP44Params, hrp string) (types.LedgerPrivKey, string, error) { 75 device, err := getDevice() 76 if err != nil { 77 return nil, "", fmt.Errorf("failed to retrieve device: %w", err) 78 } 79 defer warnIfErrors(device.Close) 80 81 pubKey, addr, err := getPubKeyAddrSafe(device, path, hrp) 82 if err != nil { 83 return nil, "", fmt.Errorf("failed to recover pubkey: %w", err) 84 } 85 86 return PrivKeyLedgerSecp256k1{pubKey, path}, addr, nil 87 } 88 89 // PubKey returns the cached public key. 90 func (pkl PrivKeyLedgerSecp256k1) PubKey() types.PubKey { 91 return pkl.CachedPubKey 92 } 93 94 // Sign returns a secp256k1 signature for the corresponding message 95 func (pkl PrivKeyLedgerSecp256k1) Sign(message []byte) ([]byte, error) { 96 device, err := getDevice() 97 if err != nil { 98 return nil, err 99 } 100 defer warnIfErrors(device.Close) 101 102 return sign(device, pkl, message) 103 } 104 105 // ShowAddress triggers a ledger device to show the corresponding address. 106 func ShowAddress(path hd.BIP44Params, expectedPubKey types.PubKey, 107 accountAddressPrefix string, 108 ) error { 109 device, err := getDevice() 110 if err != nil { 111 return err 112 } 113 defer warnIfErrors(device.Close) 114 115 pubKey, err := getPubKeyUnsafe(device, path) 116 if err != nil { 117 return err 118 } 119 120 if !pubKey.Equals(expectedPubKey) { 121 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") 122 } 123 124 pubKey2, _, err := getPubKeyAddrSafe(device, path, accountAddressPrefix) 125 if err != nil { 126 return err 127 } 128 129 if !pubKey2.Equals(expectedPubKey) { 130 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") 131 } 132 133 return nil 134 } 135 136 // ValidateKey allows us to verify the sanity of a public key after loading it 137 // from disk. 138 func (pkl PrivKeyLedgerSecp256k1) ValidateKey() error { 139 device, err := getDevice() 140 if err != nil { 141 return err 142 } 143 defer warnIfErrors(device.Close) 144 145 return validateKey(device, pkl) 146 } 147 148 // AssertIsPrivKeyInner implements the PrivKey interface. It performs a no-op. 149 func (pkl *PrivKeyLedgerSecp256k1) AssertIsPrivKeyInner() {} 150 151 // Bytes implements the PrivKey interface. It stores the cached public key so 152 // we can verify the same key when we reconnect to a ledger. 153 func (pkl PrivKeyLedgerSecp256k1) Bytes() []byte { 154 return cdc.MustMarshal(pkl) 155 } 156 157 // Equals implements the PrivKey interface. It makes sure two private keys 158 // refer to the same public key. 159 func (pkl PrivKeyLedgerSecp256k1) Equals(other types.LedgerPrivKey) bool { 160 if otherKey, ok := other.(PrivKeyLedgerSecp256k1); ok { 161 return pkl.CachedPubKey.Equals(otherKey.CachedPubKey) 162 } 163 return false 164 } 165 166 func (pkl PrivKeyLedgerSecp256k1) Type() string { return "PrivKeyLedgerSecp256k1" } 167 168 // warnIfErrors wraps a function and writes a warning to stderr. This is required 169 // to avoid ignoring errors when defer is used. Using defer may result in linter warnings. 170 func warnIfErrors(f func() error) { 171 if err := f(); err != nil { 172 _, _ = fmt.Fprint(os.Stderr, "received error when closing ledger connection", err) 173 } 174 } 175 176 func convertDERtoBER(signatureDER []byte) ([]byte, error) { 177 sigDER, err := btcec.ParseDERSignature(signatureDER, btcec.S256()) 178 if err != nil { 179 return nil, err 180 } 181 sigBER := tmbtcec.Signature{R: sigDER.R, S: sigDER.S} 182 return sigBER.Serialize(), nil 183 } 184 185 func getDevice() (SECP256K1, error) { 186 if discoverLedger == nil { 187 return nil, errors.New("no Ledger discovery function defined") 188 } 189 190 device, err := discoverLedger() 191 if err != nil { 192 return nil, errors.Wrap(err, "ledger nano S") 193 } 194 195 return device, nil 196 } 197 198 func validateKey(device SECP256K1, pkl PrivKeyLedgerSecp256k1) error { 199 pub, err := getPubKeyUnsafe(device, pkl.Path) 200 if err != nil { 201 return err 202 } 203 204 // verify this matches cached address 205 if !pub.Equals(pkl.CachedPubKey) { 206 return fmt.Errorf("cached key does not match retrieved key") 207 } 208 209 return nil 210 } 211 212 // Sign calls the ledger and stores the PubKey for future use. 213 // 214 // Communication is checked on NewPrivKeyLedger and PrivKeyFromBytes, returning 215 // an error, so this should only trigger if the private key is held in memory 216 // for a while before use. 217 func sign(device SECP256K1, pkl PrivKeyLedgerSecp256k1, msg []byte) ([]byte, error) { 218 err := validateKey(device, pkl) 219 if err != nil { 220 return nil, err 221 } 222 223 sig, err := device.SignSECP256K1(pkl.Path.DerivationPath(), msg, 0) 224 if err != nil { 225 return nil, err 226 } 227 228 return convertDERtoBER(sig) 229 } 230 231 // getPubKeyUnsafe reads the pubkey from a ledger device 232 // 233 // This function is marked as unsafe as it will retrieve a pubkey without user verification 234 // It can only be used to verify a pubkey but never to create new accounts/keys. In that case, 235 // please refer to getPubKeyAddrSafe 236 // 237 // since this involves IO, it may return an error, which is not exposed 238 // in the PubKey interface, so this function allows better error handling 239 func getPubKeyUnsafe(device SECP256K1, path hd.BIP44Params) (types.PubKey, error) { 240 publicKey, err := device.GetPublicKeySECP256K1(path.DerivationPath()) 241 if err != nil { 242 return nil, fmt.Errorf("please open Cosmos app on the Ledger device - error: %v", err) 243 } 244 245 // re-serialize in the 33-byte compressed format 246 cmp, err := btcec.ParsePubKey(publicKey, btcec.S256()) 247 if err != nil { 248 return nil, fmt.Errorf("error parsing public key: %v", err) 249 } 250 251 compressedPublicKey := make([]byte, secp256k1.PubKeySize) 252 copy(compressedPublicKey, cmp.SerializeCompressed()) 253 254 return &secp256k1.PubKey{Key: compressedPublicKey}, nil 255 } 256 257 // getPubKeyAddr reads the pubkey and the address from a ledger device. 258 // This function is marked as Safe as it will require user confirmation and 259 // account and index will be shown in the device. 260 // 261 // Since this involves IO, it may return an error, which is not exposed 262 // in the PubKey interface, so this function allows better error handling. 263 func getPubKeyAddrSafe(device SECP256K1, path hd.BIP44Params, hrp string) (types.PubKey, string, error) { 264 publicKey, addr, err := device.GetAddressPubKeySECP256K1(path.DerivationPath(), hrp) 265 if err != nil { 266 return nil, "", fmt.Errorf("%w: address rejected for path %s", err, path.String()) 267 } 268 269 // re-serialize in the 33-byte compressed format 270 cmp, err := btcec.ParsePubKey(publicKey, btcec.S256()) 271 if err != nil { 272 return nil, "", fmt.Errorf("error parsing public key: %v", err) 273 } 274 275 compressedPublicKey := make([]byte, secp256k1.PubKeySize) 276 copy(compressedPublicKey, cmp.SerializeCompressed()) 277 278 return &secp256k1.PubKey{Key: compressedPublicKey}, addr, nil 279 }