feat: add customized Ledger support (#12935)

This commit is contained in:
Austin Chandra 2022-09-06 20:01:19 -07:00 committed by GitHub
parent 518003ec29
commit 4a07259ff4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 108 additions and 13 deletions

View File

@ -46,6 +46,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
* (x/authz) [#13047](https://github.com/cosmos/cosmos-sdk/pull/13047) Add a GetAuthorization function to the keeper.
* (cli) [#13064](https://github.com/cosmos/cosmos-sdk/pull/13064) Add `debug prefixes` to list supported HRP prefixes via .
* (cli) [#12742](https://github.com/cosmos/cosmos-sdk/pull/12742) Add the `prune` CLI cmd to manually prune app store history versions based on the pruning options.
* (ledger) [#12935](https://github.com/cosmos/cosmos-sdk/pull/12935) Generalize Ledger integration to allow for different apps or keytypes that use SECP256k1.
### Improvements

View File

@ -40,6 +40,12 @@ By default, the new `MinInitialDepositRatio` parameter is set to zero during mig
feature is disabled. If chains wish to utilize the minimum proposal deposits at time of submission, the migration logic needs to be
modified to set the new parameter to the desired value.
### Ledger
Ledger support has been generalized to enable use of different apps and keytypes that use `secp256k1`. The Ledger interface remains the same, but it can now be provided through the Keyring `Options`, allowing higher-level chains to connect to different Ledger apps or use custom implementations. In addition, higher-level chains can provide custom key implementations around the Ledger public key, to enable greater flexibility with address generation and signing.
This is not a breaking change, as all values will default to use the standard Cosmos app implementation unless specified otherwise.
## [v0.46.x](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.46.0)
### Go API Changes

View File

@ -258,7 +258,7 @@ func readTxCommandFlags(clientCtx Context, flagSet *pflag.FlagSet) (Context, err
// If the `from` signer account is a ledger key, we need to use
// SIGN_MODE_AMINO_JSON, because ledger doesn't support proto yet.
// ref: https://github.com/cosmos/cosmos-sdk/issues/8109
if keyType == keyring.TypeLedger && clientCtx.SignModeStr != flags.SignModeLegacyAminoJSON {
if keyType == keyring.TypeLedger && clientCtx.SignModeStr != flags.SignModeLegacyAminoJSON && !clientCtx.LedgerHasProtobuf {
fmt.Println("Default sign-mode 'direct' not supported by Ledger, using sign-mode 'amino-json'.")
clientCtx = clientCtx.WithSignModeStr(flags.SignModeLegacyAminoJSON)
}

View File

@ -53,6 +53,7 @@ type Context struct {
FeePayer sdk.AccAddress
FeeGranter sdk.AccAddress
Viper *viper.Viper
LedgerHasProtobuf bool
PreprocessTxHook PreprocessTxFn
// IsAux is true when the signer is an auxiliary signer (e.g. the tipper).
@ -266,6 +267,13 @@ func (ctx Context) WithAux(isAux bool) Context {
return ctx
}
// WithLedgerHasProto returns the context with the provided boolean value, indicating
// whether the target Ledger application can support Protobuf payloads.
func (ctx Context) WithLedgerHasProtobuf(val bool) Context {
ctx.LedgerHasProtobuf = val
return ctx
}
// WithPreprocessTxHook returns the context with the provided preprocessing hook, which
// enables chains to preprocess the transaction using the builder.
func (ctx Context) WithPreprocessTxHook(preprocessFn PreprocessTxFn) Context {

View File

@ -144,6 +144,15 @@ type Options struct {
SupportedAlgos SigningAlgoList
// supported signing algorithms for Ledger
SupportedAlgosLedger SigningAlgoList
// define Ledger Derivation function
LedgerDerivation func() (ledger.SECP256K1, error)
// define Ledger key generation function
LedgerCreateKey func([]byte) types.PubKey
// define Ledger app name
LedgerAppName string
// indicate whether Ledger should skip DER Conversion on signature,
// depending on which format (DER or BER) the Ledger app returns signatures
LedgerSigSkipDERConv bool
}
// NewInMemory creates a transient keyring useful for testing
@ -213,6 +222,22 @@ func newKeystore(kr keyring.Keyring, cdc codec.Codec, backend string, opts ...Op
optionFn(&options)
}
if options.LedgerDerivation != nil {
ledger.SetDiscoverLedger(options.LedgerDerivation)
}
if options.LedgerCreateKey != nil {
ledger.SetCreatePubkey(options.LedgerCreateKey)
}
if options.LedgerAppName != "" {
ledger.SetAppName(options.LedgerAppName)
}
if options.LedgerSigSkipDERConv {
ledger.SetSkipDERConversion()
}
return keystore{
db: kr,
cdc: cdc,

View File

@ -23,9 +23,11 @@ import (
// set the discoverLedger function which is responsible for loading the Ledger
// device at runtime or returning an error.
func init() {
discoverLedger = func() (SECP256K1, error) {
options.discoverLedger = func() (SECP256K1, error) {
return LedgerSECP256K1Mock{}, nil
}
initOptionsDefault()
}
type LedgerSECP256K1Mock struct{}

View File

@ -13,7 +13,9 @@ import (
// set the discoverLedger function which is responsible for loading the Ledger
// device at runtime or returning an error.
func init() {
discoverLedger = func() (SECP256K1, error) {
options.discoverLedger = func() (SECP256K1, error) {
return nil, errors.New("support for ledger devices is not available in this executable")
}
initOptionsDefault()
}

View File

@ -3,13 +3,17 @@
package ledger
import ledger "github.com/cosmos/ledger-cosmos-go"
import (
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
"github.com/cosmos/cosmos-sdk/crypto/types"
ledger "github.com/cosmos/ledger-cosmos-go"
)
// If ledger support (build tag) has been enabled, which implies a CGO dependency,
// set the discoverLedger function which is responsible for loading the Ledger
// device at runtime or returning an error.
func init() {
discoverLedger = func() (SECP256K1, error) {
options.discoverLedger = func() (SECP256K1, error) {
device, err := ledger.FindLedgerCosmosUserApp()
if err != nil {
return nil, err
@ -17,4 +21,6 @@ func init() {
return device, nil
}
initOptionsDefault()
}

View File

@ -14,9 +14,8 @@ import (
"github.com/cosmos/cosmos-sdk/crypto/types"
)
// discoverLedger defines a function to be invoked at runtime for discovering
// a connected Ledger device.
var discoverLedger discoverLedgerFn
// options stores the Ledger Options that can be used to customize Ledger usage
var options Options
type (
// discoverLedgerFn defines a Ledger discovery function that returns a
@ -24,6 +23,10 @@ type (
// 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
@ -35,6 +38,15 @@ type (
SignSECP256K1([]uint32, []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 {
@ -46,6 +58,35 @@ type (
}
)
// 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 = "Cosmos"
options.skipDERConversion = false
}
// Set the discoverLedger function to use a different Ledger derivation
func SetDiscoverLedger(fn discoverLedgerFn) {
options.discoverLedger = fn
}
// Set the createPubkey function to use a different public key
func SetCreatePubkey(fn createPubkeyFn) {
options.createPubkey = fn
}
// Set the Ledger app name to use a different app name
func SetAppName(appName string) {
options.appName = appName
}
// Set the DER Conversion requirement to true (false by default)
func SetSkipDERConversion() {
options.skipDERConversion = true
}
// 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.
@ -178,11 +219,11 @@ func convertDERtoBER(signatureDER []byte) ([]byte, error) {
}
func getDevice() (SECP256K1, error) {
if discoverLedger == nil {
if options.discoverLedger == nil {
return nil, errors.New("no Ledger discovery function defined")
}
device, err := discoverLedger()
device, err := options.discoverLedger()
if err != nil {
return nil, errors.Wrap(err, "ledger nano S")
}
@ -220,6 +261,10 @@ func sign(device SECP256K1, pkl PrivKeyLedgerSecp256k1, msg []byte) ([]byte, err
return nil, err
}
if options.skipDERConversion {
return sig, nil
}
return convertDERtoBER(sig)
}
@ -234,7 +279,7 @@ func sign(device SECP256K1, pkl PrivKeyLedgerSecp256k1, msg []byte) ([]byte, err
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 Cosmos app on the Ledger device - error: %v", err)
return nil, fmt.Errorf("please open the %v app on the Ledger device - error: %v", options.appName, err)
}
// re-serialize in the 33-byte compressed format
@ -246,7 +291,7 @@ func getPubKeyUnsafe(device SECP256K1, path hd.BIP44Params) (types.PubKey, error
compressedPublicKey := make([]byte, secp256k1.PubKeySize)
copy(compressedPublicKey, cmp.SerializeCompressed())
return &secp256k1.PubKey{Key: compressedPublicKey}, nil
return options.createPubkey(compressedPublicKey), nil
}
// getPubKeyAddr reads the pubkey and the address from a ledger device.
@ -270,5 +315,5 @@ func getPubKeyAddrSafe(device SECP256K1, path hd.BIP44Params, hrp string) (types
compressedPublicKey := make([]byte, secp256k1.PubKeySize)
copy(compressedPublicKey, cmp.SerializeCompressed())
return &secp256k1.PubKey{Key: compressedPublicKey}, addr, nil
return options.createPubkey(compressedPublicKey), addr, nil
}