Add LedgerTCP hardware wallet support

This communicates with the Ledger over TCP, which is what the ledger
emulator requires.

To use, specify:

    --hw-device LedgerTCP --hw-device-address localhost:9999

to the wallet command-line arguments.
This commit is contained in:
Jason Rhinelander 2021-06-25 17:14:00 -03:00
parent b8ecb6724c
commit 06a9251f15
12 changed files with 304 additions and 16 deletions

View File

@ -595,6 +595,7 @@ build_external(zmq
${zmq_patch}
CONFIGURE_COMMAND ./configure ${zmq_cross_host} --prefix=${DEPS_DESTDIR} --enable-static --disable-shared
--disable-curve-keygen --enable-curve --disable-drafts --disable-libunwind --with-libsodium
--disable-libbsd --disable-perf
--without-pgm --without-norm --without-vmci --without-docs --with-pic --disable-Werror
"CC=${deps_cc}" "CXX=${deps_cxx}" "CFLAGS=-fstack-protector ${deps_CFLAGS}" "CXXFLAGS=-fstack-protector ${deps_CXXFLAGS}"
${cross_extra}

View File

@ -70,6 +70,7 @@ if (HIDAPI_FOUND)
target_sources(device PRIVATE
device_ledger.cpp
io_hid.cpp
io_ledger_tcp.cpp
)
target_link_libraries(device PRIVATE hidapi_libusb)
endif()

View File

@ -119,6 +119,10 @@ namespace hw {
virtual bool set_name(std::string_view name) = 0;
virtual std::string get_name() const = 0;
// Optional; can be used to take an address parameter if required (e.g. Ledger TCP uses this
// to specify the TCP address).
virtual void set_address(std::string_view address) {}
virtual bool init() = 0;
virtual bool release() = 0;

View File

@ -28,6 +28,7 @@
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
#include "device/io_ledger_tcp.hpp"
#include "io_hid.hpp"
#include "version.h"
#include "device_ledger.hpp"
@ -313,7 +314,7 @@ namespace hw::ledger {
static_assert(BLAKE2B_HASH_CHUNK_SIZE <= 254, "Max BLAKE2b data chunk size exceeds the protocol limit");
device_ledger::device_ledger(): hw_device(0x0101, 0x05, 64, 2000) {
device_ledger::device_ledger() : hw_device{std::make_unique<io::hid>(0x0101, 0x05, 64, 2000)} {
id = device_id++;
reset_buffer();
has_view_key = false;
@ -321,6 +322,14 @@ namespace hw::ledger {
MDEBUG("Device " << id << " Created");
}
device_ledger::device_ledger(io::ledger_tcp&& tcp) : hw_device{std::make_unique<io::ledger_tcp>(std::move(tcp))} {
id = device_id++;
reset_buffer();
has_view_key = false;
tx_in_progress = false;
MDEBUG("Device " << id << " (tcp) created");
}
device_ledger::~device_ledger() {
release();
MDEBUG("Device " << id << " Destroyed");
@ -497,7 +506,7 @@ namespace hw::ledger {
unsigned int device_ledger::exchange(bool wait_on_input) {
logCMD();
length_recv = hw_device.exchange(buffer_send, length_send, buffer_recv, BUFFER_SEND_SIZE, wait_on_input);
length_recv = hw_device->exchange(buffer_send, length_send, buffer_recv, BUFFER_RECV_SIZE, wait_on_input);
CHECK_AND_ASSERT_THROW_MES(length_recv >= 2, "Communication error, less than two bytes received");
length_recv -= 2;
@ -537,12 +546,25 @@ namespace hw::ledger {
return name;
}
void device_ledger::set_address(std::string_view addr) {
if (addr.empty() || !hw_device)
return;
auto* tcp = dynamic_cast<io::ledger_tcp*>(hw_device.get());
if (!tcp)
return;
if (auto pos = addr.rfind(':'); pos != addr.npos) {
tcp->port = addr.substr(pos + 1);
addr = addr.substr(0, pos);
}
tcp->host = addr;
}
bool device_ledger::init() {
#ifdef DEBUG_HWDEVICE
debug_device = &get_device("default");
#endif
release();
hw_device.init();
hw_device->init();
MDEBUG("Device " << id <<" HIDUSB inited");
return true;
}
@ -554,7 +576,12 @@ namespace hw::ledger {
bool device_ledger::connect() {
disconnect();
hw_device.connect(known_devices);
if (auto* hid_io = dynamic_cast<io::hid*>(hw_device.get()))
hid_io->connect(known_devices);
else if (auto* tcp = dynamic_cast<io::ledger_tcp*>(hw_device.get()))
tcp->connect();
else
throw std::logic_error{"Invalid ledger hardware configure"};
reset();
check_network_type();
@ -571,17 +598,17 @@ namespace hw::ledger {
}
bool device_ledger::connected() const {
return hw_device.connected();
return hw_device->connected();
}
bool device_ledger::disconnect() {
hw_device.disconnect();
hw_device->disconnect();
return true;
}
bool device_ledger::release() {
disconnect();
hw_device.release();
hw_device->release();
return true;
}
@ -1943,6 +1970,7 @@ namespace hw::ledger {
void register_all(std::map<std::string, std::unique_ptr<device>>& registry) {
registry.emplace("Ledger", std::make_unique<device_ledger>());
registry.emplace("LedgerTCP", std::make_unique<device_ledger>(io::ledger_tcp{}));
}
#else //WITH_DEVICE_LEDGER

View File

@ -34,6 +34,7 @@
#include <cstddef>
#include <string>
#include "device.hpp"
#include "device/io_ledger_tcp.hpp"
#include "log.hpp"
#include "io_hid.hpp"
@ -142,7 +143,7 @@ namespace hw::ledger {
mutable std::mutex command_locker;
//IO
hw::io::hid hw_device;
std::unique_ptr<io::device> hw_device;
unsigned int length_send;
unsigned char buffer_send[BUFFER_SEND_SIZE];
unsigned int length_recv;
@ -202,11 +203,20 @@ namespace hw::ledger {
#endif
public:
// Constructs a ledger device object that connects to a physical ledger device plugged into
// the system.
device_ledger();
// Constructs a ledger device object that uses a TCP socket to communicate with the ledger
// device, typically for use with a Ledger emulator which doesn't emulated the USB layer.
explicit device_ledger(io::ledger_tcp&& tcp);
~device_ledger();
device_ledger(const device_ledger &device) = delete ;
device_ledger& operator=(const device_ledger &device) = delete;
device_ledger(const device_ledger&) = delete ;
device_ledger(device_ledger&&) = delete ;
device_ledger& operator=(const device_ledger&) = delete;
device_ledger&& operator=(device_ledger&&) = delete;
bool is_hardware_device() const override { return connected(); }
@ -217,6 +227,8 @@ namespace hw::ledger {
/* ======================================================================= */
bool set_name(std::string_view name) override;
void set_address(std::string_view addr) override;
std::string get_name() const override;
bool init() override;
bool release() override;

View File

@ -46,7 +46,7 @@ namespace hw::io {
virtual void disconnect() = 0;
virtual bool connected() const = 0;
virtual int exchange(unsigned char *command, unsigned int cmd_len, unsigned char *response, unsigned int max_resp_len, bool user_input) = 0;
virtual int exchange(const unsigned char* command, unsigned int cmd_len, unsigned char* response, unsigned int max_resp_len, bool user_input) = 0;
};
}

View File

@ -140,7 +140,7 @@ namespace hw::io {
return usb_device != nullptr;
}
int hid::exchange(unsigned char *command, unsigned int cmd_len, unsigned char *response, unsigned int max_resp_len, bool user_input) {
int hid::exchange(const unsigned char* command, unsigned int cmd_len, unsigned char* response, unsigned int max_resp_len, bool user_input) {
unsigned char buffer[400];
unsigned char padding_buffer[MAX_BLOCK+1];
unsigned int result;

View File

@ -91,7 +91,7 @@ namespace hw {
void connect(const std::vector<hid_conn_params>& conn);
bool connect(unsigned int vid, unsigned int pid, std::optional<int> interface_number, std::optional<unsigned short> usage_page);
bool connected() const override;
int exchange(unsigned char* command, unsigned int cmd_len, unsigned char* response, unsigned int max_resp_len, bool user_input) override;
int exchange(const unsigned char* command, unsigned int cmd_len, unsigned char* response, unsigned int max_resp_len, bool user_input) override;
void disconnect() override;
void release() override;
};

View File

@ -0,0 +1,193 @@
#include "io_ledger_tcp.hpp"
#include "common/oxen.h"
#include <array>
#include <boost/endian/conversion.hpp>
#include <cstring>
#include <stdexcept>
#include "epee/misc_log_ex.h"
extern "C" {
#ifdef _WIN32
# include <ws2tcpip.h>
# include <winsock2.h>
#else
# include <arpa/inet.h>
# include <netdb.h>
# include <netinet/in.h>
# include <sys/socket.h>
#endif
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
}
#undef OXEN_DEFAULT_LOG_CATEGORY
#define OXEN_DEFAULT_LOG_CATEGORY "device.io"
namespace hw::io {
static std::string to_string(const addrinfo* a) {
std::array<char, INET6_ADDRSTRLEN> buf;
std::string addr;
#ifdef _WIN32
unsigned long buflen = buf.size();
if (auto rc = WSAAddressToString(a->ai_addr, a->ai_addrlen, nullptr, buf.data(), &buflen);
rc == 0)
addr = buf.data();
else
addr = "[error:"s + std::to_string(rc) + "]";
#else
if (inet_ntop(a->ai_family, a->ai_addr, buf.data(), buf.size()))
addr = buf.data();
else
addr = "[error:"s + strerror(errno) + "]";
#endif
if (a->ai_family == AF_INET)
(addr += ':') += std::to_string(reinterpret_cast<sockaddr_in*>(a->ai_addr)->sin_port);
else if (a->ai_family == AF_INET6)
(addr += ':') += std::to_string(reinterpret_cast<sockaddr_in6*>(a->ai_addr)->sin6_port);
return addr;
}
void ledger_tcp::connect() {
disconnect();
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0)
throw std::runtime_error{"Failed to open socket: "s + strerror(errno)};
auto closer = oxen::defer([&] { close(fd); });
#ifdef _WIN32
unsigned long blocking_param = 1; // 1 = make non-blocking, 0 = blocking
if (auto result = ioctlsocket(fd, FIONBIO, &blocking_param);
result != NO_ERROR)
throw std::runtime_error{"ioctlsocket failed with error: " + std::to_string(result)};
#else
if (-1 == fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK))
throw std::runtime_error{"Failed to set socket non-blocking: "s + strerror(errno)};
#endif
addrinfo* addr;
if (int rc = getaddrinfo(host.data(), port.data(), nullptr, &addr);
rc != 0)
throw std::runtime_error{"Failed to resolve " + host + ":" + port + ": " + gai_strerror(rc)};
auto addr_free = oxen::defer([&] { freeaddrinfo(addr); });
const addrinfo* a;
bool connected = false;
const char* err = "An unknown error occurred";
for (a = addr; a && !connected; a = a->ai_next) {
MDEBUG("Attempting to connect to " << to_string(a));
int rc = ::connect(fd, a->ai_addr, a->ai_addrlen);
connected = rc == 0;
if (rc == -1) {
if (errno == EINPROGRESS) {
timeval timeo;
timeo.tv_sec = std::chrono::duration_cast<std::chrono::seconds>(connect_timeout).count();
timeo.tv_usec = (connect_timeout % 1s).count();
fd_set myset;
FD_ZERO(&myset);
FD_SET(fd, &myset);
rc = select(fd + 1, nullptr, &myset, nullptr, &timeo);
if (rc > 0)
connected = true;
else if (rc == 0)
err = "Connection timed out";
else
err = strerror(errno);
} else {
err = strerror(errno);
}
}
}
if (!connected)
throw std::runtime_error{"Failed to connect to " + host + ":" + port + ": " + err};
MDEBUG("Connected to " << to_string(a));
#ifdef _WIN32
blocking_param = 0;
if (auto result = ioctlsocket(fd, FIONBIO, &blocking_param);
result != NO_ERROR)
throw std::runtime_error{"ioctlsocket failed with error: " + std::to_string(result)};
#else
if (-1 == fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK))
throw std::runtime_error{"Failed to set socket back to blocking: "s + strerror(errno)};
#endif
timeval timeo;
timeo.tv_sec = std::chrono::duration_cast<std::chrono::seconds>(exchange_timeout).count();
timeo.tv_usec = (exchange_timeout % 1s).count();
// The reinterpret_cast here is needed for Windows's shitty imitation of the api
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&timeo), sizeof(timeo));
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast<const char*>(&timeo), sizeof(timeo));
sockfd = std::make_unique<int>(fd);
closer.cancel();
}
void ledger_tcp::disconnect() {
if (!sockfd)
return;
close(*sockfd);
sockfd.reset();
}
ledger_tcp::~ledger_tcp() {
disconnect();
}
bool ledger_tcp::connected() const {
return (bool) sockfd;
}
void full_read(int fd, unsigned char* to, int size) {
while (size > 0) {
auto read_size = read(fd, to, size);
if (read_size == -1)
throw std::runtime_error{"Failed to read from hardware wallet socket: "s + strerror(errno)};
size -= read_size;
to += read_size;
}
}
void full_write(int fd, const unsigned char* from, int size) {
while (size > 0) {
auto wrote = write(fd, from, size);
if (wrote == -1)
throw std::runtime_error{"Failed to write to hardware wallet socket: "s + strerror(errno)};
size -= wrote;
from += wrote;
}
}
int ledger_tcp::exchange(const unsigned char* command, unsigned int cmd_len, unsigned char* response, unsigned int max_resp_len, bool user_input) {
if (!sockfd)
throw std::runtime_error{"Unable to exchange data with hardware wallet: not connected"};
// Sending: [SIZE][DATA], where SIZE is a uint32_t in network order
uint32_t size = boost::endian::native_to_big(cmd_len);
const unsigned char* size_bytes = reinterpret_cast<const unsigned char*>(&size);
full_write(*sockfd, size_bytes, 4);
full_write(*sockfd, command, cmd_len);
// Receiving: [SIZE][DATA], where SIZE is the length of DATA minus 2 (WTF) because the last two
// bytes of DATA are a 2-byte, u16 status code and... therefore not... included. Good job, Ledger
// devs.
full_read(*sockfd, reinterpret_cast<unsigned char*>(&size), 4);
auto data_size = boost::endian::big_to_native(size) + 2;
if (data_size > max_resp_len)
throw std::runtime_error{"Hardware wallet returned unexpectedly large response: got " +
std::to_string(data_size) + " bytes, expected <= " + std::to_string(max_resp_len)};
full_read(*sockfd, response, data_size);
return data_size;
}
}

View File

@ -0,0 +1,41 @@
// TCP APDU interface, as used by Ledger's emulator system (Speculos).
#pragma once
#include <chrono>
#include <memory>
#include <string>
#include "io_device.hpp"
#pragma once
namespace hw::io {
using namespace std::literals;
class ledger_tcp : public device {
std::unique_ptr<int> sockfd;
public:
std::string host = "localhost";
std::string port = "9999";
std::chrono::microseconds connect_timeout = 10s;
std::chrono::microseconds exchange_timeout = 120s;
ledger_tcp() = default;
~ledger_tcp() override;
ledger_tcp(ledger_tcp&&) = default;
ledger_tcp& operator=(ledger_tcp&&) = default;
void init() override {}
void release() override {}
void connect();
bool connected() const override;
int exchange(const unsigned char* command, unsigned int cmd_len, unsigned char* response, unsigned int max_resp_len, bool user_input) override;
void disconnect() override;
};
}

View File

@ -282,6 +282,7 @@ struct options {
};
const command_line::arg_descriptor<uint64_t> kdf_rounds = {"kdf-rounds", tools::wallet2::tr("Number of rounds for the key derivation function"), 1};
const command_line::arg_descriptor<std::string> hw_device = {"hw-device", tools::wallet2::tr("HW device to use"), ""};
const command_line::arg_descriptor<std::string> hw_device_address = {"hw-device-address", tools::wallet2::tr("HW device address, if required"), ""};
const command_line::arg_descriptor<std::string> hw_device_derivation_path = {"hw-device-deriv-path", tools::wallet2::tr("HW device wallet derivation path (e.g., SLIP-10)"), ""};
const command_line::arg_descriptor<std::string> tx_notify = { "tx-notify" , "Run a program for each new incoming transaction, '%s' will be replaced by the transaction hash" , "" };
const command_line::arg_descriptor<bool> offline = {"offline", tools::wallet2::tr("Do not connect to a daemon"), false};
@ -345,6 +346,7 @@ std::unique_ptr<tools::wallet2> make_basic(const boost::program_options::variabl
auto daemon_port = command_line::get_arg(vm, opts.daemon_port);
auto device_name = command_line::get_arg(vm, opts.hw_device);
auto device_addr = command_line::get_arg(vm, opts.hw_device_address);
auto device_derivation_path = command_line::get_arg(vm, opts.hw_device_derivation_path);
THROW_WALLET_EXCEPTION_IF(!daemon_address.empty() && (!daemon_host.empty() || 0 != daemon_port),
@ -419,6 +421,7 @@ std::unique_ptr<tools::wallet2> make_basic(const boost::program_options::variabl
wallet->get_message_store().set_options(vm);
#endif
wallet->device_name(device_name);
wallet->device_address(device_addr);
wallet->device_derivation_path(device_derivation_path);
wallet->m_long_poll_disabled = command_line::get_arg(vm, opts.disable_rpc_long_poll);
wallet->m_http_client.set_https_client_cert(command_line::get_arg(vm, opts.daemon_ssl_certificate), command_line::get_arg(vm, opts.daemon_ssl_private_key));
@ -1151,6 +1154,7 @@ void wallet2::init_options(boost::program_options::options_description& desc_par
mms::message_store::init_options(desc_params);
#endif
command_line::add_arg(desc_params, opts.hw_device);
command_line::add_arg(desc_params, opts.hw_device_address);
command_line::add_arg(desc_params, opts.hw_device_derivation_path);
command_line::add_arg(desc_params, opts.tx_notify);
command_line::add_arg(desc_params, opts.offline);
@ -1366,6 +1370,7 @@ bool wallet2::reconnect_device()
bool r = true;
hw::device &hwdev = lookup_device(m_device_name);
hwdev.set_name(m_device_name);
hwdev.set_address(m_device_address);
hwdev.set_network_type(m_nettype);
hwdev.set_derivation_path(m_device_derivation_path);
hwdev.set_callback(get_device_callback());
@ -7887,7 +7892,7 @@ void wallet2::register_devices(){
hw::trezor::register_all();
}
hw::device& wallet2::lookup_device(const std::string & device_descriptor){
hw::device& wallet2::lookup_device(const std::string & device_descriptor) {
if (!m_devices_registered){
m_devices_registered = true;
register_devices();

View File

@ -1037,8 +1037,10 @@ private:
void track_uses(bool value) { m_track_uses = value; }
std::chrono::seconds inactivity_lock_timeout() const { return m_inactivity_lock_timeout; }
void inactivity_lock_timeout(std::chrono::seconds seconds) { m_inactivity_lock_timeout = seconds; }
const std::string & device_name() const { return m_device_name; }
void device_name(const std::string & device_name) { m_device_name = device_name; }
const std::string& device_name() const { return m_device_name; }
const std::string& device_address() const { return m_device_address; }
void device_name(std::string device_name) { m_device_name = std::move(device_name); }
void device_address(std::string device_address) { m_device_address = std::move(device_address); }
const std::string & device_derivation_path() const { return m_device_derivation_path; }
void device_derivation_path(const std::string &device_derivation_path) { m_device_derivation_path = device_derivation_path; }
const ExportFormat & export_format() const { return m_export_format; }
@ -1682,6 +1684,7 @@ private:
std::unordered_set<crypto::hash> m_scanned_pool_txs[2];
size_t m_subaddress_lookahead_major, m_subaddress_lookahead_minor;
std::string m_device_name;
std::string m_device_address;
std::string m_device_derivation_path;
uint64_t m_device_last_key_image_sync;
bool m_offline;