Skip to content

Commit a9335e4

Browse files
committed
Merge #16546: External signer support - Wallet Box edition
f75e0c1 doc: add external-signer.md (Sjors Provoost) d4b0107 rpc: send: support external signer (Sjors Provoost) 245b445 rpc: signerdisplayaddress (Sjors Provoost) 7ebc7c0 wallet: ExternalSigner: add GetDescriptors method (Sjors Provoost) fc5da52 wallet: add GetExternalSigner() (Sjors Provoost) 259f52c test: external_signer wallet flag is immutable (Sjors Provoost) 2655197 rpc: add external_signer option to createwallet (Sjors Provoost) 2700f09 rpc: signer: add enumeratesigners to list external signers (Sjors Provoost) 07b7c94 rpc: add external signer RPC files (Sjors Provoost) 8ce7767 wallet: add ExternalSignerScriptPubKeyMan (Sjors Provoost) 157ea7c wallet: add external_signer flag (Sjors Provoost) f3e6ce7 test: add external signer test (Sjors Provoost) 8cf543f wallet: add -signer argument for external signer command (Sjors Provoost) f7eb7ec test: framework: add skip_if_no_external_signer (Sjors Provoost) 87a9794 configure: add --enable-external-signer (Sjors Provoost) Pull request description: Big picture overview in [this gist](https://gist.github.com/Sjors/29d06728c685e6182828c1ce9b74483d). This PR lets `bitcoind` 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 transaction (using PSBT under the hood). 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](https://github.com/bitcoin-core/HWI/blob/master/docs/bitcoin-core-usage.md). Usage is documented in [doc/external-signer.md]( https://github.com/Sjors/bitcoin/blob/2019/08/hww-box2/doc/external-signer.md), which also describes what protocol a different signer binary should conform to. Use `--enable-external-signer` to opt in, requires Boost::Process: ``` Options used to compile and link: with wallet = yes with gui / qt = no external signer = yes ``` It adds the following RPC methods: * `enumeratesigners`: asks <cmd> for a list of signers (e.g. devices) and their master key fingerprint * `signerdisplayaddress <address>`: asks <cmd> to display an address It enhances the following RPC methods: * `createwallet`: takes an additional `external_signer` argument and fetches keys from device * `send`: automatically sends transaction to device and waits Usage TL&DR: * clone HWI repo somewhere and launch `bitcoind -signer=../HWI/hwi.py` * check if you can see your hardware device: `bitcoin-cli enumeratesigners` * create wallet and auto import keys `bitcoin-cli createwallet "hww" true true "" true true true` * display address on device: `bitcoin-cli signerdisplayaddress ...` * to spend, use `send` RPC and approve transaction on device Prerequisites: - [x] #21127 load wallet flags before everything else - [x] #21182 remove mostly pointless BOOST_PROCESS macro Potentially useful followups: - GUI support: bitcoin-core#4 - bumpfee support - (automatically) verify (a subset of) keys on the device after import, through message signing ACKs for top commit: laanwj: re-ACK f75e0c1 Tree-SHA512: 7db8afd54762295c1424c3f01d8c587ec256a72f34bd5256e04b21832dabd5dc212be8ab975ae3b67de75259fd569a561491945750492f417111dc7b6641e77f
2 parents 78effb3 + f75e0c1 commit a9335e4

33 files changed

+1183
-84
lines changed

build_msvc/bitcoin_config.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@
5050
/* define if the Boost::Filesystem library is available */
5151
#define HAVE_BOOST_FILESYSTEM /**/
5252

53-
/* define if the Boost::Process library is available */
54-
#define HAVE_BOOST_PROCESS /**/
53+
/* define if external signer support is enabled (requires Boost::Process) */
54+
#define ENABLE_EXTERNAL_SIGNER /**/
5555

5656
/* define if the Boost::System library is available */
5757
#define HAVE_BOOST_SYSTEM /**/

configure.ac

Lines changed: 44 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -338,10 +338,10 @@ AC_ARG_ENABLE([werror],
338338
[enable_werror=$enableval],
339339
[enable_werror=no])
340340

341-
AC_ARG_WITH([boost-process],
342-
[AS_HELP_STRING([--with-boost-process],[Opt in to using Boost Process (default is no)])],
343-
[boost_process=$withval],
344-
[boost_process=no])
341+
AC_ARG_ENABLE([external-signer],
342+
[AS_HELP_STRING([--enable-external-signer],[compile external signer support (default is no, requires Boost::Process)])],
343+
[use_external_signer=$enableval],
344+
[use_external_signer=no])
345345

346346
AC_LANG_PUSH([C++])
347347

@@ -1253,6 +1253,7 @@ if test "x$enable_fuzz" = "xyes"; then
12531253
bitcoin_enable_qt_dbus=no
12541254
enable_wallet=no
12551255
use_bench=no
1256+
use_external_signer=no
12561257
use_upnp=no
12571258
use_natpmp=no
12581259
use_zmq=no
@@ -1390,16 +1391,20 @@ fi
13901391
AX_BOOST_SYSTEM
13911392
AX_BOOST_FILESYSTEM
13921393

1393-
dnl Opt-in to Boost Process
1394-
if test "x$boost_process" != xno; then
1394+
dnl Opt-in to Boost Process if external signer support is requested
1395+
if test "x$use_external_signer" != xno; then
13951396
AC_MSG_CHECKING(for Boost Process)
13961397
AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[#include <boost/process.hpp>]],
13971398
[[ boost::process::child* child = new boost::process::child; delete child; ]])],
1398-
[ AC_MSG_RESULT(yes); AC_DEFINE([HAVE_BOOST_PROCESS],,[define if Boost::Process is available])],
1399-
[ AC_MSG_ERROR([Boost::Process is not available!])]
1399+
[ AC_MSG_RESULT(yes)
1400+
AC_DEFINE([ENABLE_EXTERNAL_SIGNER],,[define if external signer support is enabled])
1401+
],
1402+
[ AC_MSG_ERROR([Boost::Process is required for external signer support, but not available!])]
14001403
)
14011404
fi
14021405

1406+
AM_CONDITIONAL([ENABLE_EXTERNAL_SIGNER], [test "x$use_external_signer" = "xyes"])
1407+
14031408
if test x$suppress_external_warnings != xno; then
14041409
BOOST_CPPFLAGS=SUPPRESS_WARNINGS($BOOST_CPPFLAGS)
14051410
fi
@@ -1810,6 +1815,7 @@ AC_SUBST(ARM_CRC_CXXFLAGS)
18101815
AC_SUBST(LIBTOOL_APP_LDFLAGS)
18111816
AC_SUBST(USE_SQLITE)
18121817
AC_SUBST(USE_BDB)
1818+
AC_SUBST(ENABLE_EXTERNAL_SIGNER)
18131819
AC_SUBST(USE_UPNP)
18141820
AC_SUBST(USE_QRCODE)
18151821
AC_SUBST(BOOST_LIBS)
@@ -1885,43 +1891,43 @@ esac
18851891

18861892
echo
18871893
echo "Options used to compile and link:"
1888-
echo " boost process = $with_boost_process"
1889-
echo " multiprocess = $build_multiprocess"
1890-
echo " with libs = $build_bitcoin_libs"
1891-
echo " with wallet = $enable_wallet"
1894+
echo " external signer = $use_external_signer"
1895+
echo " multiprocess = $build_multiprocess"
1896+
echo " with libs = $build_bitcoin_libs"
1897+
echo " with wallet = $enable_wallet"
18921898
if test "x$enable_wallet" != "xno"; then
1893-
echo " with sqlite = $use_sqlite"
1894-
echo " with bdb = $use_bdb"
1899+
echo " with sqlite = $use_sqlite"
1900+
echo " with bdb = $use_bdb"
18951901
fi
1896-
echo " with gui / qt = $bitcoin_enable_qt"
1902+
echo " with gui / qt = $bitcoin_enable_qt"
18971903
if test x$bitcoin_enable_qt != xno; then
1898-
echo " with qr = $use_qr"
1904+
echo " with qr = $use_qr"
18991905
fi
1900-
echo " with zmq = $use_zmq"
1906+
echo " with zmq = $use_zmq"
19011907
if test x$enable_fuzz == xno; then
1902-
echo " with test = $use_tests"
1908+
echo " with test = $use_tests"
19031909
else
1904-
echo " with test = not building test_bitcoin because fuzzing is enabled"
1905-
echo " with fuzz = $enable_fuzz"
1910+
echo " with test = not building test_bitcoin because fuzzing is enabled"
1911+
echo " with fuzz = $enable_fuzz"
19061912
fi
1907-
echo " with bench = $use_bench"
1908-
echo " with upnp = $use_upnp"
1909-
echo " with natpmp = $use_natpmp"
1910-
echo " use asm = $use_asm"
1911-
echo " ebpf tracing = $have_sdt"
1912-
echo " sanitizers = $use_sanitizers"
1913-
echo " debug enabled = $enable_debug"
1914-
echo " gprof enabled = $enable_gprof"
1915-
echo " werror = $enable_werror"
1913+
echo " with bench = $use_bench"
1914+
echo " with upnp = $use_upnp"
1915+
echo " with natpmp = $use_natpmp"
1916+
echo " use asm = $use_asm"
1917+
echo " ebpf tracing = $have_sdt"
1918+
echo " sanitizers = $use_sanitizers"
1919+
echo " debug enabled = $enable_debug"
1920+
echo " gprof enabled = $enable_gprof"
1921+
echo " werror = $enable_werror"
19161922
echo
1917-
echo " target os = $TARGET_OS"
1918-
echo " build os = $build_os"
1923+
echo " target os = $TARGET_OS"
1924+
echo " build os = $build_os"
19191925
echo
1920-
echo " CC = $CC"
1921-
echo " CFLAGS = $PTHREAD_CFLAGS $CFLAGS"
1922-
echo " CPPFLAGS = $DEBUG_CPPFLAGS $HARDENED_CPPFLAGS $CPPFLAGS"
1923-
echo " CXX = $CXX"
1924-
echo " CXXFLAGS = $DEBUG_CXXFLAGS $HARDENED_CXXFLAGS $WARN_CXXFLAGS $NOWARN_CXXFLAGS $ERROR_CXXFLAGS $GPROF_CXXFLAGS $CXXFLAGS"
1925-
echo " LDFLAGS = $PTHREAD_LIBS $HARDENED_LDFLAGS $GPROF_LDFLAGS $LDFLAGS"
1926-
echo " ARFLAGS = $ARFLAGS"
1926+
echo " CC = $CC"
1927+
echo " CFLAGS = $PTHREAD_CFLAGS $CFLAGS"
1928+
echo " CPPFLAGS = $DEBUG_CPPFLAGS $HARDENED_CPPFLAGS $CPPFLAGS"
1929+
echo " CXX = $CXX"
1930+
echo " CXXFLAGS = $DEBUG_CXXFLAGS $HARDENED_CXXFLAGS $WARN_CXXFLAGS $NOWARN_CXXFLAGS $ERROR_CXXFLAGS $GPROF_CXXFLAGS $CXXFLAGS"
1931+
echo " LDFLAGS = $PTHREAD_LIBS $HARDENED_LDFLAGS $GPROF_LDFLAGS $LDFLAGS"
1932+
echo " ARFLAGS = $ARFLAGS"
19271933
echo

doc/Doxyfile.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2073,7 +2073,7 @@ INCLUDE_FILE_PATTERNS =
20732073
# recursively expanded use the := operator instead of the = operator.
20742074
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
20752075

2076-
PREDEFINED = HAVE_BOOST_PROCESS
2076+
PREDEFINED = ENABLE_EXTERNAL_SIGNER
20772077

20782078
# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
20792079
# tag can be used to specify a list of macro names that should be expanded. The

doc/external-signer.md

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# Support for signing transactions outside of Bitcoin Core
2+
3+
Bitcoin Core can be launched with `-signer=<cmd>` where `<cmd>` is an external tool which can sign transactions and perform other functions. For example, it can be used to communicate with a hardware wallet.
4+
5+
## Example usage
6+
7+
The following example is based on the [HWI](https://github.com/bitcoin-core/HWI) tool. Although this tool is hosted under the Bitcoin Core GitHub organization and maintained by Bitcoin Core developers, it should be used with caution. It is considered experimental and has far less review than Bitcoin Core itself. Be particularly careful when running tools such as these on a computer with private keys on it.
8+
9+
When using a hardware wallet, consult the manufacturer website for (alternative) software they recommend. As long as their software conforms to the standard below, it should be able to work with Bitcoin Core.
10+
11+
Start Bitcoin Core:
12+
13+
```sh
14+
$ bitcoind -signer=../HWI/hwi.py
15+
```
16+
17+
### Device setup
18+
19+
Follow the hardware manufacturers instructions for the initial device setup, as well as their instructions for creating a backup. Alternatively, for some devices, you can use the `setup`, `restore` and `backup` commands provided by [HWI](https://github.com/bitcoin-core/HWI).
20+
21+
### Create wallet and import keys
22+
23+
Get a list of signing devices / services:
24+
25+
```
26+
$ bitcoin-cli enumeratesigners
27+
{
28+
"signers": [
29+
{
30+
"fingerprint": "c8df832a"
31+
}
32+
]
33+
```
34+
35+
The master key fingerprint is used to identify a device.
36+
37+
Create a wallet, this automatically imports the public keys:
38+
39+
```sh
40+
$ bitcoin-cli createwallet "hww" true true "" true true true
41+
```
42+
43+
### Verify an address
44+
45+
Display an address on the device:
46+
47+
```sh
48+
$ bitcoin-cli -rpcwallet=<wallet> getnewaddress
49+
$ bitcoin-cli -rpcwallet=<wallet> signerdisplayaddress <address>
50+
```
51+
52+
Replace `<address>` with the result of `getnewaddress`.
53+
54+
### Spending
55+
56+
Under the hood this uses a [Partially Signed Bitcoin Transaction](psbt.md).
57+
58+
```sh
59+
$ bitcoin-cli -rpcwallet=<wallet> sendtoaddress <address> <amount>
60+
```
61+
62+
This prompts your hardware wallet to sign, and fail if it's not connected. If successful
63+
it automatically broadcasts the transaction.
64+
65+
```sh
66+
{"complete": true, "txid": <txid>}
67+
```
68+
69+
## Signer API
70+
71+
In order to be compatible with Bitcoin Core any signer command should conform to the specification below. This specification is subject to change. Ideally a BIP should propose a standard so that other wallets can also make use of it.
72+
73+
Prerequisite knowledge:
74+
* [Output Descriptors](descriptors.md)
75+
* Partially Signed Bitcoin Transaction ([PSBT](psbt.md))
76+
77+
### `enumerate` (required)
78+
79+
Usage:
80+
```
81+
$ <cmd> enumerate
82+
[
83+
{
84+
"fingerprint": "00000000"
85+
}
86+
]
87+
```
88+
89+
The command MUST return an (empty) array with at least a `fingerprint` field.
90+
91+
A future extension could add an optional return field with device capabilities. Perhaps a descriptor with wildcards. For example: `["pkh("44'/0'/$'/{0,1}/*"), sh(wpkh("49'/0'/$'/{0,1}/*")), wpkh("84'/0'/$'/{0,1}/*")]`. This would indicate the device supports legacy, wrapped SegWit and native SegWit. In addition it restricts the derivation paths that can used for those, to maintain compatibility with other wallet software. It also indicates the device, or the driver, doesn't support multisig.
92+
93+
A future extension could add an optional return field `reachable`, in case `<cmd>` knows a signer exists but can't currently reach it.
94+
95+
### `signtransaction` (required)
96+
97+
Usage:
98+
```
99+
$ <cmd> --fingerprint=<fingerprint> (--testnet) signtransaction <psbt>
100+
base64_encode_signed_psbt
101+
```
102+
103+
The command returns a psbt with any signatures.
104+
105+
The `psbt` SHOULD include bip32 derivations. The command SHOULD fail if none of the bip32 derivations match a key owned by the device.
106+
107+
The command SHOULD fail if the user cancels.
108+
109+
The command MAY complain if `--testnet` is set, but any of the BIP32 derivation paths contain a coin type other than `1h` (and vice versa).
110+
111+
### `getdescriptors` (optional)
112+
113+
Usage:
114+
115+
```
116+
$ <cmd> --fingerprint=<fingerprint> (--testnet) getdescriptors <account>
117+
<xpub>
118+
```
119+
120+
Returns descriptors supported by the device. Example:
121+
122+
```
123+
$ <cmd> --fingerprint=00000000 --testnet getdescriptors
124+
{
125+
"receive": [
126+
"pkh([00000000/44h/0h/0h]xpub6C.../0/*)#fn95jwmg",
127+
"sh(wpkh([00000000/49h/0h/0h]xpub6B..../0/*))#j4r9hntt",
128+
"wpkh([00000000/84h/0h/0h]xpub6C.../0/*)#qw72dxa9"
129+
],
130+
"internal": [
131+
"pkh([00000000/44h/0h/0h]xpub6C.../1/*)#c8q40mts",
132+
"sh(wpkh([00000000/49h/0h/0h]xpub6B..../1/*))#85dn0v75",
133+
"wpkh([00000000/84h/0h/0h]xpub6C..../1/*)#36mtsnda"
134+
]
135+
}
136+
```
137+
138+
### `displayaddress` (optional)
139+
140+
Usage:
141+
```
142+
<cmd> --fingerprint=<fingerprint> (--testnet) displayaddress --desc descriptor
143+
```
144+
145+
Example, display the first native SegWit receive address on Testnet:
146+
147+
```
148+
<cmd> --fingerprint=00000000 --testnet displayaddress --desc "wpkh([00000000/84h/1h/0h]tpubDDUZ..../0/0)"
149+
```
150+
151+
The command MUST be able to figure out the address type from the descriptor.
152+
153+
If <descriptor> contains a master key fingerprint, the command MUST fail if it does not match the fingerprint known by the device.
154+
155+
If <descriptor> contains an xpub, the command MUST fail if it does not match the xpub known by the device.
156+
157+
The command MAY complain if `--testnet` is set, but the BIP32 coin type is not `1h` (and vice versa).
158+
159+
## How Bitcoin Core uses the Signer API
160+
161+
The `enumeratesigners` RPC simply calls `<cmd> enumerate`.
162+
163+
The `createwallet` RPC calls:
164+
165+
* `<cmd> --fingerprint=00000000 getdescriptors 0`
166+
167+
It then imports descriptors for all support address types, in a BIP44/49/84 compatible manner.
168+
169+
The `displayaddress` RPC reuses some code from `getaddressinfo` on the provided address and obtains the inferred descriptor. It then calls `<cmd> --fingerprint=00000000 displayaddress --desc=<descriptor>`.
170+
171+
`sendtoaddress` and `sendmany` check `inputs->bip32_derivs` to see if any inputs have the same `master_fingerprint` as the signer. If so, it calls `<cmd> --fingerprint=00000000 signtransaction <psbt>`. It waits for the device to return a (partially) signed psbt, tries to finalize it and broadcasts the transation.

src/Makefile.am

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,10 +265,13 @@ BITCOIN_CORE_H = \
265265
wallet/crypter.h \
266266
wallet/db.h \
267267
wallet/dump.h \
268+
wallet/external_signer.h \
269+
wallet/external_signer_scriptpubkeyman.h \
268270
wallet/feebumper.h \
269271
wallet/fees.h \
270272
wallet/ismine.h \
271273
wallet/load.h \
274+
wallet/rpcsigner.h \
272275
wallet/rpcwallet.h \
273276
wallet/salvage.h \
274277
wallet/scriptpubkeyman.h \
@@ -379,11 +382,14 @@ libbitcoin_wallet_a_SOURCES = \
379382
wallet/crypter.cpp \
380383
wallet/db.cpp \
381384
wallet/dump.cpp \
385+
wallet/external_signer_scriptpubkeyman.cpp \
386+
wallet/external_signer.cpp \
382387
wallet/feebumper.cpp \
383388
wallet/fees.cpp \
384389
wallet/interfaces.cpp \
385390
wallet/load.cpp \
386391
wallet/rpcdump.cpp \
392+
wallet/rpcsigner.cpp \
387393
wallet/rpcwallet.cpp \
388394
wallet/scriptpubkeyman.cpp \
389395
wallet/wallet.cpp \

src/dummywallet.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ void DummyWalletInit::AddWalletOptions(ArgsManager& argsman) const
3838
"-paytxfee=<amt>",
3939
"-rescan",
4040
"-salvagewallet",
41+
"-signer=<cmd>",
4142
"-spendzeroconfchange",
4243
"-txconfirmtarget=<n>",
4344
"-wallet=<path>",

src/rpc/client.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
183183
{ "createwallet", 4, "avoid_reuse"},
184184
{ "createwallet", 5, "descriptors"},
185185
{ "createwallet", 6, "load_on_startup"},
186+
{ "createwallet", 7, "external_signer"},
186187
{ "loadwallet", 1, "load_on_startup"},
187188
{ "unloadwallet", 1, "load_on_startup"},
188189
{ "getnodeaddresses", 0, "count"},

0 commit comments

Comments
 (0)