From 7e34f641d0bea5316a251ee9614330bcf51e2cdd Mon Sep 17 00:00:00 2001 From: Andreas Kemnade Date: Fri, 2 Jun 2023 23:09:02 +0200 Subject: [PATCH] Extend FIDO2 BLE support also for Linux For Windows it was already added via gh#336, so let's also add it for Linux. Unpaired devices are ignored, the user has to pair independently of libfido use using the bluetooth manager provided by the desktop environment. --- .github/workflows/alpine_builds.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/linux_builds.yml | 2 +- .github/workflows/linux_fuzz.yml | 2 +- .github/workflows/openssl3.yml | 2 +- CMakeLists.txt | 11 + src/CMakeLists.txt | 5 + src/ble.c | 280 +++++++++++++ src/ble_linux.c | 550 ++++++++++++++++++++++++++ src/dev.c | 9 + src/extern.h | 13 + 11 files changed, 873 insertions(+), 5 deletions(-) create mode 100644 src/ble.c create mode 100644 src/ble_linux.c diff --git a/.github/workflows/alpine_builds.yml b/.github/workflows/alpine_builds.yml index ece6ba5f..4c5a1f27 100644 --- a/.github/workflows/alpine_builds.yml +++ b/.github/workflows/alpine_builds.yml @@ -28,7 +28,7 @@ jobs: apk -q update apk add build-base clang clang-analyzer cmake coreutils eudev-dev apk add git linux-headers openssl-dev sudo zlib-dev pcsc-lite-dev \ - libcbor-dev + libcbor-dev elogind-dev - name: fix permissions on workdir run: chown root:wheel "${GITHUB_WORKSPACE}" - name: checkout libfido2 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index de381e9b..b6250c35 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -33,7 +33,7 @@ jobs: run: | sudo apt -q update sudo apt install -q -y libcbor-dev libudev-dev libz-dev original-awk \ - libpcsclite-dev + libpcsclite-dev libsystemd-dev ./.actions/build-linux-gcc - name: perform codeql analysis uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/linux_builds.yml b/.github/workflows/linux_builds.yml index 41174d54..3b2a9721 100644 --- a/.github/workflows/linux_builds.yml +++ b/.github/workflows/linux_builds.yml @@ -38,7 +38,7 @@ jobs: run: | sudo apt -q update sudo apt install -q -y libcbor-dev libudev-dev libz-dev \ - original-awk mandoc libpcsclite-dev + original-awk mandoc libpcsclite-dev libsystemd-dev - name: compiler env: CC: ${{ matrix.cc }} diff --git a/.github/workflows/linux_fuzz.yml b/.github/workflows/linux_fuzz.yml index fcf7934b..8d979101 100644 --- a/.github/workflows/linux_fuzz.yml +++ b/.github/workflows/linux_fuzz.yml @@ -28,7 +28,7 @@ jobs: - name: dependencies run: | sudo apt -q update - sudo apt install -q -y libudev-dev libpcsclite-dev + sudo apt install -q -y libudev-dev libpcsclite-dev libsystemd-dev - name: compiler env: CC: ${{ matrix.cc }} diff --git a/.github/workflows/openssl3.yml b/.github/workflows/openssl3.yml index f1d3dbd5..686ddd56 100644 --- a/.github/workflows/openssl3.yml +++ b/.github/workflows/openssl3.yml @@ -35,7 +35,7 @@ jobs: run: | sudo apt -q update sudo apt install -q -y libcbor-dev libudev-dev libz-dev \ - original-awk mandoc libpcsclite-dev + original-awk mandoc libpcsclite-dev libsystemd-dev sudo apt remove -y libssl-dev if [ "${CC%-*}" == "clang" ]; then sudo ./.actions/setup_clang "${CC}" diff --git a/CMakeLists.txt b/CMakeLists.txt index 870ffd59..7150649c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,7 @@ option(USE_HIDAPI "Use hidapi as the HID backend" OFF) option(USE_PCSC "Enable experimental PCSC support" ON) option(USE_WINHELLO "Abstract Windows Hello as a FIDO device" ON) option(NFC_LINUX "Enable NFC support on Linux" ON) +option(BLE_LINUX "Enable Bluetooth support on Linux" ON) add_definitions(-D_FIDO_MAJOR=${FIDO_MAJOR}) add_definitions(-D_FIDO_MINOR=${FIDO_MINOR}) @@ -216,6 +217,7 @@ if(MSVC) add_definitions(-DUSE_WINHELLO) endif() set(NFC_LINUX OFF) + set(BLE_LINUX OFF) else() include(FindPkgConfig) pkg_search_module(CBOR libcbor) @@ -255,6 +257,7 @@ else() endif() else() set(NFC_LINUX OFF) + set(BLE_LINUX OFF) endif() if(MINGW) @@ -285,6 +288,11 @@ else() add_definitions(-DUSE_NFC) endif() + if(BLE_LINUX) + add_definitions(-DUSE_BLE) + pkg_search_module(BLE libsystemd REQUIRED) + endif() + if(WIN32) if(USE_WINHELLO) add_definitions(-DUSE_WINHELLO) @@ -392,6 +400,7 @@ include_directories(${PROJECT_SOURCE_DIR}/src) include_directories(${CBOR_INCLUDE_DIRS}) include_directories(${CRYPTO_INCLUDE_DIRS}) include_directories(${HIDAPI_INCLUDE_DIRS}) +include_directories(${BLE_INCLUDE_DIRS}) include_directories(${PCSC_INCLUDE_DIRS}) include_directories(${UDEV_INCLUDE_DIRS}) include_directories(${ZLIB_INCLUDE_DIRS}) @@ -399,6 +408,7 @@ include_directories(${ZLIB_INCLUDE_DIRS}) link_directories(${CBOR_LIBRARY_DIRS}) link_directories(${CRYPTO_LIBRARY_DIRS}) link_directories(${HIDAPI_LIBRARY_DIRS}) +link_directories(${BLE_LIBRARY_DIRS}) link_directories(${PCSC_LIBRARY_DIRS}) link_directories(${UDEV_LIBRARY_DIRS}) link_directories(${ZLIB_LIBRARY_DIRS}) @@ -468,6 +478,7 @@ message(STATUS "USE_HIDAPI: ${USE_HIDAPI}") message(STATUS "USE_PCSC: ${USE_PCSC}") message(STATUS "USE_WINHELLO: ${USE_WINHELLO}") message(STATUS "NFC_LINUX: ${NFC_LINUX}") +message(STATUS "BLE_LINUX: ${BLE_LINUX}") if(BUILD_TESTS) enable_testing() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 73493b1e..24446728 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -51,6 +51,10 @@ if(FUZZ) list(APPEND FIDO_SOURCES ../fuzz/wrap.c) endif() +if(BLE_LINUX) + list(APPEND FIDO_SOURCES ble.c ble_linux.c) +endif() + if(NFC_LINUX) list(APPEND FIDO_SOURCES netlink.c nfc.c nfc_linux.c) endif() @@ -123,6 +127,7 @@ list(APPEND TARGET_LIBRARIES ${HIDAPI_LIBRARIES} ${ZLIB_LIBRARIES} ${PCSC_LIBRARIES} + ${BLE_LIBRARIES} ) # static library diff --git a/src/ble.c b/src/ble.c new file mode 100644 index 00000000..73260366 --- /dev/null +++ b/src/ble.c @@ -0,0 +1,280 @@ +#include "fido.h" +#include "fido/param.h" + +#define CTAPBLE_PING 0x81 +#define CTAPBLE_KEEPALIVE 0x82 +#define CTAPBLE_MSG 0x83 +#define CTAPBLE_CANCEL 0xBE +#define CTAPBLE_ERROR 0xBF +#define CTAPBLE_MAX_FRAME_LEN 512 +#define CTAPBLE_INIT_HEADER_LEN 3 +#define CTAPBLE_CONT_HEADER_LEN 1 + + +#ifndef MIN +#define MIN(x, y) ((x) > (y) ? (y) : (x)) +#endif + +union frame { + struct { + uint8_t cmd; + uint8_t hlen; + uint8_t llen; + uint8_t data[CTAPBLE_MAX_FRAME_LEN - CTAPBLE_INIT_HEADER_LEN]; + } init; + struct { + uint8_t seq; + uint8_t data[CTAPBLE_MAX_FRAME_LEN - CTAPBLE_CONT_HEADER_LEN]; + } cont; +}; + +static size_t +tx_preamble(fido_dev_t *d, uint8_t cmd, const u_char *buf, size_t count) +{ + union frame frag_buf; + size_t fragment_len = MIN(fido_ble_get_cp_size(d), CTAPBLE_MAX_FRAME_LEN); + int r; + + if (fragment_len <= CTAPBLE_INIT_HEADER_LEN) + return 0; + + frag_buf.init.cmd = cmd; + frag_buf.init.hlen = (count >> 8) & 0xff; + frag_buf.init.llen = count & 0xff; + + count = MIN(count, fragment_len - CTAPBLE_INIT_HEADER_LEN); + memcpy(frag_buf.init.data, buf, count); + + count += CTAPBLE_INIT_HEADER_LEN; + r = d->io.write(d->io_handle, (const u_char *)&frag_buf, count); + explicit_bzero(&frag_buf, sizeof(frag_buf)); + + if ((r < 0) || ((size_t)r != count)) + return 0; + + return count - CTAPBLE_INIT_HEADER_LEN; +} + +static size_t +tx_cont(fido_dev_t *d, uint8_t seq, const u_char *buf, size_t count) +{ + union frame frag_buf; + int r; + size_t fragment_len = MIN(fido_ble_get_cp_size(d), CTAPBLE_MAX_FRAME_LEN); + + if (fragment_len <= CTAPBLE_CONT_HEADER_LEN) + return 0; + + frag_buf.cont.seq = seq; + count = MIN(count, fragment_len - CTAPBLE_CONT_HEADER_LEN); + memcpy(frag_buf.cont.data, buf, count); + + count += CTAPBLE_CONT_HEADER_LEN; + r = d->io.write(d->io_handle, (const u_char *)&frag_buf, count); + explicit_bzero(&frag_buf, sizeof(frag_buf)); + + if ((r < 0) || ((size_t)r != count)) + return 0; + + return count - CTAPBLE_CONT_HEADER_LEN; +} + +static int +fido_ble_fragment_tx(fido_dev_t *d, uint8_t cmd, const u_char *buf, size_t count) +{ + size_t n, sent; + + if ((sent = tx_preamble(d, cmd, buf, count)) == 0) { + fido_log_debug("%s: tx_preamble", __func__); + return (-1); + } + + for (uint8_t seq = 0; sent < count; sent += n) { + if ((n = tx_cont(d, seq++, buf + sent, count - sent)) == 0) { + fido_log_debug("%s: tx_frame", __func__); + return (-1); + } + + seq &= 0x7f; + } + + return 0; +} + +int +fido_ble_tx(fido_dev_t *d, uint8_t cmd, const u_char *buf, size_t count) +{ + switch(cmd) { + case CTAP_CMD_INIT: + return 0; + case CTAP_CMD_CBOR: + case CTAP_CMD_MSG: + return fido_ble_fragment_tx(d, CTAPBLE_MSG, buf, count); + default: + fido_log_debug("%s: unsupported command %02x", __func__, cmd); + return -1; + } +} + +static int +rx_init(fido_dev_t *d, unsigned char *buf, size_t count, int ms) +{ + (void)ms; + fido_ctap_info_t *attr = (fido_ctap_info_t *)buf; + if (count != sizeof(*attr)) { + fido_log_debug("%s: count=%zu", __func__, count); + return -1; + } + + memset(attr, 0, sizeof(*attr)); + + /* we allow only FIDO2 devices for now for simplicity */ + attr->flags = FIDO_CAP_CBOR | FIDO_CAP_NMSG; + memcpy(&attr->nonce, &d->nonce, sizeof(attr->nonce)); + + return (int)count; +} + +static int +rx_preamble(fido_dev_t *d, unsigned char **buf, size_t *count, size_t *reply_length, int ms) +{ + union frame reply; + int ret; + size_t payload; + size_t fragment_len = fido_ble_get_cp_size(d); + + if (fragment_len <= CTAPBLE_INIT_HEADER_LEN) { + return -1; + } + + payload = fragment_len - CTAPBLE_INIT_HEADER_LEN; + if (*count < payload) + payload = *count; + + do { + ret = d->io.read(d->io_handle, (u_char *)&reply, + payload + CTAPBLE_INIT_HEADER_LEN, ms); + if (ret <= 0) { + fido_log_debug("%s: read header", __func__); + goto out; + } + } while (reply.init.cmd == CTAPBLE_KEEPALIVE); + + if ((reply.init.cmd != CTAPBLE_MSG) || ret <= CTAPBLE_INIT_HEADER_LEN) { + ret = -1; + goto out; + } + ret -= CTAPBLE_INIT_HEADER_LEN; + *reply_length = ((size_t)reply.init.hlen) << 8 | reply.init.llen; + if (*reply_length > *count) { + fido_log_debug("%s: more data in reply than expected", __func__); + ret = -1; + goto out; + } + + *count = MIN(*reply_length, *count); + + if (fido_buf_write(buf, count, reply.init.data, (size_t)ret) < 0) { + ret = -1; + goto out; + } + ret = 0; +out: + explicit_bzero(&reply, sizeof(reply)); + return ret; +} + +static int +rx_cont(fido_dev_t *d, unsigned char **buf, uint8_t seq, size_t *count, int ms) +{ + union frame reply; + int ret; + size_t payload; + size_t fragment_len = fido_ble_get_cp_size(d); + payload = fragment_len - CTAPBLE_CONT_HEADER_LEN; + payload = MIN(*count, payload); + ret = d->io.read(d->io_handle, (u_char *) &reply, + payload + CTAPBLE_CONT_HEADER_LEN, ms); + + if (ret <= CTAPBLE_CONT_HEADER_LEN) { + if (ret >= 0) + ret = -1; + fido_log_debug("%s: read cont", __func__); + goto out; + } + ret -= CTAPBLE_CONT_HEADER_LEN; + if (reply.cont.seq != seq) { + ret = -1; + goto out; + } + + if (fido_buf_write(buf, count, reply.cont.data, (size_t)ret) < 0) + ret = -1; + +out: + explicit_bzero(&reply, sizeof(reply)); + return ret; +} + +static int +rx_fragments(fido_dev_t *d, unsigned char *buf, size_t count, int ms) +{ + uint8_t seq; + size_t reply_length; + + if (rx_preamble(d, &buf, &count, &reply_length, ms) < 0) + return -1; + + seq = 0; + while(count > 0) { + if (rx_cont(d, &buf, seq, &count, ms) < 0) + return -1; + + seq++; + seq &= 0x7f; + } + return (int)reply_length; +} + +int +fido_ble_rx(fido_dev_t *d, uint8_t cmd, u_char *buf, size_t count, int ms) +{ + switch(cmd) { + case CTAP_CMD_INIT: + return rx_init(d, buf, count, ms); + case CTAP_CMD_CBOR: + return rx_fragments(d, buf, count, ms); + default: + fido_log_debug("%s: unsupported command %02x", __func__, cmd); + return -1; + } +} + +bool +fido_is_ble(const char *path) +{ + return !strncmp(path, FIDO_BLE_PREFIX, strlen(FIDO_BLE_PREFIX)); +} + +int +fido_dev_set_ble(fido_dev_t *d) +{ + if (d->io_handle != NULL) { + fido_log_debug("%s: device open", __func__); + return -1; + } + d->io_own = true; + d->io = (fido_dev_io_t) { + fido_ble_open, + fido_ble_close, + fido_ble_read, + fido_ble_write, + }; + d->transport = (fido_dev_transport_t) { + fido_ble_rx, + fido_ble_tx, + }; + + return 0; +} + diff --git a/src/ble_linux.c b/src/ble_linux.c new file mode 100644 index 00000000..c14318fd --- /dev/null +++ b/src/ble_linux.c @@ -0,0 +1,550 @@ +#include +#include +#include +#include + +#include "fido.h" +#include "fido/param.h" + +#define FIDO_SERVICE_UUID "0000fffd-0000-1000-8000-00805f9b34fb" +#define FIDO_STATUS_UUID "f1d0fff2-deaa-ecee-b42f-c9ba7ed623bb" +#define FIDO_CONTROL_POINT_UUID "f1d0fff1-deaa-ecee-b42f-c9ba7ed623bb" +#define FIDO_CONTROL_POINT_LENGTH_UUID "f1d0fff3-deaa-ecee-b42f-c9ba7ed623bb" +#define FIDO_SERVICE_REVISION_UUID "f1d0fff4-deaa-ecee-b42f-c9ba7ed623bb" + +#define DBUS_CHAR_IFACE "org.bluez.GattCharacteristic1" +#define DBUS_DEV_IFACE "org.bluez.Device1" +#define DBUS_SERVICE_IFACE "org.bluez.GattService1" +#define DBUS_PROFILE_IFACE "org.bluez.GattProfile1" +#define DBUS_ADAPTER_IFACE "org.bluez.Adapter1" +#define DBUS_GATTMANAGER_IFACE "org.bluez.GattManager1" + +static int ble_fido_is_useable_device(sd_bus_message * reply, const char **name); +struct ble { + sd_bus *bus; + struct { + char *dev; + char *service; + char *control_point; + char *service_revision; + } paths; + size_t controlpoint_size; + int status_fd; +}; + +struct manifest_ctx { + sd_bus *bus; + fido_dev_info_t *devlist; + size_t ilen; + size_t *olen; +}; + +static size_t read_cpl(sd_bus *bus, const char *path) +{ + uint8_t cp_len[2]; + sd_bus_message *reply; + size_t ret; + if (sd_bus_call_method(bus, "org.bluez", path, + DBUS_CHAR_IFACE, "ReadValue", NULL, &reply, "a{sv}", 0) < 0) + return 0; + + if (sd_bus_message_read(reply, "ay", 2, cp_len, cp_len + 1) >= 0) + ret = ((size_t)cp_len[0] << 8) + cp_len[1]; + else + ret = 0; + + sd_bus_message_unref(reply); + return ret; +} + +static int acquire_status(sd_bus *bus, const char *path) +{ + int fd; + sd_bus_message *reply; + if (sd_bus_call_method(bus, "org.bluez", path, + DBUS_CHAR_IFACE, "AcquireNotify", NULL, &reply, "a{sv}", 0) < 0) + return -1; + + if (sd_bus_message_read_basic(reply, 'h', &fd) < 0) + return -1; + + return fd; +} + +static int read_revision(sd_bus *bus, const char *path) +{ + uint8_t revision; + int ret; + sd_bus_message *reply; + if (sd_bus_call_method(bus, "org.bluez", path, + DBUS_CHAR_IFACE, "ReadValue", NULL, &reply, "a{sv}", 0) < 0) + return -1; + + if (sd_bus_message_read(reply, "ay", 1, &revision) >= 0) + ret = revision; + else + ret = -1; + + sd_bus_message_unref(reply); + return ret; +} + +static int write_revision(sd_bus *bus, const char *path, uint8_t revision) +{ + if (sd_bus_call_method(bus, "org.bluez", path, + DBUS_CHAR_IFACE, "WriteValue", NULL, NULL, "aya{sv}", 1, revision, 0) < 0) + return -1; + + return 0; +} + +static int +found_gatt_characteristic(struct ble *dev, const char *path, sd_bus_message *reply) +{ + bool matches = false; + bool status_found = false; + bool control_point_found = false; + bool control_point_length_found = false; + bool service_revision_found = false; + + if (!dev->paths.service) { + if (sd_bus_message_skip(reply, "a{sv}") < 0) + return -1; + + return 0; + } + if (sd_bus_message_enter_container(reply, 'a', "{sv}") < 0) + return -1; + + while (sd_bus_message_enter_container(reply, 'e', "sv") > 0) { + const char *prop; + if (sd_bus_message_read_basic(reply, 's', &prop) <= 0) + return -1; + + if (!strcmp(prop, "Service")) { + const char *devpath; + if (sd_bus_message_read(reply, "v", "o", &devpath) <= 0) + return -1; + + if (!strcmp(devpath, dev->paths.service)) + matches = true; + + } else if (!strcmp(prop, "UUID")) { + const char *uuid; + if (sd_bus_message_read(reply, "v", "s", &uuid) <= 0) + return -1; + + if (!strcmp(uuid, FIDO_STATUS_UUID)) + status_found = true; + if (!strcmp(uuid, FIDO_CONTROL_POINT_UUID)) + control_point_found = true; + if (!strcmp(uuid, FIDO_CONTROL_POINT_LENGTH_UUID)) + control_point_length_found = true; + if (!strcmp(uuid, FIDO_SERVICE_REVISION_UUID)) + service_revision_found = true; + } else { + if (sd_bus_message_skip(reply, "v") < 0) + return -1; + } + if (sd_bus_message_exit_container(reply) < 0) + return -1; + } + if (sd_bus_message_exit_container(reply) < 0) + return -1; + + if (!matches) + return 0; + + if (status_found) { + dev->status_fd = acquire_status(dev->bus, path); + if (dev->status_fd < 0) + return -1; + } + + if (control_point_found) + dev->paths.control_point = strdup(path); + + if (control_point_length_found) { + dev->controlpoint_size = read_cpl(dev->bus, path); + if (dev->controlpoint_size == 0) + return -1; + } + + if (service_revision_found) + dev->paths.service_revision = strdup(path); + + return 0; +} + +static int +found_gatt_service(struct ble *dev, const char *path, sd_bus_message *reply) +{ + bool matches = false; + bool service_found = false; + if (sd_bus_message_enter_container(reply, 'a', "{sv}") < 0) + return -1; + + while (sd_bus_message_enter_container(reply, 'e', "sv") > 0) { + const char *prop; + if (sd_bus_message_read_basic(reply, 's', &prop) <= 0) + return -1; + + if (!strcmp(prop, "Device")) { + const char *devpath; + if (sd_bus_message_read(reply, "v", "o", &devpath) < 0) + return -1; + + if (!strcmp(devpath, dev->paths.dev)) + matches = true; + + } else if (!strcmp(prop, "UUID")) { + const char *uuid; + if (sd_bus_message_read(reply, "v", "s", &uuid) < 0) + return -1; + + if (!strcmp(uuid, FIDO_SERVICE_UUID)) + service_found = true; + } else { + if (sd_bus_message_skip(reply, "v") < 0) + return -1; + } + if (sd_bus_message_exit_container(reply) < 0) + return -1; + } + if (sd_bus_message_exit_container(reply) < 0) + return -1; + + if (matches && service_found) { + dev->paths.service = strdup(path); + } + return 0; +} + +static int +collect_device_chars(void *data, const char *path, const char *iface, sd_bus_message *reply) +{ + struct ble *dev = (struct ble *)data; + + if (!strcmp(iface, DBUS_SERVICE_IFACE)) + return found_gatt_service(dev, path, reply); + + if (!strcmp(iface, DBUS_CHAR_IFACE)) + return found_gatt_characteristic(dev, path, reply); + + return sd_bus_message_skip(reply, "a{sv}") < 0 ? -1 : 0; +} + +static int iterate_over_all_objs(sd_bus_message *reply, + int (*new_dbus_interface)(void *, + const char *, const char *, + sd_bus_message *), void *data) +{ + if (sd_bus_message_enter_container(reply, 'a', "{oa{sa{sv}}}") <= 0) + return -1; + + while (sd_bus_message_enter_container(reply, 'e', "oa{sa{sv}}") > 0) { + const char *ifacepath = NULL; + if (sd_bus_message_read_basic(reply, 'o', &ifacepath) <= 0) + return -1; + + if (sd_bus_message_enter_container(reply, 'a', "{sa{sv}}") < 0) + return -1; + while (sd_bus_message_enter_container(reply, 'e', "sa{sv}") > 0) { + const char *iface; + if (sd_bus_message_read_basic(reply, 's', &iface) <= 0) + return -1; + + new_dbus_interface(data, ifacepath, iface, reply); + if (sd_bus_message_exit_container(reply) < 0) + return -1; + } + if (sd_bus_message_exit_container(reply) < 0) + return -1; + + if (sd_bus_message_exit_container(reply) < 0) + return -1; + } + return 0; +} + +void * +fido_ble_open(const char *path) +{ + struct ble *dev; + sd_bus_message *reply = NULL; + int ret; + if (!fido_is_ble(path)) + return NULL; + + path += strlen(FIDO_BLE_PREFIX); + + dev = calloc(1, sizeof(*dev)); + if (!dev) + return NULL; + + dev->status_fd = -1; + dev->paths.dev = strdup(path); + if (!dev->paths.dev) + goto out; + + if (sd_bus_default_system(&dev->bus) < 0) + goto out; + + if (sd_bus_call_method(dev->bus, "org.bluez", + path, "org.freedesktop.DBus.Properties", "GetAll", NULL, &reply, + "s", DBUS_DEV_IFACE) < 0) + goto out; + + if (ble_fido_is_useable_device(reply, NULL) <= 0) + goto out; + + sd_bus_message_unref(reply); + reply = NULL; + ret = sd_bus_call_method(dev->bus, "org.bluez", "/", "org.freedesktop.DBus.ObjectManager", "GetManagedObjects", NULL, &reply, ""); + if (ret <= 0) + goto out; + + sd_bus_message_rewind(reply, 1); + if (iterate_over_all_objs(reply, collect_device_chars, dev) < 0) + goto out; + + sd_bus_message_unref(reply); + reply = NULL; + + if (dev->status_fd >=0 && + dev->paths.control_point && + dev->controlpoint_size > 0 && + dev->paths.service_revision) { + int revision; + revision = read_revision(dev->bus, dev->paths.service_revision); + if (revision < 0) + goto out; + + /* for simplicity, we allow now only FIDO2 */ + if (!(revision & 0x20)) + goto out; + + if (write_revision(dev->bus, dev->paths.service_revision, 0x20) < 0) + goto out; + + return dev; + } +out: + if (reply) + sd_bus_message_unref(reply); + + fido_ble_close(dev); + return NULL; +} + +void fido_ble_close(void *handle) +{ + struct ble *dev = (struct ble *)handle; + if (dev->status_fd >= 0) + close(dev->status_fd); + free(dev->paths.service_revision); + free(dev->paths.control_point); + free(dev->paths.service); + free(dev->paths.dev); + if (dev->bus) + sd_bus_unref(dev->bus); + + free(dev); +} + +int +fido_ble_read(void *handle, unsigned char *buf, size_t len, int ms) +{ + struct ble *dev = (struct ble *)handle; + ssize_t r; + if (fido_hid_unix_wait(dev->status_fd, ms, NULL) < 0) + return -1; + + r = read(dev->status_fd, buf, len); + if (r < 0) + return -1; + + return (int)r; +} + +int +fido_ble_write(void *handle, const unsigned char *buf, size_t len) +{ + struct ble *dev = (struct ble *)handle; + sd_bus_message *send_msg; + int r = sd_bus_message_new_method_call(dev->bus, &send_msg, "org.bluez", + dev->paths.control_point, + DBUS_CHAR_IFACE, "WriteValue"); + if (r < 0) + goto out; + + r = sd_bus_message_append_array(send_msg, 'y', buf, len); + if (r < 0) + goto out; + + r = sd_bus_message_append(send_msg, "a{sv}", 0); + if (r < 0) + goto out; + + r = sd_bus_call(dev->bus, send_msg, 0, NULL, NULL); +out: + sd_bus_message_unref(send_msg); + + return (r >= 0) ? (int)len : -1; +} + +size_t +fido_ble_get_cp_size(fido_dev_t *d) +{ + return ((struct ble *)d->io_handle)->controlpoint_size; +} + + +static int +ble_fido_is_useable_device(sd_bus_message * reply, const char **name) +{ + int ret; + int connected = 0; + int paired = 0; + int resolved = 0; + bool has_service = false; + + if (sd_bus_message_enter_container(reply, 'a', "{sv}") < 0) + return -1; + + while (sd_bus_message_enter_container(reply, 'e', "sv") > 0) { + const char *propname; + ret = sd_bus_message_read_basic(reply, 's', &propname); + if (ret <= 0) + return -1; + + if (!strcmp(propname, "Connected")) { + if (sd_bus_message_read(reply, "v", "b", &connected) < 0) + return -1; + } else if (!strcmp(propname, "Paired")) { + if (sd_bus_message_read(reply, "v", "b", &paired) < 0) + return -1; + } else if (!strcmp(propname, "ServicesResolved")) { + if (sd_bus_message_read(reply, "v", "b", &resolved) < 0) + return -1; + } else if (!strcmp(propname, "Name")) { + if (sd_bus_message_read(reply, "v", "s", name) < 0) + return -1; + } else if (!strcmp(propname, "UUIDs")) { + if (sd_bus_message_enter_container(reply, 'v', "as") < 0) + return -1; + if (sd_bus_message_enter_container(reply, 'a', "s") < 0) + return -1; + + const char *uuid; + while(sd_bus_message_read_basic(reply, 's', &uuid) > 0) { + if (!strcasecmp(uuid, FIDO_SERVICE_UUID)) + has_service = true; + + } + if (sd_bus_message_exit_container(reply) < 0) /* s */ + return -1; + if (sd_bus_message_exit_container(reply) < 0) /* as */ + return -1; + } else { + sd_bus_message_skip(reply,"v"); + } + if (sd_bus_message_exit_container(reply) < 0) /* sv */ + return -1; + } + sd_bus_message_exit_container(reply); /* {sv} */ + return ((connected != 0) && (resolved != 0) && has_service && (paired != 0)) ? 1 : 0; +} + +static int +init_ble_fido_dev(fido_dev_info_t *di, + const char *path, const char *name) +{ + memset(di, 0, sizeof(*di)); + if (asprintf(&di->path, "%s%s", FIDO_BLE_PREFIX, path) == -1) { + di->path = NULL; + } + + if (di->path && + (di->manufacturer = strdup("BLE")) && + (di->product = strdup(name))) { + di->io = (fido_dev_io_t) { + fido_ble_open, + fido_ble_close, + fido_ble_read, + fido_ble_write, + }; + di->transport = (fido_dev_transport_t) { + fido_ble_rx, + fido_ble_tx, + }; + + return 0; + } + + free(di->product); + free(di->manufacturer); + free(di->path); + explicit_bzero(di, sizeof(*di)); + + return -1; +} + +static int +fido_ble_add_device(void *data, const char *path, const char *iface, sd_bus_message *reply) +{ + struct manifest_ctx *ctx = (struct manifest_ctx *) data; + const char *name; + int r; + + if (strcmp(iface, DBUS_DEV_IFACE)) + return sd_bus_message_skip(reply, "a{sv}") < 0 ? -1 : 0; + + r = ble_fido_is_useable_device(reply, &name); + if (r <= 0) + return r; + + if (*ctx->olen < ctx->ilen) { + if (!init_ble_fido_dev(&ctx->devlist[*ctx->olen], path, name)) + (*ctx->olen)++; + } + + return 0; +} + +int +fido_ble_manifest(fido_dev_info_t *devlist, size_t ilen, size_t *olen) +{ + sd_bus *bus; + sd_bus_message *reply; + int ret; + struct manifest_ctx ctx; + + *olen = 0; + if (ilen == 0) + return FIDO_OK; + if (devlist == NULL) + return FIDO_ERR_INVALID_ARGUMENT; + + ctx.devlist = devlist; + ctx.olen = olen; + ctx.ilen = ilen; + if (0 > sd_bus_default_system(&bus)) + return FIDO_ERR_INTERNAL; + + ctx.bus = bus; + ret = sd_bus_call_method(bus, "org.bluez", "/", "org.freedesktop.DBus.ObjectManager", + "GetManagedObjects", NULL, &reply, ""); + if (ret <= 0) { + sd_bus_unref(bus); + return FIDO_ERR_INTERNAL; + } + + sd_bus_message_rewind(reply, 1); + /* search what is connected */ + iterate_over_all_objs(reply, fido_ble_add_device, &ctx); + + sd_bus_message_unref(reply); + sd_bus_unref(bus); + return FIDO_OK; +} diff --git a/src/dev.c b/src/dev.c index 2d662a6c..9a6eefdb 100644 --- a/src/dev.c +++ b/src/dev.c @@ -263,6 +263,9 @@ fido_dev_info_manifest(fido_dev_info_t *devlist, size_t ilen, size_t *olen) { *olen = 0; +#ifdef USE_BLE + run_manifest(devlist, ilen, olen, "ble", fido_ble_manifest); +#endif run_manifest(devlist, ilen, olen, "hid", fido_hid_manifest); #ifdef USE_NFC run_manifest(devlist, ilen, olen, "nfc", fido_nfc_manifest); @@ -305,6 +308,12 @@ fido_dev_open(fido_dev_t *dev, const char *path) return FIDO_ERR_INTERNAL; } #endif +#ifdef USE_BLE + if (fido_is_ble(path) && fido_dev_set_ble(dev) < 0) { + fido_log_debug("%s: fido_dev_set_pcsc", __func__); + return FIDO_ERR_INTERNAL; + } +#endif return (fido_dev_open_wait(dev, path, &ms)); } diff --git a/src/extern.h b/src/extern.h index 1bc95b27..3ce27b54 100644 --- a/src/extern.h +++ b/src/extern.h @@ -151,6 +151,17 @@ int fido_winhello_get_assert(fido_dev_t *, fido_assert_t *, const char *, int); int fido_winhello_get_cbor_info(fido_dev_t *, fido_cbor_info_t *); int fido_winhello_make_cred(fido_dev_t *, fido_cred_t *, const char *, int); +/* ble */ +bool fido_is_ble(const char *); +int fido_dev_set_ble(fido_dev_t *); +void *fido_ble_open(const char *); +void fido_ble_close(void *); +int fido_ble_read(void *, unsigned char *, size_t, int); +int fido_ble_write(void *, const unsigned char *, size_t); +int fido_ble_rx(fido_dev_t *, uint8_t, unsigned char *, size_t, int); +int fido_ble_tx(fido_dev_t *, uint8_t, const unsigned char *, size_t); +size_t fido_ble_get_cp_size(fido_dev_t *d); + /* generic i/o */ int fido_rx_cbor_status(fido_dev_t *, int *); int fido_rx(fido_dev_t *, uint8_t, void *, size_t, int *); @@ -239,6 +250,7 @@ int fido_get_signed_hash_tpm(fido_blob_t *, const fido_blob_t *, const fido_blob_t *, const fido_attstmt_t *, const fido_attcred_t *); /* device manifest functions */ +int fido_ble_manifest(fido_dev_info_t *, size_t, size_t *); int fido_hid_manifest(fido_dev_info_t *, size_t, size_t *); int fido_nfc_manifest(fido_dev_info_t *, size_t, size_t *); int fido_pcsc_manifest(fido_dev_info_t *, size_t, size_t *); @@ -268,6 +280,7 @@ uint32_t uniform_random(uint32_t); #define FIDO_WINHELLO_PATH "windows://hello" #define FIDO_NFC_PREFIX "nfc:" #define FIDO_PCSC_PREFIX "pcsc:" +#define FIDO_BLE_PREFIX "ble:" #ifdef __cplusplus } /* extern "C" */