From 2700f09c4130af6167ce71f46960e92ca800e205 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Fri, 15 Feb 2019 12:54:29 +0100 Subject: rpc: signer: add enumeratesigners to list external signers --- src/wallet/external_signer.cpp | 54 ++++++++++++++++++++++++++++++++++-- src/wallet/external_signer.h | 23 +++++++++++++++- src/wallet/rpcsigner.cpp | 59 +++++++++++++++++++++++++++++++--------- test/functional/mocks/signer.py | 6 ++++ test/functional/wallet_signer.py | 31 +++++++++++++++++++++ 5 files changed, 157 insertions(+), 16 deletions(-) diff --git a/src/wallet/external_signer.cpp b/src/wallet/external_signer.cpp index 6f850e4779..c06b178bc1 100644 --- a/src/wallet/external_signer.cpp +++ b/src/wallet/external_signer.cpp @@ -2,7 +2,57 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include #include -#include -ExternalSigner::ExternalSigner(const std::string& command, const std::string& fingerprint): m_command(command), m_fingerprint(fingerprint) {} +ExternalSigner::ExternalSigner(const std::string& command, const std::string& fingerprint, std::string chain, std::string name): m_command(command), m_fingerprint(fingerprint), m_chain(chain), m_name(name) {} + +const std::string ExternalSigner::NetworkArg() const +{ + return " --chain " + m_chain; +} + +#ifdef ENABLE_EXTERNAL_SIGNER + +bool ExternalSigner::Enumerate(const std::string& command, std::vector& signers, std::string chain, bool ignore_errors) +{ + // Call enumerate + const UniValue result = RunCommandParseJSON(command + " enumerate"); + if (!result.isArray()) { + if (ignore_errors) return false; + throw ExternalSignerException(strprintf("'%s' received invalid response, expected array of signers", command)); + } + for (UniValue signer : result.getValues()) { + // Check for error + const UniValue& error = find_value(signer, "error"); + if (!error.isNull()) { + if (ignore_errors) return false; + if (!error.isStr()) { + throw ExternalSignerException(strprintf("'%s' error", command)); + } + throw ExternalSignerException(strprintf("'%s' error: %s", command, error.getValStr())); + } + // Check if fingerprint is present + const UniValue& fingerprint = find_value(signer, "fingerprint"); + if (fingerprint.isNull()) { + if (ignore_errors) return false; + throw ExternalSignerException(strprintf("'%s' received invalid response, missing signer fingerprint", command)); + } + std::string fingerprintStr = fingerprint.get_str(); + // Skip duplicate signer + bool duplicate = false; + for (ExternalSigner signer : signers) { + if (signer.m_fingerprint.compare(fingerprintStr) == 0) duplicate = true; + } + if (duplicate) break; + std::string name = ""; + const UniValue& model_field = find_value(signer, "model"); + if (model_field.isStr() && model_field.getValStr() != "") { + name += model_field.getValStr(); + } + signers.push_back(ExternalSigner(command, fingerprintStr, chain, name)); + } + return true; +} + +#endif diff --git a/src/wallet/external_signer.h b/src/wallet/external_signer.h index 08fb0c9f39..9d32fe1e04 100644 --- a/src/wallet/external_signer.h +++ b/src/wallet/external_signer.h @@ -8,6 +8,7 @@ #include #include #include +#include class ExternalSignerException : public std::runtime_error { public: @@ -25,10 +26,30 @@ private: public: //! @param[in] command the command which handles interaction with the external signer //! @param[in] fingerprint master key fingerprint of the signer - ExternalSigner(const std::string& command, const std::string& fingerprint); + //! @param[in] chain "main", "test", "regtest" or "signet" + //! @param[in] name device name + ExternalSigner(const std::string& command, const std::string& fingerprint, std::string chain, std::string name); //! Master key fingerprint of the signer std::string m_fingerprint; + + //! Bitcoin mainnet, testnet, etc + std::string m_chain; + + //! Name of signer + std::string m_name; + + const std::string NetworkArg() const; + +#ifdef ENABLE_EXTERNAL_SIGNER + //! Obtain a list of signers. Calls ` enumerate`. + //! @param[in] command the command which handles interaction with the external signer + //! @param[in,out] signers vector to which new signers (with a unique master key fingerprint) are added + //! @param chain "main", "test", "regtest" or "signet" + //! @param[out] success Boolean + static bool Enumerate(const std::string& command, std::vector& signers, std::string chain, bool ignore_errors = false); + +#endif }; #endif // BITCOIN_WALLET_EXTERNAL_SIGNER_H diff --git a/src/wallet/rpcsigner.cpp b/src/wallet/rpcsigner.cpp index d2478908df..76f4f3c6aa 100644 --- a/src/wallet/rpcsigner.cpp +++ b/src/wallet/rpcsigner.cpp @@ -2,36 +2,69 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include #include +#include #include #include +#include #include #ifdef ENABLE_EXTERNAL_SIGNER -// CRPCCommand table won't compile with an empty array -static RPCHelpMan dummy() +static RPCHelpMan enumeratesigners() { - return RPCHelpMan{"dummy", - "\nDoes nothing.\n" - "", - {}, - RPCResult{RPCResult::Type::NONE, "", ""}, - RPCExamples{""}, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - return NullUniValue; -}, + return RPCHelpMan{ + "enumeratesigners", + "Returns a list of external signers from -signer.", + {}, + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::ARR, "signers", /* optional */ false, "", + { + {RPCResult::Type::STR_HEX, "masterkeyfingerprint", "Master key fingerprint"}, + {RPCResult::Type::STR, "name", "Device name"}, + }, + } + } + }, + RPCExamples{""}, + [](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { + std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); + if (!wallet) return NullUniValue; + + const std::string command = gArgs.GetArg("-signer", ""); + if (command == "") throw JSONRPCError(RPC_WALLET_ERROR, "Error: restart bitcoind with -signer="); + std::string chain = gArgs.GetChainName(); + UniValue signers_res = UniValue::VARR; + try { + std::vector signers; + ExternalSigner::Enumerate(command, signers, chain); + for (ExternalSigner signer : signers) { + UniValue signer_res = UniValue::VOBJ; + signer_res.pushKV("fingerprint", signer.m_fingerprint); + signer_res.pushKV("name", signer.m_name); + signers_res.push_back(signer_res); + } + } catch (const ExternalSignerException& e) { + throw JSONRPCError(RPC_WALLET_ERROR, e.what()); + } + UniValue result(UniValue::VOBJ); + result.pushKV("signers", signers_res); + return result; + } }; } Span GetSignerRPCCommands() { + // clang-format off static const CRPCCommand commands[] = { // category actor (function) // --------------------- ------------------------ - { "signer", &dummy, }, + { "signer", &enumeratesigners, }, }; // clang-format on return MakeSpan(commands); diff --git a/test/functional/mocks/signer.py b/test/functional/mocks/signer.py index 6b5c903886..cde85bcd73 100755 --- a/test/functional/mocks/signer.py +++ b/test/functional/mocks/signer.py @@ -17,10 +17,16 @@ def perform_pre_checks(): sys.stdout.write(mock_result[2:]) sys.exit(int(mock_result[0])) +def enumerate(args): + sys.stdout.write(json.dumps([{"fingerprint": "00000001", "type": "trezor", "model": "trezor_t"}, {"fingerprint": "00000002"}])) + parser = argparse.ArgumentParser(prog='./signer.py', description='External signer mock') subparsers = parser.add_subparsers(description='Commands', dest='command') subparsers.required = True +parser_enumerate = subparsers.add_parser('enumerate', help='list available signers') +parser_enumerate.set_defaults(func=enumerate) + args = parser.parse_args() perform_pre_checks() diff --git a/test/functional/wallet_signer.py b/test/functional/wallet_signer.py index 62d4db837e..295cdcf398 100755 --- a/test/functional/wallet_signer.py +++ b/test/functional/wallet_signer.py @@ -48,5 +48,36 @@ class SignerTest(BitcoinTestFramework): def run_test(self): self.log.debug(f"-signer={self.mock_signer_path()}") + assert_raises_rpc_error(-4, 'Error: restart bitcoind with -signer=', + self.nodes[0].enumeratesigners + ) + + # Handle script missing: + assert_raises_rpc_error(-1, 'execve failed: No such file or directory', + self.nodes[2].enumeratesigners + ) + + # Handle error thrown by script + self.set_mock_result(self.nodes[1], "2") + assert_raises_rpc_error(-1, 'RunCommandParseJSON error', + self.nodes[1].enumeratesigners + ) + self.clear_mock_result(self.nodes[1]) + + self.set_mock_result(self.nodes[1], '0 [{"type": "trezor", "model": "trezor_t", "error": "fingerprint not found"}]') + assert_raises_rpc_error(-4, 'fingerprint not found', + self.nodes[1].enumeratesigners + ) + self.clear_mock_result(self.nodes[1]) + + # Create new wallets with private keys disabled: + self.nodes[1].createwallet(wallet_name='hww', disable_private_keys=True, descriptors=True) + hww = self.nodes[1].get_wallet_rpc('hww') + + result = hww.enumeratesigners() + assert_equal(len(result['signers']), 2) + assert_equal(result['signers'][0]["fingerprint"], "00000001") + assert_equal(result['signers'][0]["name"], "trezor_t") + if __name__ == '__main__': SignerTest().main() -- cgit v1.2.3