Skip to content

Commit

Permalink
Add SECP256k1 Ledger customization
Browse files Browse the repository at this point in the history
  • Loading branch information
0a1c committed Dec 1, 2022
1 parent 25bd172 commit 13d908d
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 12 deletions.
2 changes: 1 addition & 1 deletion client/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
8 changes: 8 additions & 0 deletions client/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ type Context struct {
FeePayer sdk.AccAddress
FeeGranter sdk.AccAddress
Viper *viper.Viper
LedgerHasProtobuf bool

// IsAux is true when the signer is an auxiliary signer (e.g. the tipper).
IsAux bool
Expand Down Expand Up @@ -277,6 +278,13 @@ func (ctx Context) WithPreprocessTxHook(preprocessFn PreprocessTxFn) 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
}

// PrintString prints the raw string to ctx.Output if it's defined, otherwise to os.Stdout
func (ctx Context) PrintString(str string) error {
return ctx.PrintBytes([]byte(str))
Expand Down
25 changes: 25 additions & 0 deletions crypto/keyring/keyring.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion crypto/ledger/ledger_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down
4 changes: 3 additions & 1 deletion crypto/ledger/ledger_notavail.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
4 changes: 3 additions & 1 deletion crypto/ledger/ledger_real.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import ledger "github.com/cosmos/ledger-cosmos-go"
// 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
}

return device, nil
}

initOptionsDefault()
}
61 changes: 53 additions & 8 deletions crypto/ledger/ledger_secp256k1.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,19 @@ 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
// connected device or an error upon failure. Its 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
Expand All @@ -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 {
Expand All @@ -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.
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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)
}

Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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
}

0 comments on commit 13d908d

Please sign in to comment.