-
Notifications
You must be signed in to change notification settings - Fork 37.8k
[WIP] External signer support (e.g. hardware wallet) #14912
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
e021812
to
a2b018d
Compare
Now that #14491 has been rebased, the See also the wallet meeting notes. |
a2b018d
to
9ec4aac
Compare
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. ConflictsReviewers, this pull request conflicts with the following ones:
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. |
std::string fingerprintStr = fingerprint.get_str(); | ||
// Skip duplicate signer | ||
bool duplicate = false; | ||
for (ExternalSigner signer : signers) { |
There was a problem hiding this comment.
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
:-)
src/wallet/rpcdump.cpp
Outdated
UniValue importdata(UniValue::VARR); | ||
|
||
uint64_t keypool_target_size = 0; | ||
keypool_target_size = gArgs.GetArg("-keypool", DEFAULT_KEYPOOL_SIZE); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
src/util/system.cpp
Outdated
{ | ||
if (strCommand.empty()) return UniValue::VNULL; | ||
|
||
std::array<char, 128> buffer; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initialize to zero?
src/wallet/rpcwallet.cpp
Outdated
ExternalSigner *signer = GetSignerForJSONRPCRequest(request, 4, pwallet); | ||
if (signer && !bip32derivs) JSONRPCError(RPC_WALLET_ERROR, "Using signer requires bip32derivs argument to be true"); | ||
|
||
// Check if signer fingerpint matches any input master key fingerprint |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be "fingerprint" :-)
src/util/system.cpp
Outdated
|
||
std::array<char, 128> buffer; | ||
std::string result; | ||
std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(strCommand.c_str(), "r"), pclose); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/util/strencodings.cpp
Outdated
return ret; | ||
} | ||
|
||
std::string WriteHdKeypath(const std::vector<uint32_t> keypath) |
There was a problem hiding this comment.
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?
src/wallet/rpcwallet.cpp
Outdated
} | ||
|
||
const std::string command = gArgs.GetArg("-signer", DEFAULT_EXTERNAL_SIGNER); | ||
if (command == "") throw JSONRPCError(RPC_WALLET_ERROR, "Error: restart bitcoind with -signer=<cmd>"); |
There was a problem hiding this comment.
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()
? :-)
const UniValue result = runCommandParseJSON(command + " enumerate"); | ||
if (!result.isArray()) | ||
throw ExternalSignerException(strprintf("'%s' received invalid response, expected array of signers", command)); | ||
for (UniValue signer : result.getValues()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be const reference? :-)
const UniValue& fingerprint = find_value(signer, "fingerprint"); | ||
if (result.isNull()) | ||
throw ExternalSignerException(strprintf("'%s' received invalid response, missing signer fingerprint", command)); | ||
std::string fingerprintStr = fingerprint.get_str(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be const reference? :-)
// Skip duplicate signer | ||
bool duplicate = false; | ||
for (ExternalSigner signer : signers) { | ||
if (signer.m_fingerprint.compare(fingerprintStr) == 0) duplicate = true; |
There was a problem hiding this comment.
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
:-)
src/wallet/rpcwallet.cpp
Outdated
for (unsigned int i = 0; i < psbtx.inputs.size(); ++i) { | ||
const PSBTInput& input = psbtx.inputs[i]; | ||
for (auto entry : input.hd_keypaths) { | ||
if (signer->m_fingerprint == strprintf("%08x", ReadBE32(entry.second.fingerprint))) match = true; |
There was a problem hiding this comment.
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?
src/wallet/rpcwallet.cpp
Outdated
if (!match) JSONRPCError(RPC_WALLET_ERROR, "Signer fingerprint does not match any of the inputs"); | ||
|
||
// Call signer, get result | ||
const UniValue signer_result = signer->signTransaction(request.params[0].get_str()); |
There was a problem hiding this comment.
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?
src/util/strencodings.h
Outdated
@@ -204,6 +204,10 @@ bool ConvertBits(const O& outfn, I it, I end) { | |||
/** Parse an HD keypaths like "m/7/0'/2000". */ | |||
NODISCARD bool ParseHDKeypath(const std::string& keypath_str, std::vector<uint32_t>& keypath); | |||
|
|||
/** Write HD keypaths as strings */ | |||
std::string WriteHdKeypath(const std::vector<uint32_t> keypath); |
There was a problem hiding this comment.
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?
src/wallet/rpcdump.cpp
Outdated
UniValue receive_descriptors = signer->getKeys(receive_desc); | ||
if (!receive_descriptors.isArray()) JSONRPCError(RPC_WALLET_ERROR, "Expected an array of receive descriptors"); | ||
for (const UniValue& descriptor : receive_descriptors.getValues()) { | ||
descriptors.push_back(descriptor); |
There was a problem hiding this comment.
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?
src/wallet/rpcdump.cpp
Outdated
UniValue change_descriptors = signer->getKeys(change_desc); | ||
if (!change_descriptors.isArray()) JSONRPCError(RPC_WALLET_ERROR, "Expected an array of change descriptors"); | ||
for (const UniValue& descriptor : change_descriptors.getValues()) { | ||
descriptors.push_back(descriptor); |
There was a problem hiding this comment.
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
?
src/wallet/rpcdump.cpp
Outdated
std::string desc_prefix = ""; | ||
std::string desc_suffix = ""; | ||
std::string purpose = ""; | ||
switch(pwallet->m_default_address_type) { |
There was a problem hiding this comment.
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
:-)
src/wallet/rpcdump.cpp
Outdated
} | ||
|
||
|
||
switch(pwallet->m_default_change_type) { |
There was a problem hiding this comment.
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 (
.
Create basic ExternalSigner class with contructor. A Signer(<cmd>) is added to CWallet on load if -signer=<cmd> is set.
c739648
to
157b8f2
Compare
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. |
157b8f2
to
3ba77e9
Compare
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 |
Includes a mock to mimick the HWI interace.
3ba77e9
to
264425b
Compare
Closing in favor of the native descriptor edition in #16546. |
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 fingerprintsignerfetchkeys
(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 addresssignerprocesspsbt <psbt>
to send the<psbt>
to<cmd>
to sign and wait for the resultUsage TL&DR:
bitcoind -signer=../HWI/hwi.py
bitcoin-cli createwallet hww true
bitcoin-cli enumeratesigners
bitcoin-cli -rpcwallet=hww signerfetchkeys
For easier review, this builds on the following PRs:
Potentially useful followups: