Skip to content

Commit 4a07259

Browse files
feat: add customized Ledger support (#12935)
1 parent 518003e commit 4a07259

File tree

9 files changed

+108
-13
lines changed

9 files changed

+108
-13
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
4646
* (x/authz) [#13047](https://github.com/cosmos/cosmos-sdk/pull/13047) Add a GetAuthorization function to the keeper.
4747
* (cli) [#13064](https://github.com/cosmos/cosmos-sdk/pull/13064) Add `debug prefixes` to list supported HRP prefixes via .
4848
* (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.
49+
* (ledger) [#12935](https://github.com/cosmos/cosmos-sdk/pull/12935) Generalize Ledger integration to allow for different apps or keytypes that use SECP256k1.
4950

5051
### Improvements
5152

UPGRADING.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ By default, the new `MinInitialDepositRatio` parameter is set to zero during mig
4040
feature is disabled. If chains wish to utilize the minimum proposal deposits at time of submission, the migration logic needs to be
4141
modified to set the new parameter to the desired value.
4242

43+
### Ledger
44+
45+
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.
46+
47+
This is not a breaking change, as all values will default to use the standard Cosmos app implementation unless specified otherwise.
48+
4349
## [v0.46.x](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.46.0)
4450

4551
### Go API Changes

client/cmd.go

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

client/context.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ type Context struct {
5353
FeePayer sdk.AccAddress
5454
FeeGranter sdk.AccAddress
5555
Viper *viper.Viper
56+
LedgerHasProtobuf bool
5657
PreprocessTxHook PreprocessTxFn
5758

5859
// 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 {
266267
return ctx
267268
}
268269

270+
// WithLedgerHasProto returns the context with the provided boolean value, indicating
271+
// whether the target Ledger application can support Protobuf payloads.
272+
func (ctx Context) WithLedgerHasProtobuf(val bool) Context {
273+
ctx.LedgerHasProtobuf = val
274+
return ctx
275+
}
276+
269277
// WithPreprocessTxHook returns the context with the provided preprocessing hook, which
270278
// enables chains to preprocess the transaction using the builder.
271279
func (ctx Context) WithPreprocessTxHook(preprocessFn PreprocessTxFn) Context {

crypto/keyring/keyring.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,15 @@ type Options struct {
144144
SupportedAlgos SigningAlgoList
145145
// supported signing algorithms for Ledger
146146
SupportedAlgosLedger SigningAlgoList
147+
// define Ledger Derivation function
148+
LedgerDerivation func() (ledger.SECP256K1, error)
149+
// define Ledger key generation function
150+
LedgerCreateKey func([]byte) types.PubKey
151+
// define Ledger app name
152+
LedgerAppName string
153+
// indicate whether Ledger should skip DER Conversion on signature,
154+
// depending on which format (DER or BER) the Ledger app returns signatures
155+
LedgerSigSkipDERConv bool
147156
}
148157

149158
// NewInMemory creates a transient keyring useful for testing
@@ -213,6 +222,22 @@ func newKeystore(kr keyring.Keyring, cdc codec.Codec, backend string, opts ...Op
213222
optionFn(&options)
214223
}
215224

225+
if options.LedgerDerivation != nil {
226+
ledger.SetDiscoverLedger(options.LedgerDerivation)
227+
}
228+
229+
if options.LedgerCreateKey != nil {
230+
ledger.SetCreatePubkey(options.LedgerCreateKey)
231+
}
232+
233+
if options.LedgerAppName != "" {
234+
ledger.SetAppName(options.LedgerAppName)
235+
}
236+
237+
if options.LedgerSigSkipDERConv {
238+
ledger.SetSkipDERConversion()
239+
}
240+
216241
return keystore{
217242
db: kr,
218243
cdc: cdc,

crypto/ledger/ledger_mock.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ import (
2323
// set the discoverLedger function which is responsible for loading the Ledger
2424
// device at runtime or returning an error.
2525
func init() {
26-
discoverLedger = func() (SECP256K1, error) {
26+
options.discoverLedger = func() (SECP256K1, error) {
2727
return LedgerSECP256K1Mock{}, nil
2828
}
29+
30+
initOptionsDefault()
2931
}
3032

3133
type LedgerSECP256K1Mock struct{}

crypto/ledger/ledger_notavail.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import (
1313
// set the discoverLedger function which is responsible for loading the Ledger
1414
// device at runtime or returning an error.
1515
func init() {
16-
discoverLedger = func() (SECP256K1, error) {
16+
options.discoverLedger = func() (SECP256K1, error) {
1717
return nil, errors.New("support for ledger devices is not available in this executable")
1818
}
19+
20+
initOptionsDefault()
1921
}

crypto/ledger/ledger_real.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,24 @@
33

44
package ledger
55

6-
import ledger "github.com/cosmos/ledger-cosmos-go"
6+
import (
7+
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
8+
"github.com/cosmos/cosmos-sdk/crypto/types"
9+
ledger "github.com/cosmos/ledger-cosmos-go"
10+
)
711

812
// If ledger support (build tag) has been enabled, which implies a CGO dependency,
913
// set the discoverLedger function which is responsible for loading the Ledger
1014
// device at runtime or returning an error.
1115
func init() {
12-
discoverLedger = func() (SECP256K1, error) {
16+
options.discoverLedger = func() (SECP256K1, error) {
1317
device, err := ledger.FindLedgerCosmosUserApp()
1418
if err != nil {
1519
return nil, err
1620
}
1721

1822
return device, nil
1923
}
24+
25+
initOptionsDefault()
2026
}

crypto/ledger/ledger_secp256k1.go

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,19 @@ import (
1414
"github.com/cosmos/cosmos-sdk/crypto/types"
1515
)
1616

17-
// discoverLedger defines a function to be invoked at runtime for discovering
18-
// a connected Ledger device.
19-
var discoverLedger discoverLedgerFn
17+
// options stores the Ledger Options that can be used to customize Ledger usage
18+
var options Options
2019

2120
type (
2221
// discoverLedgerFn defines a Ledger discovery function that returns a
2322
// connected device or an error upon failure. Its allows a method to avoid CGO
2423
// dependencies when Ledger support is potentially not enabled.
2524
discoverLedgerFn func() (SECP256K1, error)
2625

26+
// createPubkeyFn supports returning different public key types that implement
27+
// types.PubKey
28+
createPubkeyFn func([]byte) types.PubKey
29+
2730
// SECP256K1 reflects an interface a Ledger API must implement for SECP256K1
2831
SECP256K1 interface {
2932
Close() error
@@ -35,6 +38,15 @@ type (
3538
SignSECP256K1([]uint32, []byte) ([]byte, error)
3639
}
3740

41+
// Options hosts customization options to account for differences in Ledger
42+
// signing and usage across chains.
43+
Options struct {
44+
discoverLedger discoverLedgerFn
45+
createPubkey createPubkeyFn
46+
appName string
47+
skipDERConversion bool
48+
}
49+
3850
// PrivKeyLedgerSecp256k1 implements PrivKey, calling the ledger nano we
3951
// cache the PubKey from the first call to use it later.
4052
PrivKeyLedgerSecp256k1 struct {
@@ -46,6 +58,35 @@ type (
4658
}
4759
)
4860

61+
// Initialize the default options values for the Cosmos Ledger
62+
func initOptionsDefault() {
63+
options.createPubkey = func(key []byte) types.PubKey {
64+
return &secp256k1.PubKey{Key: key}
65+
}
66+
options.appName = "Cosmos"
67+
options.skipDERConversion = false
68+
}
69+
70+
// Set the discoverLedger function to use a different Ledger derivation
71+
func SetDiscoverLedger(fn discoverLedgerFn) {
72+
options.discoverLedger = fn
73+
}
74+
75+
// Set the createPubkey function to use a different public key
76+
func SetCreatePubkey(fn createPubkeyFn) {
77+
options.createPubkey = fn
78+
}
79+
80+
// Set the Ledger app name to use a different app name
81+
func SetAppName(appName string) {
82+
options.appName = appName
83+
}
84+
85+
// Set the DER Conversion requirement to true (false by default)
86+
func SetSkipDERConversion() {
87+
options.skipDERConversion = true
88+
}
89+
4990
// NewPrivKeySecp256k1Unsafe will generate a new key and store the public key for later use.
5091
//
5192
// 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) {
178219
}
179220

180221
func getDevice() (SECP256K1, error) {
181-
if discoverLedger == nil {
222+
if options.discoverLedger == nil {
182223
return nil, errors.New("no Ledger discovery function defined")
183224
}
184225

185-
device, err := discoverLedger()
226+
device, err := options.discoverLedger()
186227
if err != nil {
187228
return nil, errors.Wrap(err, "ledger nano S")
188229
}
@@ -220,6 +261,10 @@ func sign(device SECP256K1, pkl PrivKeyLedgerSecp256k1, msg []byte) ([]byte, err
220261
return nil, err
221262
}
222263

264+
if options.skipDERConversion {
265+
return sig, nil
266+
}
267+
223268
return convertDERtoBER(sig)
224269
}
225270

@@ -234,7 +279,7 @@ func sign(device SECP256K1, pkl PrivKeyLedgerSecp256k1, msg []byte) ([]byte, err
234279
func getPubKeyUnsafe(device SECP256K1, path hd.BIP44Params) (types.PubKey, error) {
235280
publicKey, err := device.GetPublicKeySECP256K1(path.DerivationPath())
236281
if err != nil {
237-
return nil, fmt.Errorf("please open Cosmos app on the Ledger device - error: %v", err)
282+
return nil, fmt.Errorf("please open the %v app on the Ledger device - error: %v", options.appName, err)
238283
}
239284

240285
// re-serialize in the 33-byte compressed format
@@ -246,7 +291,7 @@ func getPubKeyUnsafe(device SECP256K1, path hd.BIP44Params) (types.PubKey, error
246291
compressedPublicKey := make([]byte, secp256k1.PubKeySize)
247292
copy(compressedPublicKey, cmp.SerializeCompressed())
248293

249-
return &secp256k1.PubKey{Key: compressedPublicKey}, nil
294+
return options.createPubkey(compressedPublicKey), nil
250295
}
251296

252297
// 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
270315
compressedPublicKey := make([]byte, secp256k1.PubKeySize)
271316
copy(compressedPublicKey, cmp.SerializeCompressed())
272317

273-
return &secp256k1.PubKey{Key: compressedPublicKey}, addr, nil
318+
return options.createPubkey(compressedPublicKey), addr, nil
274319
}

0 commit comments

Comments
 (0)