Skip to content

Commit

Permalink
wallet: Add support for signing runtime txes on ledger
Browse files Browse the repository at this point in the history
  • Loading branch information
matevz committed Jan 25, 2023
1 parent 332806b commit e43b2eb
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 49 deletions.
26 changes: 14 additions & 12 deletions cmd/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"math/big"
"os"

ethCommon "github.com/ethereum/go-ethereum/common"
"github.com/spf13/cobra"
flag "github.com/spf13/pflag"

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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()
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
13 changes: 11 additions & 2 deletions cmd/common/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

Expand Down
8 changes: 5 additions & 3 deletions cmd/common/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
10 changes: 5 additions & 5 deletions cmd/contracts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/inspect/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
26 changes: 19 additions & 7 deletions wallet/ledger/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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)
}
73 changes: 62 additions & 11 deletions wallet/ledger/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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.
Expand Down
Loading

0 comments on commit e43b2eb

Please sign in to comment.