github.com/cosmos/cosmos-sdk@v0.50.1/crypto/ledger/ledger_secp256k1.go (about) 1 package ledger 2 3 import ( 4 "errors" 5 "fmt" 6 "math/big" 7 "os" 8 9 secp "github.com/decred/dcrd/dcrec/secp256k1/v4" 10 "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" 11 12 "github.com/cosmos/cosmos-sdk/crypto/hd" 13 "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" 14 "github.com/cosmos/cosmos-sdk/crypto/types" 15 ) 16 17 // options stores the Ledger Options that can be used to customize Ledger usage 18 var options Options 19 20 type ( 21 // discoverLedgerFn defines a Ledger discovery function that returns a 22 // connected device or an error upon failure. Its allows a method to avoid CGO 23 // dependencies when Ledger support is potentially not enabled. 24 discoverLedgerFn func() (SECP256K1, error) 25 26 // createPubkeyFn supports returning different public key types that implement 27 // types.PubKey 28 createPubkeyFn func([]byte) types.PubKey 29 30 // SECP256K1 reflects an interface a Ledger API must implement for SECP256K1 31 SECP256K1 interface { 32 Close() error 33 // Returns an uncompressed pubkey 34 GetPublicKeySECP256K1([]uint32) ([]byte, error) 35 // Returns a compressed pubkey and bech32 address (requires user confirmation) 36 GetAddressPubKeySECP256K1([]uint32, string) ([]byte, string, error) 37 // Signs a message (requires user confirmation) 38 // The last byte denotes the SIGN_MODE to be used by Ledger: 0 for 39 // LEGACY_AMINO_JSON, 1 for TEXTUAL. It corresponds to the P2 value 40 // in https://github.com/cosmos/ledger-cosmos/blob/main/docs/APDUSPEC.md 41 SignSECP256K1([]uint32, []byte, byte) ([]byte, error) 42 } 43 44 // Options hosts customization options to account for differences in Ledger 45 // signing and usage across chains. 46 Options struct { 47 discoverLedger discoverLedgerFn 48 createPubkey createPubkeyFn 49 appName string 50 skipDERConversion bool 51 } 52 53 // PrivKeyLedgerSecp256k1 implements PrivKey, calling the ledger nano we 54 // cache the PubKey from the first call to use it later. 55 PrivKeyLedgerSecp256k1 struct { 56 // CachedPubKey should be private, but we want to encode it via 57 // go-amino so we can view the address later, even without having the 58 // ledger attached. 59 CachedPubKey types.PubKey 60 Path hd.BIP44Params 61 } 62 ) 63 64 // Initialize the default options values for the Cosmos Ledger 65 func initOptionsDefault() { 66 options.createPubkey = func(key []byte) types.PubKey { 67 return &secp256k1.PubKey{Key: key} 68 } 69 options.appName = "Cosmos" 70 options.skipDERConversion = false 71 } 72 73 // Set the discoverLedger function to use a different Ledger derivation 74 func SetDiscoverLedger(fn discoverLedgerFn) { 75 options.discoverLedger = fn 76 } 77 78 // Set the createPubkey function to use a different public key 79 func SetCreatePubkey(fn createPubkeyFn) { 80 options.createPubkey = fn 81 } 82 83 // Set the Ledger app name to use a different app name 84 func SetAppName(appName string) { 85 options.appName = appName 86 } 87 88 // Set the DER Conversion requirement to true (false by default) 89 func SetSkipDERConversion() { 90 options.skipDERConversion = true 91 } 92 93 // NewPrivKeySecp256k1Unsafe will generate a new key and store the public key for later use. 94 // 95 // This function is marked as unsafe as it will retrieve a pubkey without user verification. 96 // It can only be used to verify a pubkey but never to create new accounts/keys. In that case, 97 // please refer to NewPrivKeySecp256k1 98 func NewPrivKeySecp256k1Unsafe(path hd.BIP44Params) (types.LedgerPrivKeyAminoJSON, error) { 99 device, err := getDevice() 100 if err != nil { 101 return nil, err 102 } 103 defer warnIfErrors(device.Close) 104 105 pubKey, err := getPubKeyUnsafe(device, path) 106 if err != nil { 107 return nil, err 108 } 109 110 return PrivKeyLedgerSecp256k1{pubKey, path}, nil 111 } 112 113 // NewPrivKeySecp256k1 will generate a new key and store the public key for later use. 114 // The request will require user confirmation and will show account and index in the device 115 func NewPrivKeySecp256k1(path hd.BIP44Params, hrp string) (types.LedgerPrivKey, string, error) { 116 device, err := getDevice() 117 if err != nil { 118 return nil, "", fmt.Errorf("failed to retrieve device: %w", err) 119 } 120 defer warnIfErrors(device.Close) 121 122 pubKey, addr, err := getPubKeyAddrSafe(device, path, hrp) 123 if err != nil { 124 return nil, "", fmt.Errorf("failed to recover pubkey: %w", err) 125 } 126 127 return PrivKeyLedgerSecp256k1{pubKey, path}, addr, nil 128 } 129 130 // PubKey returns the cached public key. 131 func (pkl PrivKeyLedgerSecp256k1) PubKey() types.PubKey { 132 return pkl.CachedPubKey 133 } 134 135 // Sign returns a secp256k1 signature for the corresponding message using 136 // SIGN_MODE_TEXTUAL. 137 func (pkl PrivKeyLedgerSecp256k1) Sign(message []byte) ([]byte, error) { 138 device, err := getDevice() 139 if err != nil { 140 return nil, err 141 } 142 defer warnIfErrors(device.Close) 143 144 return sign(device, pkl, message, 1) 145 } 146 147 // SignLedgerAminoJSON returns a secp256k1 signature for the corresponding message using 148 // SIGN_MODE_LEGACY_AMINO_JSON. 149 func (pkl PrivKeyLedgerSecp256k1) SignLedgerAminoJSON(message []byte) ([]byte, error) { 150 device, err := getDevice() 151 if err != nil { 152 return nil, err 153 } 154 defer warnIfErrors(device.Close) 155 156 return sign(device, pkl, message, 0) 157 } 158 159 // ShowAddress triggers a ledger device to show the corresponding address. 160 func ShowAddress(path hd.BIP44Params, expectedPubKey types.PubKey, accountAddressPrefix string) error { 161 device, err := getDevice() 162 if err != nil { 163 return err 164 } 165 defer warnIfErrors(device.Close) 166 167 pubKey, err := getPubKeyUnsafe(device, path) 168 if err != nil { 169 return err 170 } 171 172 if !pubKey.Equals(expectedPubKey) { 173 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") 174 } 175 176 pubKey2, _, err := getPubKeyAddrSafe(device, path, accountAddressPrefix) 177 if err != nil { 178 return err 179 } 180 181 if !pubKey2.Equals(expectedPubKey) { 182 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") 183 } 184 185 return nil 186 } 187 188 // ValidateKey allows us to verify the sanity of a public key after loading it 189 // from disk. 190 func (pkl PrivKeyLedgerSecp256k1) ValidateKey() error { 191 device, err := getDevice() 192 if err != nil { 193 return err 194 } 195 defer warnIfErrors(device.Close) 196 197 return validateKey(device, pkl) 198 } 199 200 // AssertIsPrivKeyInner implements the PrivKey interface. It performs a no-op. 201 func (pkl *PrivKeyLedgerSecp256k1) AssertIsPrivKeyInner() {} 202 203 // Bytes implements the PrivKey interface. It stores the cached public key so 204 // we can verify the same key when we reconnect to a ledger. 205 func (pkl PrivKeyLedgerSecp256k1) Bytes() []byte { 206 return cdc.MustMarshal(pkl) 207 } 208 209 // Equals implements the PrivKey interface. It makes sure two private keys 210 // refer to the same public key. 211 func (pkl PrivKeyLedgerSecp256k1) Equals(other types.LedgerPrivKey) bool { 212 if otherKey, ok := other.(PrivKeyLedgerSecp256k1); ok { 213 return pkl.CachedPubKey.Equals(otherKey.CachedPubKey) 214 } 215 return false 216 } 217 218 func (pkl PrivKeyLedgerSecp256k1) Type() string { return "PrivKeyLedgerSecp256k1" } 219 220 // warnIfErrors wraps a function and writes a warning to stderr. This is required 221 // to avoid ignoring errors when defer is used. Using defer may result in linter warnings. 222 func warnIfErrors(f func() error) { 223 if err := f(); err != nil { 224 _, _ = fmt.Fprint(os.Stderr, "received error when closing ledger connection", err) 225 } 226 } 227 228 func convertDERtoBER(signatureDER []byte) ([]byte, error) { 229 sigDER, err := ecdsa.ParseDERSignature(signatureDER) 230 if err != nil { 231 return nil, err 232 } 233 234 sigStr := sigDER.Serialize() 235 // The format of a DER encoded signature is as follows: 236 // 0x30 <total length> 0x02 <length of R> <R> 0x02 <length of S> <S> 237 r, s := new(big.Int), new(big.Int) 238 r.SetBytes(sigStr[4 : 4+sigStr[3]]) 239 s.SetBytes(sigStr[4+sigStr[3]+2:]) 240 241 sModNScalar := new(secp.ModNScalar) 242 sModNScalar.SetByteSlice(s.Bytes()) 243 // based on https://github.com/tendermint/btcd/blob/ec996c5/btcec/signature.go#L33-L50 244 if sModNScalar.IsOverHalfOrder() { 245 s = new(big.Int).Sub(secp.S256().N, s) 246 } 247 248 sigBytes := make([]byte, 64) 249 // 0 pad the byte arrays from the left if they aren't big enough. 250 copy(sigBytes[32-len(r.Bytes()):32], r.Bytes()) 251 copy(sigBytes[64-len(s.Bytes()):64], s.Bytes()) 252 253 return sigBytes, nil 254 } 255 256 func getDevice() (SECP256K1, error) { 257 if options.discoverLedger == nil { 258 return nil, errors.New("no Ledger discovery function defined") 259 } 260 261 device, err := options.discoverLedger() 262 if err != nil { 263 return nil, fmt.Errorf("ledger nano S: %w", err) 264 } 265 266 return device, nil 267 } 268 269 func validateKey(device SECP256K1, pkl PrivKeyLedgerSecp256k1) error { 270 pub, err := getPubKeyUnsafe(device, pkl.Path) 271 if err != nil { 272 return err 273 } 274 275 // verify this matches cached address 276 if !pub.Equals(pkl.CachedPubKey) { 277 return fmt.Errorf("cached key does not match retrieved key") 278 } 279 280 return nil 281 } 282 283 // Sign calls the ledger and stores the PubKey for future use. 284 // 285 // Communication is checked on NewPrivKeyLedger and PrivKeyFromBytes, returning 286 // an error, so this should only trigger if the private key is held in memory 287 // for a while before use. 288 // 289 // Last byte P2 is 0 for LEGACY_AMINO_JSON, and 1 for TEXTUAL. 290 func sign(device SECP256K1, pkl PrivKeyLedgerSecp256k1, msg []byte, p2 byte) ([]byte, error) { 291 err := validateKey(device, pkl) 292 if err != nil { 293 return nil, err 294 } 295 296 sig, err := device.SignSECP256K1(pkl.Path.DerivationPath(), msg, p2) 297 if err != nil { 298 return nil, err 299 } 300 301 if options.skipDERConversion { 302 return sig, nil 303 } 304 305 return convertDERtoBER(sig) 306 } 307 308 // getPubKeyUnsafe reads the pubkey from a ledger device 309 // 310 // This function is marked as unsafe as it will retrieve a pubkey without user verification 311 // It can only be used to verify a pubkey but never to create new accounts/keys. In that case, 312 // please refer to getPubKeyAddrSafe 313 // 314 // since this involves IO, it may return an error, which is not exposed 315 // in the PubKey interface, so this function allows better error handling 316 func getPubKeyUnsafe(device SECP256K1, path hd.BIP44Params) (types.PubKey, error) { 317 publicKey, err := device.GetPublicKeySECP256K1(path.DerivationPath()) 318 if err != nil { 319 return nil, fmt.Errorf("please open the %v app on the Ledger device - error: %v", options.appName, err) 320 } 321 322 // re-serialize in the 33-byte compressed format 323 cmp, err := secp.ParsePubKey(publicKey) 324 if err != nil { 325 return nil, fmt.Errorf("error parsing public key: %v", err) 326 } 327 328 compressedPublicKey := make([]byte, secp256k1.PubKeySize) 329 copy(compressedPublicKey, cmp.SerializeCompressed()) 330 331 return options.createPubkey(compressedPublicKey), nil 332 } 333 334 // getPubKeyAddr reads the pubkey and the address from a ledger device. 335 // This function is marked as Safe as it will require user confirmation and 336 // account and index will be shown in the device. 337 // 338 // Since this involves IO, it may return an error, which is not exposed 339 // in the PubKey interface, so this function allows better error handling. 340 func getPubKeyAddrSafe(device SECP256K1, path hd.BIP44Params, hrp string) (types.PubKey, string, error) { 341 publicKey, addr, err := device.GetAddressPubKeySECP256K1(path.DerivationPath(), hrp) 342 if err != nil { 343 return nil, "", fmt.Errorf("%w: address rejected for path %s", err, path.String()) 344 } 345 346 // re-serialize in the 33-byte compressed format 347 cmp, err := secp.ParsePubKey(publicKey) 348 if err != nil { 349 return nil, "", fmt.Errorf("error parsing public key: %v", err) 350 } 351 352 compressedPublicKey := make([]byte, secp256k1.PubKeySize) 353 copy(compressedPublicKey, cmp.SerializeCompressed()) 354 355 return options.createPubkey(compressedPublicKey), addr, nil 356 }