Skip to content

[WIP] External signer support (e.g. hardware wallet)#14912

Closed
Sjors wants to merge 21 commits intobitcoin:masterfrom
Sjors:2018/11/rpc-signer
Closed

[WIP] External signer support (e.g. hardware wallet)#14912
Sjors wants to merge 21 commits intobitcoin:masterfrom
Sjors:2018/11/rpc-signer

Conversation

@Sjors
Copy link
Member

@Sjors Sjors commented Dec 10, 2018

This PR lets bitcoind to call an arbitrary command -signer=<cmd>, e.g. a hardware wallet driver, where it can fetch public keys, ask to display an address, and sign a PSBT.

It's design to work with https://github.com/bitcoin-core/HWI, which supports multiple hardware wallets. Any command with the same arguments and return values will work. It simplifies the manual procedure described here.

Usage is documented in doc/external-signer.md, which also describes what protocol a different signer binary should conform to.

It adds the following RPC methods:

  • enumeratesigners: asks for a list of signers (e.g. devices) and their master key fingerprint
  • signerfetchkeys (needs Add getdescriptors command bitcoin-core/HWI#137): asks for descriptors and then fills the keypool (no private keys)
  • signerdisplayaddress <address>: asks to display an address
  • signerprocesspsbt <psbt> to send the <psbt> to <cmd> to sign and wait for the result

Usage TL&DR:

  • clone HWI repo somewhere and launch bitcoind -signer=../HWI/hwi.py
  • create wallet without private keys: bitcoin-cli createwallet hww true
  • list hardware devices: bitcoin-cli enumeratesigners
  • fetch keys from hardware device into the wallet: bitcoin-cli -rpcwallet=hww signerfetchkeys
  • display address on device, sign transaction: see doc/external-signer.md

For easier review, this builds on the following PRs:

Potentially useful followups:

@Sjors
Copy link
Member Author

Sjors commented Dec 15, 2018

Now that #14491 has been rebased, the hww branch I'm building off should also soon be rebased. At that point I can rebase and make Travis happy. In addition, I'll be able to leverage #14646 to clean up my descriptor related code (I'm currently using string concatenation to build descriptors). I have a few other spring cleaning items on my todo list too.

See also the wallet meeting notes.

@DrahtBot
Copy link
Contributor

DrahtBot commented Dec 17, 2018

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #16542 (Return more specific errors about invalid descriptors by achow101)
  • #16539 ([wallet] lower -txmaxfee default from 0.1 to 0.01 BTC by Sjors)
  • #16528 ([WIP] Native Descriptor Wallets (take 2) by achow101)
  • #16365 (Log RPC parameters (arguments) if -debug=rpcparams by LarryRuane)
  • #16341 (Introduce ScriptPubKeyMan interface and use it for key and script management (aka wallet boxes) by achow101)
  • #16273 (refactor: Remove unused includes by practicalswift)
  • #15876 ([rpc] signer send and fee bump convenience methods by Sjors)
  • #15529 (Add Qt programs to msvc build (updated, no code changes) by sipsorcery)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExternalSigner signer shadows UniValue signer :-)

Copy link
Contributor

@practicalswift practicalswift Dec 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make signedness changing implicit conversion explicit? Also, remove redundant initialization to zero on the line above? Merge the two lines.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The compiler didn't like it when I initialized using gArgs.GetArg.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initialize to zero?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be "fingerprint" :-)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't reviewed this use of popen(…) closer, but please note the recommendations/risks with regards to popen(…) used described in the CERT secure coding guidelines (more specifically rule ENV33-C).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is straight from stack overflow, so certainly needs more review... I'm surprised how complicated it is to just read some JSON from stdout.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

popen and pclose are unix-like only. You can't use them on Windows. IMO you could use boost process instead.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we don't depend on boost process.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know. But this is the simplest way if this is really necessary to call another process. I don't think that any one here know how to use Windows CreateProcess API.

Copy link
Contributor

@ryanofsky ryanofsky Jan 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be reasonable to call _popen function here on windows or #define popen _popen: https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/system-wsystem.

You do need to be very careful about special characters passed in the command string, but it should be ok if you stick to hex/base64.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_popen would only work in console program. See https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/popen-wpopen. So it doesn't work in Qt.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keypath should be const reference?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Could use command.empty()? :-)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be const reference? :-)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be const reference? :-)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use string equality operator instead of compare :-)

Copy link
Contributor

@practicalswift practicalswift Dec 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

signer can be nullptr here if the check on L4016 is correct?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here: signer can be nullptr here if the check on L4016 is correct?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keypath should be const reference?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Could use std::copy instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here: Could you std::copy?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing space before (. Consider running new code through clang-format :-)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here: Missing whitespace before (.

@Sjors
Copy link
Member Author

Sjors commented Jun 7, 2019

Rebased and dropped signersend from this PR, moved it to #15876 (which also contains fee bump support).

@Sjors
Copy link
Member Author

Sjors commented Jul 11, 2019

I'm considering rebasing* this on top of #16341 (aka "box"), by introducing a ExternalSignerScriptPubKeyMan. It can be make a bit safer in the process.

  1. move all of externalsigner.{h,cpp} into a ExternalSignerScriptPubKeyMan
  2. Get rid of signerfetchkeys RPC; createwallet and keypoolrefill cover this
  3. SetupGeneration: this would call getdescriptors on the device at wallet creation time. The BIP32 account number could be passed as on option to createwallet. Wallet creation will fail if it can't fetch keys.
  4. TopUp will also call getdescriptors on the device, with a higher range. This will become unecessary with native wallet descriptors. In practice with a keypool size of 1000 this is not a big inconvenience. The current behavior of GetReservedDestination calling TopUp needs to go away.
  5. add a feature flag that this wallet should use ExternalSignerScriptPubKeyMan
  6. store the device fingerprint in the wallet metadata at creation time, so enumeratesigners is unnecessary after wallet creation
  7. allow only a single OutputType, store it and refuse getnewaddress for different types. This is necessary to remain compatible with BIP44/49/84, which requires a different derivation path per output type. This can be relaxed with native descriptor wallets, see also Descriptor: add GetAddressType() and IsSegWit() #15590.
  8. The current flow of calling enumeratesigners on an existing wallet won't do anymore, because we already need to know the signer at createwallet time. Instead enumeratesigners could work without a wallet and not store the result anywhere. When creating a wallet it will just use the first available result from enumeratesigners unless a fingerprint option is provided.
  9. signerdisplayaddress remains unchanged
  10. signerprocesspsbt can be melted into processpsbt thanks to the feature flag

* = later though, I'd rather not have a PR with 110 commits

@Sjors
Copy link
Member Author

Sjors commented Aug 2, 2019

Rebased now that #15911 is merged. Dropped a few commits that should have been in #15876 and aren't needed anyway after #15713. Removed the need for the unit test to add private keys to the keypool (closing #15414).

I'll keep this and the convenience methods in #15876 up to date and work on a "boxed" version in a separate PR.

@Sjors
Copy link
Member Author

Sjors commented Aug 3, 2019

Here's a simple rebase on top of a native descriptor wallet #16528 (benefit: gives access to full BIP44/49/84 address tree): https://github.com/Sjors/bitcoin/tree/2019/08/hww-box

@Sjors
Copy link
Member Author

Sjors commented Aug 4, 2019

Closing in favor of the native descriptor edition in #16546.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.