From df9bd9f35148822fbc7d7867da650b91550ada7f Mon Sep 17 00:00:00 2001 From: piux2 <> Date: Thu, 24 Mar 2022 16:58:09 -0700 Subject: [PATCH] complete bounty#5 --- Makefile | 6 +- README.md | 19 +- bounty5instruction.md | 180 +++++++++ bounty5solution.md | 125 ++++++ cmd/gnokeybk/backup.go | 143 +++++++ cmd/gnokeybk/gnokeybk.go | 40 ++ cmd/gnokeybk/list.go | 92 +++++ cmd/gnokeybk/sign.go | 101 +++++ cmd/gnoland/main.go | 4 +- pkgs/crypto/keys/backup_keybase.go | 495 ++++++++++++++++++++++++ pkgs/crypto/keys/backup_keybase_test.go | 58 +++ pkgs/crypto/keys/package.go | 1 + 12 files changed, 1259 insertions(+), 5 deletions(-) create mode 100644 bounty5instruction.md create mode 100644 bounty5solution.md create mode 100644 cmd/gnokeybk/backup.go create mode 100644 cmd/gnokeybk/gnokeybk.go create mode 100644 cmd/gnokeybk/list.go create mode 100644 cmd/gnokeybk/sign.go create mode 100644 pkgs/crypto/keys/backup_keybase.go create mode 100644 pkgs/crypto/keys/backup_keybase_test.go diff --git a/Makefile b/Makefile index a10c67a70bb..fc82344f1b5 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -all: gnoland gnokey goscan logos +all: gnoland gnokey goscan logos gnokeybk .PHONY: logos goscan gnoland gnokey logos reset test test1 test2 testrealm testrealm1 testrealm2 testpackages testpkgs @@ -18,6 +18,10 @@ gnoland: gnokey: echo "Building gnokey" go build -o build/gnokey ./cmd/gnokey +# Key tool +gnokeybk: + echo "Building gnokeybk" + go build -o build/gnokeybk ./cmd/gnokeybk # goscan scans go code to determine its AST goscan: diff --git a/README.md b/README.md index 2eeaeaa4685..2df3ee706eb 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,18 @@ + +# Bounty5 + +Problem Definition + +https://github.com/gnolang/bounties/issues/15 + +Solution + +https://github.com/piux2/gnobounty5/blob/master/bounty5solution.md + +Instruction + +https://github.com/piux2/gnobounty5/blob/master/bounty5instruction.md + # Gno At first, there was Bitcoin, out of entropy soup of the greater All. @@ -17,7 +32,7 @@ simulated by the Gnomes of the Greater Resistance. * Completely deterministic, for complete accountability. * Transactional persistence across data realms. * Designed for concurrent blockchain smart contracts systems. - + ## Status _Update Aug 26th, 2021: SDK/store,baseapp ported; Plan updated_ @@ -42,7 +57,7 @@ This is a still a work in a progress, though much of the structure of the interp and AST have taken place. Work is ongoing now to demonstrate the Realm concept before continuing to make the tests/files/\*.go tests pass. -Make sure you have >=[go1.15](https://golang.org/doc/install) installed, and then try this: +Make sure you have >=[go1.15](https://golang.org/doc/install) installed, and then try this: ```bash > git clone git@github.com:gnolang/gno.git diff --git a/bounty5instruction.md b/bounty5instruction.md new file mode 100644 index 00000000000..abdf642f502 --- /dev/null +++ b/bounty5instruction.md @@ -0,0 +1,180 @@ + + +## Install + + git clone https://github.com/piux2/gnobounty5 + + cd gnobounty5 + + make all + +My code is based on + +The codebase committed on Dec 9, 2021 +https://github.com/gnolang/gno/tree/5a1ea776cac472a42e3b0ecf4d32ebc1ede289f9 + + +## Testing data + + #### Primary Key: + + name: test1 + + mnemonic: + + source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast + + passphrase: test 1 + + generated address and pubkey + + test1 (local) - addr: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 + pub: gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pq0skzdkmzu0r9h6gny6eg8c9dc303xrrudee6z4he4y7cs5rnjwmyf40yaj + + #### Backup Key + + name: test1 + + mnemonic: + + curious syrup memory cabbage razor emotion ketchup best alley cotton enjoy nature furnace shallow donor oval tornado razor clock roof pave enroll solar wrist + + generated multisig address and pubkey + + test1 (backup local- multisig address) - addr: g16ptpek560p53qdmeja7vm2crc0gpgtqyzfuthv + multisig pub: + [0]gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pq0skzdkmzu0r9h6gny6eg8c9dc303xrrudee6z4he4y7cs5rnjwmyf40yaj + [1]gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zp7xtkykvttvcxnz9n74hfd8t4tav3t7l33p5trvyeuxd3ea8d95vhp767p + + +## Instructions + +#### Start from here if you have not created primary key yet, otherwise skip to the next step +Note: Enter words within < >. Do not enter brackets + + ./build/gnokeybk add test1 --recover + Enter a passphrase to encrypt your key to disk: + + Enter your bip39 mnemonic + + + test1 (local) - addr: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 pub: gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pq0skzdkmzu0r9h6gny6eg8c9dc303xrrudee6z4he4y7cs5rnjwmyf40yaj, path: + + +#### Start from here if you have created primary key yet. +Back up your primary key to a seperate backup keybase + + ./build/gnokeybk bkkey test1 + Enter a passphrase to encrypt your key to disk: + + Enter your backup bip39 mnemonic, which should be different from you primary mnemonic, or hit enter to generate a new one + + + Backup key is created for primary key address + g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 + + Backup key's multisig address is + g16ptpek560p53qdmeja7vm2crc0gpgtqyzfuthv + +### check and list primary key and backup key. both share the same name + + ./build/gnokeybk listbk + + Keybase primary + 0. test1 (local) - addr: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 pub: gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pq0skzdkmzu0r9h6gny6eg8c9dc303xrrudee6z4he4y7cs5rnjwmyf40yaj, path: + + + --------------------------- + Keybase backup + 0. test1 (local) - addr: g16ptpek560p53qdmeja7vm2crc0gpgtqyzfuthv pub: gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pq0skzdkmzu0r9h6gny6eg8c9dc303xrrudee6z4he4y7cs5rnjwmyf40yaj | gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zp7xtkykvttvcxnz9n74hfd8t4tav3t7l33p5trvyeuxd3ea8d95vhp767p | , path: + + + +### Sign transactions with the backup key + +Launch the gnoland chain in a separate terminal + +./build/gnoland + +Check both keys are available on-chain. these are preconfigured accounts in genesis. +In the real case, you will have to send the token to your backup key address, which is a multisig address. + +Before you sign and broadcasted messages. these two accounts on chain do not have pub keys published on the chain yet. + + ./build/gnokeybk query "auth/accounts/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" + + height: 0 + data: { + "BaseAccount": { + "address": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "coins": "1000000gnot", + "public_key": null, + "account_number": "0", + "sequence": "0" + } + } + + ./build/gnokeybk query "auth/accounts/g16ptpek560p53qdmeja7vm2crc0gpgtqyzfuthv" + + height: 0 + data: { + "BaseAccount": { + "address": "g16ptpek560p53qdmeja7vm2crc0gpgtqyzfuthv", + "coins": "1000000gnot", + "public_key": null, + "account_number": "1", + "sequence": "0" + } + } + + + +Now let's use the backup key to sign and broadcast the signed transaction +This transaction is created by test1 following the examples in https://github.com/gnolang/gno/tree/master/examples/gno.land/r/boards + +Let's sign it with the backup key. The account number is set in genesis. We need to increment sequence number each time we sign a transaction. The signer's address will be replaced by the backup key multisig address. + + ./build/gnokeybk signbk test1 --txpath addpkg.avl.unsigned.json --chainid "testchain" --number 1 --sequence 0 > addpkg.avl.signed.json + + Enter password. + + +### broadcast transactions + + ./build/gnokeybk broadcast addpkg.avl.signed.json + + $ OK! + +The transaction is successfully broadcasted and accepted by the chain. + +We query the backup key account on chain. The multisig pub key is published. + + + .build/gnokeybk query "auth/accounts/g16ptpek560p53qdmeja7vm2crc0gpgtqyzfuthv" + + height: 0 + data: { + "BaseAccount": { + "address": "g16ptpek560p53qdmeja7vm2crc0gpgtqyzfuthv", + "coins": "999898gnot", + "public_key": { + "@type": "/tm.PubKeyMultisig", + "threshold": "2", + "pubkeys": [ + { + "@type": "/tm.PubKeySecp256k1", + "value": "A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y" + }, + { + "@type": "/tm.PubKeyEd25519", + "value": "+MuxLMWtmDTEWfq3S066r6yK/fjENFjYTPDNjnp2low=" + } + ] + }, + "account_number": "1", + "sequence": "1" + } + } + + +DONE! diff --git a/bounty5solution.md b/bounty5solution.md new file mode 100644 index 00000000000..f72a291dffc --- /dev/null +++ b/bounty5solution.md @@ -0,0 +1,125 @@ +## Problem Definition + +here is the problem definition + +https://github.com/gnolang/bounties/issues/15 + + +## Solution Breakdown + + +>Defend against hacking issues that may arise from hardware wallet providers. + +hardware wallet provider may not use a RAND number to generate mnemonic. The attacker could use each number in a pre-existing sequence and a counter stored on devices to generate mnemonics that look random but is not. For example, use pre-existing sequence in Pi, Prime number, or even block hash in the major blockchain. + +> Defend against potential weaknesses in the Secp256k1 algorithm. For example, Satoshi's "2^256 - 2^32 - 2^9 - 2^8 - 2^7 - 2^6 - 2^4 - 1" constant is potentially flawed. + +we can use the ed25519 algorithm to create a backup key and store it in a separate key base. + +> Defend against potential weaknesses in bip32's HMAC-SHA512 function. SHA256 is somewhat economically tested by the Bitcoin hashing algorithm and mining incentives, so the goal here is to provide an alternative that relies primarily on sha256. + + +> Defend against potential weaknesses in bip39's PBKDF2 function (which also relies on HMAC-SHA512). For example, the PBKDF2 function may have a limited range of outputs, which limits the private keyspace. + +We can use a real hkdf with an extract-then-expand scheme. +https://en.wikipedia.org/wiki/HKDF + +It is standardized in RFC5869 by Internet Engineering Task Force. + +https://datatracker.ietf.org/doc/html/rfc5869 + +The primitive is implemented in golang. +https://pkg.go.dev/golang.org/x/crypto/hkdf + + +>Continue to allow the usage of hardware signing devices, and bip32/39 and secp256k1 algorithms for day-to-day usage. + +Create a 2/2 multi-sig combined from primary key and backup key +Use a single command to sign transactions with a 2/2 multi-sig. No need to sign a transaction twice and no need to combine two signatures to gather. + + +## Implementation explained. + +This bounty#5 implementation uses the following priorities to resolve conflicts + +Security > Useability > Simple implementation + +I extended gnokey to gnokeybk which is backward compatible + +#### bkkey sub command: + +it generates a backup key: + +- Create a backup key using a mnemonic generate on an air gap computer +- Use ed25519 and HKDF to generate a backup key +- Store the backup key in a separate key base file. It provides additional security. We can even move the backup key store from the air-gap computer to a USB stick after we complete the signing task. + +- The backup key info is multi-sig info. It contains ed25519 key and multisig pubkeys that combine primary pubkey and backup pubkey. Since the attacking point for multi-sig is at the time of combining two keys, we need to make sure only the person holding the primary key can create this backup key info. This is very IMPORTANT. + +- This implementation introduces a primary key signature in backup key info. It uses the primary key to sign the backup info including name, ed25519 privkey armor, and combined multisig pubkeys. The signature and the primary pubkey are used to prove that the backup info is created by the primary key holder. If the backup key store is altered, it will give errors + + + + +#### listbk sub command: + +It lists the primary key and backup key from two different key stores. + +#### sign sub command: +It retrieves the primary key and backup key from Keystore sign the transaction, combine signatures in one transaction with multisig pubkeys. During the process, it also verifies the backup key integrity stored in the backup key store. + +#### changes in the forked code base. + +To minimize the impact to the code base before the implementation is reviewed. I wrote all the relevant files in gno/pkgs/crypto/keys/backup_keybase.go and gno/cmd/gnokeybk/ + once we review it and approve the implementation. these codes can be merged back to the existing framework. + +There are two additional minor updates on the forked code base. +Registered infoBk package +github.com/gnolang/gno/pkgs/package.go + + +Added backup key multisig address in genesis state +github.com/gnolang/gno/cmd/gnoland/main.go + + +## Discussions. +> I propose that we allow for the registration of an alternative key based on ed25519 as a backup key that is not used but can be used in case of emergencies when issues arise with the default bip32/39/sec256k1 keys. + + +This part is tricky since on-chain verification needs to decide if it requires verifying two signatures or just one. Maybe we can force users to use the backup key (two signatures)to sign contracts once it is generated and registered on the Chain. To transfer funds, we may not need to be strict and allow the use of either primary key( one signature) or backup key (a multisig with two signatures) + +> Tooling should be provided to allow this alternative backup key to be generated on an air-gapped computer, without the aid of a specialized crypto signing device. + +Agree, done + +> The (primary) mnemonic for the secp256k1 key must be separate from the (secondary) mnemonic for the ed25519 key, because hardware crypto signers ask for the mnemonic to set up the device, and the second mnemonic should only be entered on air-gapped general computers. + +Agree, done + +> In Gno, instead of relying on the memo field, we can add a "RegisterAccount" sdk.Msg to provide the backup ed25519 pubkey-hash, and furthermore, we can require the user to register their account w/ backup key before actually using their secp256k1 keys. This way, users can just use a hardware wallet to generate a secp256k1 address to receive funds but are incentivized to register a ed25519 pubkey. + +Suggest to store backup multisig key address in registeredAccount + +> In the case of issues with the bip32/39/secp256k1 system, the gno.land chain can fork and just use the ed25519 key. Users who have not yet registered an ed25519 backup key would either end up losing their tokens, or possibly the tokens would have to be distributed with real-world KYC etc to catch the hacker who may be trying to reclaim them on the gno.land fork -- this assuming that we come up with a reasonable way to protect the privacy of users while also keeping the recovery accountable. Users who register their account with a backup key would not be affected, and if there are no issues with bip32/39/secp256k1, none of this matters. + +With existing implementation and force using the backup once it is generated, we do not need to fork the chain even we know the primary keys are compromised. + + +> 24-words -> standard kdf & hd derivation -> secp256k1 address +> 24-words (different) -> sha256-based-kdf -> ed25519 address + +Agree, done. + +> And finally, the gnokey command should be updated to make all of this easier. I like the approach taken by #11 and #14; there just needs to be another gnokey subcommand that bundles these two together (and explains everything, and requires different mnemonics) and produces an unsigned msg for account registration on gno, as well as MEMO-based airdrop-registration on cosmos. + +Agree, users can recover the cosmos key on gnoland first and then generate a backup key. The generated backup key address can be stored in "RegisterAccount" sdk.Msg and broadcast to the chain. This way chain can key a record of who generated backup. + +## Assumptions, Limitations, and Further Discussions + +When the attack happens we probably do not know in advance. We will only discover it after the primary key is distributed and used widely. + + +For security reasons, no backup key should be altered or recreated. Since the bad guy can create a backup key as well and submit registration transactions to the chain. There is no way to prevent it on Chain. So we are under the assumption that users create backup keys before bad guys take the action + + +Due above open issues and assumptions, when a backup key is recognized on the chain, the chain should only accept the tx signed by backup key multisig diff --git a/cmd/gnokeybk/backup.go b/cmd/gnokeybk/backup.go new file mode 100644 index 00000000000..619280df9d2 --- /dev/null +++ b/cmd/gnokeybk/backup.go @@ -0,0 +1,143 @@ +package main + +import ( + "fmt" + + "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/crypto" + "github.com/gnolang/gno/pkgs/crypto/bip39" + "github.com/gnolang/gno/pkgs/crypto/keys" + "github.com/gnolang/gno/pkgs/crypto/keys/client" + "github.com/gnolang/gno/pkgs/errors" +) + +// It finds the address to the key name and ask user to generate a new priviate key with the same nemonic +// and sign the relation between the new backup public key and current pubkey. +// If the name is not found, it asks user to add new key, which automatically genereate back up key. +// TODO Add customized entropy to generate Mnemonic. +const mnemonicEntropySize = 256 + +func backupKeyApp(cmd *command.Command, args []string, iopts interface{}) error { + opts := iopts.(client.BaseOptions) + + if len(args) != 1 { + + cmd.ErrPrintln("Usage: gnokeybk bkkey ") + return errors.New("invalid args") + } + + // read primary key's public info + name := args[0] + kb, err := keys.NewKeyBaseFromDir(opts.Home) + if err != nil { + + return err + } + + info, err := kb.Get(name) + if err != nil { + //TODO: call addApp to generate mnemonic and the primary key + return fmt.Errorf("%s does not exist. please create a primary key first", name) + } + //TODO: add switch to support ledger info + + if info.GetType() != keys.TypeLocal { + return errors.New("backup key only work for local private key") + } + + addr := info.GetAddress() + cmd.Printfln("This is your primary wallet address: %s\n", addr) + cmd.Printfln("Please input corresponding mnemonic to generate back up key.") + + // import mnemonic and add bkkey in backup key store + // TODO: take care of multisig case and ledger case as in addApp() + + // you can have one single seed with multiple passphrases to create multiple different wallets. + // Each wallet would be designated by a different passphrase. seed = "mnemonic"+phassphrase? + const bip39Passphrase string = "" + // TODO: should user enter bip39 passphrase? Maybe not. User has a lot burn already. + // TODO: should we add bip39 passphrase to backup key generation automatically? + // Maybe not, backup key already creates an layer of security and extra burdon to the user. + // Plus, backward compatible maintenaince will be a nightmare + + passphrase, err := cmd.GetPassword("Enter the passphrase to unlock the key store") + + var priv crypto.PrivKey + priv, err = kb.ExportPrivateKeyObject(name, passphrase) + + if err != nil { + + return fmt.Errorf("Please check the pass phrase for %s, it can not unlock the keybase.", name) + } + + kbBK, err := keys.NewBkKeyBaseFromDir(opts.Home) + + if err != nil { + return err + } + + //TODO: Do we allow people create multiple backup key? + // It could be a nightmare for an end user track multiple backup key. + + i, err := kbBK.Get(name) + if i != nil { + + return fmt.Errorf("backup key already generated for %s", name) + } + + bip39Message := "Enter your backup bip39 mnemonic, which should be different from you primary mnemonic, or hit enter to generate a new one" + mnemonic, err := cmd.GetString(bip39Message) + + if err != nil { + + return err + } + + if len(mnemonic) == 0 { + // read entropy seed straight from crypto.Rand and convert to mnemonic + entropySeed, err := bip39.NewEntropy(mnemonicEntropySize) + if err != nil { + return err + } + + mnemonic, err = bip39.NewMnemonic(entropySeed[:]) + if err != nil { + return err + } + + cmd.Printfln(` +**IMPORTANT** write this mnemonic phrase in a safe place. +It is the only way to recover your back up account if you ever forget your password. +%v +`, mnemonic) + } + + if !bip39.IsMnemonicValid(mnemonic) { + + return errors.New("invalid mnemonic") + + } + // the bip39 passphrase is appendixed to the mnemonic to generate new account + //TODO: take care multi derived accounts from the same mnemonic + account := uint32(0) + index := uint32(0) + + infobk, err := keys.BackupAccount(priv, kbBK, name, mnemonic, bip39Passphrase, passphrase, account, index) + + addrbk := infobk.GetAddress() + // verify if mnemonic generate the same address + /* + if addr.Compare(addrbk) != 0 { + mnemonicMsg := "The imput mnemonic is not correct.\n %s \n" + addrMsg := "It does not match the address.\n %s \n" + return fmt.Errorf( + mnemonicMsg, mnemonic, addrMsg, mnemonic, addr.String()) + + } + */ + + cmd.Printfln("\nBackup key is created for primary key address\n%s", addr) + cmd.Printfln("\nBackup key's multisig address is \n%s", addrbk.String()) + + return nil +} diff --git a/cmd/gnokeybk/gnokeybk.go b/cmd/gnokeybk/gnokeybk.go new file mode 100644 index 00000000000..5b120acb58d --- /dev/null +++ b/cmd/gnokeybk/gnokeybk.go @@ -0,0 +1,40 @@ +package main + +import ( + + // "io/ioutil" + "os" + + // "strings" + + // "github.com/gnolang/gno/pkgs/amino" + + "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/crypto/keys/client" + // "github.com/gnolang/gno/pkgs/sdk/vm" + // "github.com/gnolang/gno/pkgs/std" +) + +func main() { + cmd := command.NewStdCommand() + + // set default options. + + // customize call to command. + // insert args and options here. + // TODO: use flags or */pflags. + + exec := os.Args[0] + args := os.Args[1:] + + client.AddApp(backupKeyApp, "bkkey", "create a backup key to a backup keybase", client.DefaultBaseOptions) + client.AddApp(signBkApp, "signbk", "sign a transaction with the primary key and backup key", client.DefaultSignOptions) + client.AddApp(listBkApp, "listbk", "list all know keys including back up keys", client.DefaultListOptions) + + err := client.RunMain(cmd, exec, args) + if err != nil { + cmd.ErrPrintfln("%s", err.Error()) + //cmd.ErrPrintfln("%#v", err) + return // exit + } +} diff --git a/cmd/gnokeybk/list.go b/cmd/gnokeybk/list.go new file mode 100644 index 00000000000..a527ed62ca7 --- /dev/null +++ b/cmd/gnokeybk/list.go @@ -0,0 +1,92 @@ +package main + +import ( + "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/crypto/keys" + "github.com/gnolang/gno/pkgs/crypto/keys/client" + "github.com/gnolang/gno/pkgs/crypto/multisig" + "github.com/gnolang/gno/pkgs/errors" +) + +/* +type ListOptions struct { + client.BaseOptions // home, ... +} + +var DefaultListOptions = ListOptions{ + BaseOptions: client.DefaultBaseOptions, +} +*/ + +func listBkApp(cmd *command.Command, args []string, iopts interface{}) error { + + if len(args) != 0 { + cmd.ErrPrintfln("Usage: list (no args)") + return errors.New("invalid args") + } + + opts := iopts.(client.ListOptions) + kb, err := keys.NewKeyBaseFromDir(opts.Home) + if err != nil { + return err + } + + bkKeyBase, err := keys.NewBkKeyBaseFromDir(opts.Home) + if err != nil { + return err + } + + infos, err := kb.List() + if err != nil { + + return err + + } + + printInfos(cmd, infos, "primary") + + cmd.Println("\n---------------------------") + + infos, err = bkKeyBase.List() + if err != nil { + + return err + + } + printInfos(cmd, infos, "backup") + + return nil +} + +func printInfos(cmd *command.Command, infos []keys.Info, keybaseName string) { + + cmd.Printfln("Keybase %s", keybaseName) + var keypubString string + + for i, info := range infos { + keyname := info.GetName() + keytype := info.GetType() + keypub := info.GetPubKey() + keyaddr := info.GetAddress() + keypath, _ := info.GetPath() + keypubString = "" + + if mPub, ok := keypub.(multisig.PubKeyMultisigThreshold); ok { + + for _, pub := range mPub.PubKeys { + + keypubString = keypubString + pub.String() + " | " + } + + } else { + + keypubString = keypub.String() + } + + cmd.Printfln("%d. %s (%s) - addr: %v pub: %v, path: %v\n", + i, keyname, keytype, keyaddr, keypubString, keypath) + + //TODO: implement PubKeyMultisigThreshold.String() + + } +} diff --git a/cmd/gnokeybk/sign.go b/cmd/gnokeybk/sign.go new file mode 100644 index 00000000000..5180705ecb4 --- /dev/null +++ b/cmd/gnokeybk/sign.go @@ -0,0 +1,101 @@ +package main + +import ( + "io/ioutil" + + "github.com/gnolang/gno/pkgs/amino" + "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/crypto/keys" + "github.com/gnolang/gno/pkgs/crypto/keys/client" + "github.com/gnolang/gno/pkgs/errors" + "github.com/gnolang/gno/pkgs/std" +) + +// replace the message sender/creater/signer to back key address if the message's sender address matches +// the backup key's primary address. + +func signBkApp(cmd *command.Command, args []string, iopts interface{}) error { + var kbPrimary keys.Keybase + var kbBackup keys.Keybase + var err error + var opts client.SignOptions = iopts.(client.SignOptions) + + if len(args) != 1 { + cmd.ErrPrintfln("Usage: sign ") + return errors.New("invalid args") + } + if opts.AccountNumber == nil { + return errors.New("invalid account number") + } + if opts.Sequence == nil { + return errors.New("invalid sequence") + } + + name := args[0] + txpath := opts.TxPath + kbPrimary, err = keys.NewKeyBaseFromDir(opts.Home) + if err != nil { + return err + } + + kbBackup, err = keys.NewBkKeyBaseFromDir(opts.Home) + if err != nil { + return err + } + + // read tx to sign + var tx std.Tx + var txjson []byte + if txpath == "-" { // from stdin. + txjsonstr, err := cmd.GetString("Enter tx to sign, terminated by a newline.") + if err != nil { + return err + } + txjson = []byte(txjsonstr) + } else { // from file + txjson, err = ioutil.ReadFile(txpath) + if err != nil { + return err + } + } + err = amino.UnmarshalJSON(txjson, &tx) + if err != nil { + return err + } + + // retrieve password + pass, err := "", error(nil) + if opts.Quiet { + pass, err = cmd.GetPassword("") + } else { + pass, err = cmd.GetPassword("Enter password.") + } + if err != nil { + return err + } + + s := keys.SignerInfo{ + ChainId: opts.ChainID, + AccountNumber: *opts.AccountNumber, // back info's mutlsig account number + Sequence: *opts.Sequence, // back info's multisig sequence number + + } + + // sign tx + + signedTx, err := keys.SignTx(kbPrimary, kbBackup, name, pass, tx, s) + + if err != nil { + + return err + } + // print tx + txjson2, err := amino.MarshalJSON(signedTx) + if err != nil { + return err + } + cmd.Printfln(string(txjson2)) + + return nil + +} diff --git a/cmd/gnoland/main.go b/cmd/gnoland/main.go index 7d66cedb991..580ea45ea57 100644 --- a/cmd/gnoland/main.go +++ b/cmd/gnoland/main.go @@ -75,8 +75,8 @@ func makeGenesisDoc(pvPub crypto.PubKey) *bft.GenesisDoc { } gen.AppState = gnoland.GnoGenesisState{ Balances: []string{ - "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5=1000gnot", - "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj=1000000000gnot", + "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5=1000000gnot", + "g16ptpek560p53qdmeja7vm2crc0gpgtqyzfuthv=1000000gnot", }, } return gen diff --git a/pkgs/crypto/keys/backup_keybase.go b/pkgs/crypto/keys/backup_keybase.go new file mode 100644 index 00000000000..a7f5e26fe59 --- /dev/null +++ b/pkgs/crypto/keys/backup_keybase.go @@ -0,0 +1,495 @@ +package keys + +import ( + "bytes" + "crypto/sha256" + "encoding/json" + "errors" + "fmt" + "io" + "path/filepath" + + "github.com/gnolang/gno/pkgs/amino" + dbm "github.com/gnolang/gno/pkgs/db" + "github.com/gnolang/gno/pkgs/std" + + "github.com/gnolang/gno/pkgs/crypto" + + "github.com/gnolang/gno/pkgs/crypto/bip39" + gnoEd25519 "github.com/gnolang/gno/pkgs/crypto/ed25519" + "github.com/gnolang/gno/pkgs/crypto/hd" + "github.com/gnolang/gno/pkgs/crypto/keys/armor" + "github.com/gnolang/gno/pkgs/sdk/vm" + + "github.com/gnolang/gno/pkgs/crypto/multisig" + "golang.org/x/crypto/ed25519" + "golang.org/x/crypto/hkdf" +) + +// InfoBK store's back up key in a backup local storage +// TODO: move gno/pkgs/crypto/keys/type.go + +// need review for this infoBk structure +/* +type Info = keys.Info +type Keybase = keys.Keybase +type KeyType = keys.KeyType + +const TypeLocal = keys.TypeLocal +*/ + +// infoBk contains multisig info that is the main property to callers. + +type infoBk struct { + // backup local key information + Name string `json:"name"` // same as primary key + // no pubkey field. the pubkey or infoBk is in MultisignInfo + + PrivKeyArmor string `json:"privkey.armor"` // private back key in armored ASCII format + MultisigInfo Info `json:"multisig_info"` // Multisig holds the primary pubkey and back pubkey as a 2/2 multisig + // A Secp256k1 signature. + // Use the primary priv key sign the ecoded JSON string back up info Name + Pubkey(backup)+PrivKeyArmo(backup) + // The signature is to show that infoBk is created by the primary key holder. + // It is also verifable if someone change the infoBk record. + + Signature []byte `json:"signature"` + + // this is used to verify the signature signed using primary key secp256k1 + PrimaryPubKey crypto.PubKey `json:"primary_pubkey"` +} + +//ask the compiler to check infoBk type implements Info interface + +var _ Info = &infoBk{} + +func newInfoBk(name string, privArmor string) Info { + return &infoBk{ + Name: name, + + PrivKeyArmor: privArmor, + } + +} + +// GetType implements Info interface +func (i infoBk) GetType() KeyType { + return TypeLocal +} + +// GetType implements Info interface +func (i infoBk) GetName() string { + return i.Name +} + +// GetType implements Info interface +func (i infoBk) GetPubKey() crypto.PubKey { + return i.MultisigInfo.GetPubKey() +} + +// GetType implements Info interface +func (i infoBk) GetAddress() crypto.Address { + return i.MultisigInfo.GetAddress() +} + +// GetType implements Info interface +func (i infoBk) GetPath() (*hd.BIP44Params, error) { + return nil, fmt.Errorf("BIP44 Paths are not available for this type") +} + +//TODO: once reviewed passed, merge this methods to /pkgs/crypto/keys/keybase.go +func BackupAccount(primaryPrivKey crypto.PrivKey, kbBk Keybase, name, mnemonic, bip39Passwd, encryptPasswd string, account uint32, index uint32) (Info, error) { + + coinType := crypto.CoinType + hdPath := hd.NewFundraiserParams(account, coinType, index) + //create a backup info + info, err := CreateBackupAccountBip44(primaryPrivKey, kbBk, name, mnemonic, bip39Passwd, encryptPasswd, *hdPath) + return info, err + +} + +func CreateBackupAccountBip44(primaryPrivKey crypto.PrivKey, kbBk Keybase, name, mnemonic, bip39Passphrase, encryptPasswd string, params hd.BIP44Params) (Info, error) { + + //bip39 uses PBKDF2 to hash the mnemonic. PBKDF2 is a pass word hash function and not a + // KDF which provides key extraction and extension + // at this point the seed is still a seed not a private key yet. + seed, err := bip39.NewSeedWithErrorChecking(mnemonic, bip39Passphrase) + if err != nil { + return infoBk{}, err + } + + info, err := persistBkKey(primaryPrivKey, kbBk, seed, name, encryptPasswd, params.String()) + return info, err +} + +// +// persistBkKey uses primary key to sign the backup key info to key the backup key's integrity + +func persistBkKey(primaryPrivKey crypto.PrivKey, kbBk Keybase, seed []byte, name, passwd, fullHdPath string) (Info, error) { + + //The back up key need a simple Sha256 based KDF which is different from the primary key + + //We can use HKDF for KDF + //https://rfc-editor.org/rfc/rfc5869.html + // hkdf does not expect the salt to be a secret and is optional + + /* + hash := sha256.New + salt := make([]byte, hash().Size()) + + if _, err := rand.Read(salt); err != nil { + panic(err) + } + */ + //TODO: should we use a hard coded salt? + + var salt []byte + info := []byte("gnokey hkdf") + // the size of the privkey is 32 byte + expanedKeyReader := hkdf.New(sha256.New, seed, salt, info) + privBkKey := make([]byte, 32) + if _, err := io.ReadFull(expanedKeyReader, privBkKey); err != nil { + + panic(err) + } + + // in go/x/crypto/ed25519/ed25519.go + //This package refers to the RFC8032 private key as the “seed”. + + // ed25519 is used to generate public key from private key + // the returned a key (64 byte) = priveky(32 byte) + pubkey(32 byte) + // the first 32 byte is private key (see) and the reset is public key + bkKey := ed25519.NewKeyFromSeed(privBkKey) + + // cover to PriveKeyEd25519 type used in gno + var privKeyEd gnoEd25519.PrivKeyEd25519 + copy(privKeyEd[:], bkKey) + + bkInfo, err := writeLocalBkKey(kbBk, name, privKeyEd, primaryPrivKey, passwd) + + return bkInfo, err +} + +//primary key + backup key is a 2/2 multisig threshold pubkey + +func writeLocalBkKey(kbBk Keybase, name string, bkKey crypto.PrivKey, primaryKey crypto.PrivKey, passphrase string) (Info, error) { + + //TODO: updated the armored privKey file with correct passwd encryption notaion + // bcrypt is not KDF. It is a secure hash to protect the password + privArmor := armor.EncryptArmorPrivKey(bkKey, passphrase) + pub := bkKey.PubKey() // signle backup Key + info := newInfoBk(name, privArmor) + //fmt.Println("back up PubKey", pub) + //fmt.Println("privArmor", privArmor) + + // sign name+pubkey+privArmor + multisiginfo + + infobk := info.(*infoBk) + pubkeys := []crypto.PubKey{ + primaryKey.PubKey(), //primary pubkey + pub, //backup pubkey + } + + multisig := multisig.NewPubKeyMultisigThreshold(2, pubkeys) + + infobk.MultisigInfo = NewMultiInfo("backup", multisig) + + //TODO: disussion, could use a document structure. json is simple and good enough for now. + msg, err := json.Marshal(infobk) + //fmt.Println("msg", string(msg)) + + // sign name + PubKey + PrivKeyArmor + MultisgInfo + // To show that the multisig is created by the primary key holder + infobk.Signature, err = primaryKey.Sign(msg) + //fmt.Println("Signature", infobk.Signature) + + // attach the primary pubkey in the end. it is used to verify the signature and pubkey + infobk.PrimaryPubKey = primaryKey.PubKey() + //fmt.Println("PrimaryPubkey", infobk.PrimaryPubKey.String()) + + k := kbBk.(dbKeybase) + + k.writeInfo(name, infobk) + return info.(*infoBk), err +} + +// Sign uses primary key and backup key to sign the message with the multisig +// The primary keybase and backup keybase must be accessible at the same time, which +// is more secure. +// the other option is to sign the the message with priamaryKey and back up KEY seperately. +// since the primary private key is not available at time of siging, the verificatin need to +// only relies on the signature, primary pubkey and information in backup keybase. +// it will introduce attacking oppertunity at the time the messages are combined. +// TODO: A ADR This is also a trade off between usability and security and implementaion complexity. + +func signBackup(primaryPriv crypto.PrivKey, backupInfo infoBk, name, passPhrase string, msg []byte) (sig []byte, pub crypto.PubKey, err error) { + + var backupPriv crypto.PrivKey + + // validate + + err = verifyBkInfo(backupInfo, primaryPriv) + + if err != nil { + + return nil, nil, err + + } + backupPriv, err = armor.UnarmorDecryptPrivKey(backupInfo.PrivKeyArmor, passPhrase) + if err != nil { + return nil, nil, err + } + + //sign the message + + // the signer property of the message is primaryKey + + backupSig, err := backupPriv.Sign(msg) + if err != nil { + return nil, nil, err + } + + backupPub := backupPriv.PubKey() + + return backupSig, backupPub, nil + +} + +// verifyBkInfo verify if the info entry in bkKeybase is modify by attackers. +// It checks Pubkey, Signature of infoBk +// TODO: you don't need private key to validate signature with signer's PubKey +//Here is used a shortcut solution since this function is only called by Sign() which has +//privkey at the time calling verifyBkInfo already. + +func verifyBkInfo(binfo infoBk, primaryPrivKey crypto.PrivKey) (err error) { + + var mPub multisig.PubKeyMultisigThreshold + var ok bool + + primaryPubKey := primaryPrivKey.PubKey() + backupMultiKeys := binfo.GetPubKey() + + if mPub, ok = backupMultiKeys.(multisig.PubKeyMultisigThreshold); ok { + + //check pubkey + + if primaryPubKey.Equals(mPub.PubKeys[0]) == false { + + return fmt.Errorf("pubkey in back up info %v does not match with primary pubkey %v", mPub.PubKeys[0], primaryPubKey) + + } + + } else { + + return fmt.Errorf("backup keybase is compromised: can assert the type %T", backupMultiKeys) + } + + // check signature + + backupPubKey := mPub.PubKeys[1] + + infoBkSig, err := createInfoBkSignature(primaryPrivKey, primaryPubKey, backupPubKey, binfo.GetName(), binfo.PrivKeyArmor) + + if err != nil { + + return err + + } + + if bytes.Equal(infoBkSig, binfo.Signature) == false { + + return errors.New("infoBk's signature does not match with orignal") + } + + return nil +} + +func createInfoBkSignature(primaryPrivKey crypto.PrivKey, primaryPubKey, backupPubKey crypto.PubKey, name string, privkeyArmor string) (infoBkSig []byte, err error) { + + // create infoBk + info := newInfoBk(name, privkeyArmor) + infobk := info.(*infoBk) + pubkeys := []crypto.PubKey{ + primaryPubKey, //primary pubkey + backupPubKey, //backup pubkey + } + + multisig := multisig.NewPubKeyMultisigThreshold(2, pubkeys) + infobk.MultisigInfo = NewMultiInfo("backup", multisig) + + //sign name+pubkey+privArmor + multisiginfo + msg, err := json.Marshal(infobk) + + infoBkSig, err = primaryPrivKey.Sign(msg) + + return + +} + +//Todo merge it to keys/uitls.go +const defaultBkKeyDBName = "keys_backup" + +func NewBkKeyBaseFromDir(rootDir string) (Keybase, error) { + //TODO: Remove this after BackupAccount() method are implemented in lazyDBKeybase + // create data directory and make sure the program has the rwx ownership of data directory + _ = NewLazyDBKeybase(defaultBkKeyDBName, filepath.Join(rootDir, "data")) + + db, err := dbm.NewGoLevelDB(defaultBkKeyDBName, filepath.Join(rootDir, "data")) + if err != nil { + + return nil, err + } + + //defer db.Close() + + return NewDBKeybase(db), nil +} + +type SignerInfo struct { + ChainId string + AccountNumber uint64 + Sequence uint64 +} + +func SignTx(kbPrimary Keybase, kbBackup Keybase, name, passPhrase string, unsignedTx std.Tx, signerInfo SignerInfo) (signedTx std.Tx, err error) { + + // get primary + primaryInfo, err := kbPrimary.Get(name) + + if err != nil { + + err = fmt.Errorf("%s not found in primary keybase\n", name) + return signedTx, err + } + var primaryPriv crypto.PrivKey + + switch primaryInfo.(type) { + + case localInfo: + p := primaryInfo.(localInfo) + if p.PrivKeyArmor == "" { + err = fmt.Errorf("private key not available") + return signedTx, err + } + + primaryPriv, err = armor.UnarmorDecryptPrivKey(p.PrivKeyArmor, passPhrase) + if err != nil { + return signedTx, err + } + + case ledgerInfo, offlineInfo, multiInfo: + err = fmt.Errorf("cannot sign with key %s, only a local key is supported", name) + + return signedTx, err + } + + // if the backup database is presented in the directory. + // sign the transaction with back keys + // TODO: add indicator in primary keybase that a back up key is generated and prompt user to provide + // bkKeybase if it is presented in the directory + + primaryPub := primaryPriv.PubKey() + backupInfo, err := kbBackup.Get(name) + + if err != nil { + err = fmt.Errorf("%s not found in backup keybase. backup your %s first", name, name) + return signedTx, err + } + b := backupInfo.(infoBk) + + multisigInfo := b.MultisigInfo + + // The signature needs to be multisig with sequence. The first is the primary key and second is the back up keys + // However, account # and sequences # of primary account maybe different from those of backup account. + + multisigPub := multisigInfo.GetPubKey().(multisig.PubKeyMultisigThreshold) + multisigAddress := multisigInfo.GetAddress() + multisigSig := multisig.NewMultisig(len(multisigPub.PubKeys)) + + var msg std.Msg + // replace creator to backup key address + for i := 0; i < len(unsignedTx.Msgs); i++ { + //TODO: we need to refactor this. we should not check caller and creator for every messages. + // Is caller's address of a MsgCall also signers or should be address of another smart contract? + + msg = unsignedTx.Msgs[i] + + switch msg.(type) { + + case vm.MsgAddPackage: + + m, ok := msg.(vm.MsgAddPackage) + if !ok { + + return signedTx, err + + } + + m.Creator = multisigAddress + msg = m + + case vm.MsgCall: + + m, ok := msg.(vm.MsgCall) + if !ok { + + return signedTx, err + + } + + m.Caller = multisigAddress + msg = m + + default: + + return signedTx, fmt.Errorf("Msg type T% is not supported", msg) + + } + + unsignedTx.Msgs[i] = msg + + } + + signbz := unsignedTx.GetSignBytes(signerInfo.ChainId, signerInfo.AccountNumber, signerInfo.Sequence) + + primarySig, err := primaryPriv.Sign(signbz) + + if err != nil { + return + } + + backupSig, backupPub, err := signBackup(primaryPriv, b, name, passPhrase, signbz) + if err != nil { + + return + } + + err = multisigSig.AddSignatureFromPubKey(primarySig, primaryPub, multisigPub.PubKeys) + if err != nil { + + return + } + err = multisigSig.AddSignatureFromPubKey(backupSig, backupPub, multisigPub.PubKeys) + if err != nil { + + return + } + + newStdSig := std.Signature{Signature: amino.MustMarshal(multisigSig), PubKey: multisigPub} + + signedTx = std.Tx{ + Msgs: unsignedTx.GetMsgs(), + Fee: unsignedTx.Fee, + Signatures: []std.Signature{newStdSig}, + Memo: unsignedTx.GetMemo(), + } + + return + +} + +/* +// Verify verifies the msg signed by primaryKey and backupKey multisig +func Verify(kbBk Keybase, name string, msg []byte, sig []byte) (err error) { + +} +*/ diff --git a/pkgs/crypto/keys/backup_keybase_test.go b/pkgs/crypto/keys/backup_keybase_test.go new file mode 100644 index 00000000000..685c76062b9 --- /dev/null +++ b/pkgs/crypto/keys/backup_keybase_test.go @@ -0,0 +1,58 @@ +package keys + +import ( + + "testing" + + "github.com/gnolang/gno/pkgs/crypto/keys/armor" + "github.com/stretchr/testify/assert" +) + +const key_name = "test1" +const test1_passcode = "test1rocks" + +const test1_mnemonic = "source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast" +const primary_pubkey = "gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pq0skzdkmzu0r9h6gny6eg8c9dc303xrrudee6z4he4y7cs5rnjwmyf40yaj" + +const test1bk_mnemonic = "curious syrup memory cabbage razor emotion ketchup best alley cotton enjoy nature furnace shallow donor oval tornado razor clock roof pave enroll solar wrist" +const backup_pubkey = "gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zp7xtkykvttvcxnz9n74hfd8t4tav3t7l33p5trvyeuxd3ea8d95vhp767p" + +func TestBackupAccount(t *testing.T) { + + kb := NewInMemory() + kbBk := NewInMemory() + + primaryInfo, err := kb.CreateAccount( + key_name, + test1_mnemonic, + "", test1_passcode, 0, 1) + + assert.NoError(t, err, "create primary account failed") + + p, ok := primaryInfo.(*localInfo) + + assert.True(t, ok, "primaryInfo should be localInfo") + assert.NotNil(t, p, "primaryInfo should not be nil") + + primaryPrivKey, err := armor.UnarmorDecryptPrivKey(p.PrivKeyArmor, test1_passcode) + + + assert.NoError(t, err, "read primary localInfo.PrivKeyArmor failed") + + assert.NotNil(t, primaryPrivKey, "primaryPrivKey should not be nil") + + + info, err := BackupAccount(primaryPrivKey, kbBk, key_name, test1_mnemonic, "", test1_passcode, 0, 1) + assert.NoError(t, err, "creating backup info failed") + + assert.Equal(t, primaryInfo.GetName(), info.GetName(), "Names are equal") + + infoBackup, ok := info.(*infoBk) + assert.True(t, ok, "info should be infoBk") + assert.NotNil(t, p, "info should not be nil") + + err = verifyBkInfo(*infoBackup, primaryPrivKey) + + assert.NotNil(t, primaryPrivKey, "BkInfo is not corrected created") + +} diff --git a/pkgs/crypto/keys/package.go b/pkgs/crypto/keys/package.go index 688086d0724..d3bd92d0334 100644 --- a/pkgs/crypto/keys/package.go +++ b/pkgs/crypto/keys/package.go @@ -13,4 +13,5 @@ var Package = amino.RegisterPackage(amino.NewPackage( ledgerInfo{}, "LedgerInfo", offlineInfo{}, "OfflineInfo", multiInfo{}, "MultiInfo", + infoBk{}, "InfoBk", ))