From e43b2eb1350593577401dab54e3e26b1715819e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matev=C5=BE=20Jekovec?= Date: Wed, 25 Jan 2023 18:35:32 +0100 Subject: [PATCH] wallet: Add support for signing runtime txes on ledger --- cmd/accounts.go | 26 +++++++------- cmd/common/transaction.go | 13 +++++-- cmd/common/wallet.go | 8 +++-- cmd/contracts.go | 10 +++--- cmd/inspect/registry.go | 2 +- wallet/ledger/common.go | 26 ++++++++++---- wallet/ledger/device.go | 73 +++++++++++++++++++++++++++++++++------ wallet/ledger/ledger.go | 11 ++++-- wallet/ledger/signer.go | 19 +++++++--- 9 files changed, 139 insertions(+), 49 deletions(-) diff --git a/cmd/accounts.go b/cmd/accounts.go index 61745032..657fddbf 100644 --- a/cmd/accounts.go +++ b/cmd/accounts.go @@ -6,6 +6,7 @@ import ( "math/big" "os" + ethCommon "github.com/ethereum/go-ethereum/common" "github.com/spf13/cobra" flag "github.com/spf13/pflag" @@ -66,7 +67,7 @@ var ( c, err := connection.Connect(ctx, npa.Network) cobra.CheckErr(err) - addr, err := common.ResolveLocalAccountOrAddress(npa.Network, targetAddress) + addr, _, err := common.ResolveLocalAccountOrAddress(npa.Network, targetAddress) cobra.CheckErr(err) height, err := common.GetActualHeight( @@ -235,7 +236,7 @@ var ( } // Resolve beneficiary address. - benAddr, err := common.ResolveLocalAccountOrAddress(npa.Network, beneficiary) + benAddr, _, err := common.ResolveLocalAccountOrAddress(npa.Network, beneficiary) cobra.CheckErr(err) // Parse amount. @@ -294,9 +295,10 @@ var ( // Resolve destination address when specified. var toAddr *types.Address + var toEthAddr *ethCommon.Address if to != "" { var err error - toAddr, err = common.ResolveLocalAccountOrAddress(npa.Network, to) + toAddr, toEthAddr, err = common.ResolveLocalAccountOrAddress(npa.Network, to) cobra.CheckErr(err) } @@ -318,7 +320,7 @@ var ( }) acc := common.LoadAccount(cfg, npa.AccountName) - sigTx, meta, err := common.SignParaTimeTransaction(ctx, npa, acc, conn, tx) + sigTx, meta, err := common.SignParaTimeTransaction(ctx, npa, acc, conn, tx, map[string]interface{}{"origTo": toEthAddr}) cobra.CheckErr(err) if txCfg.Offline { @@ -395,12 +397,12 @@ var ( var addrToCheck string if to != "" { var err error - toAddr, err = common.ResolveLocalAccountOrAddress(npa.Network, to) + toAddr, _, err = common.ResolveLocalAccountOrAddress(npa.Network, to) cobra.CheckErr(err) addrToCheck = toAddr.String() } else { // Destination address is implicit, but obtain it for safety check below nonetheless. - addr, err := helpers.ResolveAddress(npa.Network, npa.Account.Address) + addr, _, err := helpers.ResolveAddress(npa.Network, npa.Account.Address) cobra.CheckErr(err) addrToCheck = addr.String() } @@ -423,7 +425,7 @@ var ( }) acc := common.LoadAccount(cfg, npa.AccountName) - sigTx, meta, err := common.SignParaTimeTransaction(ctx, npa, acc, conn, tx) + sigTx, meta, err := common.SignParaTimeTransaction(ctx, npa, acc, conn, tx, map[string]interface{}{}) cobra.CheckErr(err) if txCfg.Offline { @@ -490,7 +492,7 @@ var ( } // Resolve destination address. - toAddr, err := common.ResolveLocalAccountOrAddress(npa.Network, to) + toAddr, toEthAddr, err := common.ResolveLocalAccountOrAddress(npa.Network, to) cobra.CheckErr(err) // Check, if to address is known to be unspendable. @@ -528,7 +530,7 @@ var ( Amount: *amountBaseUnits, }) - sigTx, meta, err = common.SignParaTimeTransaction(ctx, npa, acc, conn, tx) + sigTx, meta, err = common.SignParaTimeTransaction(ctx, npa, acc, conn, tx, map[string]interface{}{"origTo": toEthAddr}) cobra.CheckErr(err) } @@ -605,7 +607,7 @@ var ( } // Resolve destination address. - toAddr, err := common.ResolveLocalAccountOrAddress(npa.Network, to) + toAddr, _, err := common.ResolveLocalAccountOrAddress(npa.Network, to) cobra.CheckErr(err) acc := common.LoadAccount(cfg, npa.AccountName) @@ -658,7 +660,7 @@ var ( } // Resolve destination address. - fromAddr, err := common.ResolveLocalAccountOrAddress(npa.Network, from) + fromAddr, _, err := common.ResolveLocalAccountOrAddress(npa.Network, from) cobra.CheckErr(err) acc := common.LoadAccount(cfg, npa.AccountName) @@ -728,7 +730,7 @@ var ( now, err = conn.Consensus().Beacon().GetEpoch(ctx, height) cobra.CheckErr(err) - addr, err := common.ResolveLocalAccountOrAddress(npa.Network, npa.Account.Address) + addr, _, err := common.ResolveLocalAccountOrAddress(npa.Network, npa.Account.Address) cobra.CheckErr(err) stakingConn := conn.Consensus().Staking() diff --git a/cmd/common/transaction.go b/cmd/common/transaction.go index 8332b777..82178686 100644 --- a/cmd/common/transaction.go +++ b/cmd/common/transaction.go @@ -147,6 +147,7 @@ func SignParaTimeTransaction( wallet wallet.Account, conn connection.Connection, tx *types.Transaction, + txDetails map[string]interface{}, ) (*types.UnverifiedTransaction, interface{}, error) { // Default to passed values and do online estimation when possible. nonce := txNonce @@ -240,9 +241,17 @@ func SignParaTimeTransaction( PrintTransactionBeforeSigning(npa, tx) // Sign the transaction. - sigCtx := signature.DeriveChainContext(npa.ParaTime.Namespace(), npa.Network.ChainContext) + // If hardware wallet used follow signing procedure as defined in ADR 14. ts := tx.PrepareForSigning() - if err := ts.AppendSign(sigCtx, wallet.Signer()); err != nil { + var err error + if wallet.UnsafeExport() == "" { + metadata := signature.EncodeAsMetadata(npa.ParaTime.Namespace(), npa.Network.ChainContext, txDetails) + err = ts.AppendRawSign(metadata, wallet.Signer()) + } else { + sigCtx := signature.DeriveChainContext(npa.ParaTime.Namespace(), npa.Network.ChainContext) + err = ts.AppendSign(sigCtx, wallet.Signer()) + } + if err != nil { return nil, nil, fmt.Errorf("failed to sign transaction: %w", err) } diff --git a/cmd/common/wallet.go b/cmd/common/wallet.go index fa692c22..3db4d517 100644 --- a/cmd/common/wallet.go +++ b/cmd/common/wallet.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/AlecAivazis/survey/v2" + ethCommon "github.com/ethereum/go-ethereum/common" "github.com/spf13/cobra" staking "github.com/oasisprotocol/oasis-core/go/staking/api" @@ -78,17 +79,18 @@ func LoadTestAccountConfig(name string) (*config.Account, error) { } // ResolveLocalAccountOrAddress resolves a string address into the corresponding account address. -func ResolveLocalAccountOrAddress(net *configSdk.Network, address string) (*types.Address, error) { +func ResolveLocalAccountOrAddress(net *configSdk.Network, address string) (*types.Address, *ethCommon.Address, error) { // Check if address is the account name in the wallet. if acc, ok := config.Global().Wallet.All[address]; ok { addr := acc.GetAddress() - return &addr, nil + // TODO: Implement acc.GetEthAddress() + return &addr, nil, nil } // Check if address is the name of an address book entry. if entry, ok := config.Global().AddressBook.All[address]; ok { addr := entry.GetAddress() - return &addr, nil + return &addr, entry.GetEthAddress(), nil } return helpers.ResolveAddress(net, address) diff --git a/cmd/contracts.go b/cmd/contracts.go index 0145f63d..ba88cd23 100644 --- a/cmd/contracts.go +++ b/cmd/contracts.go @@ -270,7 +270,7 @@ otherwise as Base64.`, }) acc := common.LoadAccount(cfg, npa.AccountName) - sigTx, meta, err := common.SignParaTimeTransaction(ctx, npa, acc, conn, tx) + sigTx, meta, err := common.SignParaTimeTransaction(ctx, npa, acc, conn, tx, map[string]interface{}{}) cobra.CheckErr(err) var result contracts.UploadResult @@ -332,7 +332,7 @@ otherwise as Base64.`, }) acc := common.LoadAccount(cfg, npa.AccountName) - sigTx, meta, err := common.SignParaTimeTransaction(ctx, npa, acc, conn, tx) + sigTx, meta, err := common.SignParaTimeTransaction(ctx, npa, acc, conn, tx, map[string]interface{}{}) cobra.CheckErr(err) var result contracts.InstantiateResult @@ -389,7 +389,7 @@ otherwise as Base64.`, }) acc := common.LoadAccount(cfg, npa.AccountName) - sigTx, meta, err := common.SignParaTimeTransaction(ctx, npa, acc, conn, tx) + sigTx, meta, err := common.SignParaTimeTransaction(ctx, npa, acc, conn, tx, map[string]interface{}{}) cobra.CheckErr(err) var result contracts.CallResult @@ -452,7 +452,7 @@ otherwise as Base64.`, }) acc := common.LoadAccount(cfg, npa.AccountName) - sigTx, meta, err := common.SignParaTimeTransaction(ctx, npa, acc, conn, tx) + sigTx, meta, err := common.SignParaTimeTransaction(ctx, npa, acc, conn, tx, map[string]interface{}{}) cobra.CheckErr(err) common.BroadcastTransaction(ctx, npa.ParaTime, conn, sigTx, meta, nil) @@ -484,7 +484,7 @@ func parsePolicy(net *config.Network, wallet *cliConfig.Account, policy string) return &contracts.Policy{Address: &address} case strings.HasPrefix(policy, "address:"): policy = strings.TrimPrefix(policy, "address:") - address, err := common.ResolveLocalAccountOrAddress(net, policy) + address, _, err := common.ResolveLocalAccountOrAddress(net, policy) if err != nil { cobra.CheckErr(fmt.Errorf("malformed address in policy: %w", err)) } diff --git a/cmd/inspect/registry.go b/cmd/inspect/registry.go index eb51bcbf..3a280680 100644 --- a/cmd/inspect/registry.go +++ b/cmd/inspect/registry.go @@ -193,7 +193,7 @@ func parseIdentifier( return sel, nil } - addr, err := helpers.ResolveAddress(npa.Network, s) + addr, _, err := helpers.ResolveAddress(npa.Network, s) if err == nil { return addr, nil } diff --git a/wallet/ledger/common.go b/wallet/ledger/common.go index dd1ab869..e91aec40 100644 --- a/wallet/ledger/common.go +++ b/wallet/ledger/common.go @@ -38,7 +38,7 @@ func getSerializedPath(path []uint32) ([]byte, error) { return message, nil } -func getSerializedPathBip44(path []uint32) ([]byte, error) { +func getSerializedBip44Path(path []uint32) ([]byte, error) { message := make([]byte, 4*len(path)) switch len(path) { case 5: @@ -56,12 +56,16 @@ func getSerializedPathBip44(path []uint32) ([]byte, error) { return message, nil } -func prepareChunks(bip44PathBytes, context, message []byte, chunkSize int) ([][]byte, error) { - if len(context) > 255 { - return nil, fmt.Errorf("maximum supported context size is 255 bytes") - } +func prepareChunks(pathBytes, context, message []byte, chunkSize int, ctxLen bool) ([][]byte, error) { + var body []byte + if ctxLen { + if len(context) > 255 { + return nil, fmt.Errorf("maximum supported context size is 255 bytes") + } - body := append([]byte{byte(len(context))}, context...) + body = []byte{byte(len(context))} + } + body = append(body, context...) body = append(body, message...) packetCount := 1 + len(body)/chunkSize @@ -70,7 +74,7 @@ func prepareChunks(bip44PathBytes, context, message []byte, chunkSize int) ([][] } chunks := make([][]byte, 0, packetCount) - chunks = append(chunks, bip44PathBytes) // First chunk is path. + chunks = append(chunks, pathBytes) // First chunk is path. r := bytes.NewReader(body) readLoop: @@ -93,3 +97,11 @@ readLoop: return chunks, nil } + +func prepareConsensusChunks(pathBytes, context, message []byte, chunkSize int) ([][]byte, error) { + return prepareChunks(pathBytes, context, message, chunkSize, true) +} + +func prepareRuntimeChunks(pathBytes, metadata, message []byte, chunkSize int) ([][]byte, error) { + return prepareChunks(pathBytes, metadata, message, chunkSize, false) +} diff --git a/wallet/ledger/device.go b/wallet/ledger/device.go index c0240968..8ba3781a 100644 --- a/wallet/ledger/device.go +++ b/wallet/ledger/device.go @@ -125,7 +125,7 @@ func (ld *ledgerDevice) GetPublicKeyEd25519(path []uint32, requireConfirmation b // GetPublicKeySecp256k1 returns the Secp256k1 public key associated with the given derivation path. // If the requireConfirmation flag is set, this will require confirmation from the user. func (ld *ledgerDevice) GetPublicKeySecp256k1(path []uint32, requireConfirmation bool) ([]byte, error) { - pathBytes, err := getSerializedPathBip44(path) + pathBytes, err := getSerializedBip44Path(path) if err != nil { return nil, fmt.Errorf("ledger: failed to get serialized BIP44 path bytes: %w", err) } @@ -176,13 +176,13 @@ func (ld *ledgerDevice) GetPublicKeySecp256k1(path []uint32, requireConfirmation // SignEd25519 asks the device to sign the given domain-separated message with the key derived from // the given derivation path. -func (ld *ledgerDevice) SignEd25519(bip44Path []uint32, context, message []byte) ([]byte, error) { - pathBytes, err := getSerializedPath(bip44Path) +func (ld *ledgerDevice) SignEd25519(path []uint32, context, message []byte) ([]byte, error) { + pathBytes, err := getSerializedPath(path) if err != nil { - return nil, fmt.Errorf("ledger: failed to get BIP44 bytes: %w", err) + return nil, fmt.Errorf("ledger: failed to get serialized path bytes: %w", err) } - chunks, err := prepareChunks(pathBytes, context, message, userMessageChunkSize) + chunks, err := prepareConsensusChunks(pathBytes, context, message, userMessageChunkSize) if err != nil { return nil, fmt.Errorf("ledger: failed to prepare chunks: %w", err) } @@ -228,16 +228,67 @@ func (ld *ledgerDevice) SignEd25519(bip44Path []uint32, context, message []byte) // SignRtEd25519 asks the device to sign the given message and metadata with the key derived from // the given hardened path. -func (ld *ledgerDevice) SignRtEd25519(bip44Path []uint32, meta, message []byte) ([]byte, error) { - // TODO - return nil, nil +func (ld *ledgerDevice) SignRtEd25519(path []uint32, metadata, message []byte) ([]byte, error) { + pathBytes, err := getSerializedPath(path) + if err != nil { + return nil, fmt.Errorf("ledger: failed to get serialized path bytes: %w", err) + } + return ld.signRt(pathBytes, metadata, message, insSignRtEd25519) } // SignRtSecp256k1 asks the device to sign the given message and metadata with the key derived from // the given BIP44 path. -func (ld *ledgerDevice) SignRtSecp256k1(bip44Path []uint32, meta, message []byte) ([]byte, error) { - // TODO - return nil, nil +func (ld *ledgerDevice) SignRtSecp256k1(bip44Path []uint32, metadata, message []byte) ([]byte, error) { + pathBytes, err := getSerializedBip44Path(bip44Path) + if err != nil { + return nil, fmt.Errorf("ledger: failed to get serialized BIP44 path bytes: %w", err) + } + return ld.signRt(pathBytes, metadata, message, insSignRtSecp256k1) +} + +func (ld *ledgerDevice) signRt(pathBytes, metadata, message []byte, instruction byte) ([]byte, error) { + chunks, err := prepareRuntimeChunks(pathBytes, metadata, message, userMessageChunkSize) + if err != nil { + return nil, fmt.Errorf("ledger: failed to prepare chunks: %w", err) + } + + var finalResponse []byte + for idx, chunk := range chunks { + payloadLen := byte(len(chunk)) + + var payloadDesc byte + switch idx { + case 0: + payloadDesc = payloadChunkInit + case len(chunks) - 1: + payloadDesc = payloadChunkLast + default: + payloadDesc = payloadChunkAdd + } + + message := []byte{claConsumer, instruction, payloadDesc, 0, payloadLen} + message = append(message, chunk...) + + response, err := ld.raw.Exchange(message) + if err != nil { + switch err.Error() { + case errMsgInvalidParameters, errMsgInvalidated: + return nil, fmt.Errorf("ledger: failed to sign: %s", string(response)) + case errMsgRejected: + return nil, fmt.Errorf("ledger: signing request rejected by user") + } + return nil, fmt.Errorf("ledger: failed to sign: %w", err) + } + + finalResponse = response + } + + // XXX: Work-around for Oasis App issue of currently not being capable of + // signing two transactions immediately one after another: + // https://github.com/Zondax/ledger-oasis/issues/68. + time.Sleep(100 * time.Millisecond) + + return finalResponse, nil } // connectToDevice connects to the first connected Ledger device. diff --git a/wallet/ledger/ledger.go b/wallet/ledger/ledger.go index f844c276..ef4731dd 100644 --- a/wallet/ledger/ledger.go +++ b/wallet/ledger/ledger.go @@ -49,7 +49,7 @@ func (af *ledgerAccountFactory) PrettyKind(rawCfg map[string]interface{}) string // Show legacy, if algorithm not set. algorithm := cfg.Algorithm if algorithm == "" { - algorithm = wallet.AlgorithmEd25519Legacy + algorithm = wallet.AlgorithmEd25519Adr8 } return fmt.Sprintf("%s (%s:%d)", af.Kind(), algorithm, cfg.Number) } @@ -94,9 +94,14 @@ func (af *ledgerAccountFactory) unmarshalConfig(raw map[string]interface{}) (*ac return nil, fmt.Errorf("missing configuration") } - // CONFIG MIGRATION: Derivation -> Algorithm. + // CONFIG MIGRATION 1 (add legacy derivation support): Set default derivation to ADR 8, if not set. + if raw["derivation"] == nil && raw["algorithm"] == nil { + raw["derivation"] = "adr8" + } + + // CONFIG MIGRATION 2 (convert derivation -> algorithm). if val, ok := raw["derivation"]; ok { - raw["algorithm"] = "ed25519-" + fmt.Sprintf("%v", val) + raw["algorithm"] = fmt.Sprintf("ed25519-%s", val) } var cfg accountConfig diff --git a/wallet/ledger/signer.go b/wallet/ledger/signer.go index 66a32d0a..8e1c90ba 100644 --- a/wallet/ledger/signer.go +++ b/wallet/ledger/signer.go @@ -3,6 +3,7 @@ package ledger import ( "fmt" + "github.com/oasisprotocol/cli/wallet" coreSignature "github.com/oasisprotocol/oasis-core/go/common/crypto/signature" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/crypto/signature" ) @@ -31,7 +32,7 @@ func (ls *ledgerCoreSigner) ContextSign(context coreSignature.Context, message [ } func (ls *ledgerCoreSigner) String() string { - return fmt.Sprintf("[ledger signer: %s]", ls.pk) + return fmt.Sprintf("[ledger consensus signer: %s]", ls.pk) } func (ls *ledgerCoreSigner) Reset() { @@ -49,16 +50,24 @@ func (ls *ledgerSigner) Public() (pk signature.PublicKey) { return ls.pk } -func (ls *ledgerSigner) ContextSign(context, message []byte) ([]byte, error) { - return nil, fmt.Errorf("ledger: signing paratime transactions without metadata not supported") +func (ls *ledgerSigner) ContextSign(metadata, message []byte) ([]byte, error) { + switch ls.algorithm { + case wallet.AlgorithmEd25519Adr8: + case wallet.AlgorithmEd25519Legacy: + return ls.dev.SignRtEd25519(ls.path, metadata, message) + case wallet.AlgorithmSecp256k1Bip44: + return ls.dev.SignRtSecp256k1(ls.path, metadata, message) + } + + return nil, fmt.Errorf("ledger: algorithm %s not supported", ls.algorithm) } func (ls *ledgerSigner) Sign(message []byte) ([]byte, error) { - return nil, fmt.Errorf("ledger: signing paratime transactions not supported") + return nil, fmt.Errorf("ledger: signing without context not supported") } func (ls *ledgerSigner) String() string { - return fmt.Sprintf("[ledger signer: %s]", ls.pk) + return fmt.Sprintf("[ledger runtime signer: %s]", ls.pk) } func (ls *ledgerSigner) Reset() {