From 080d3b14470f6ac59f4cfb97a4200ed18df5c260 Mon Sep 17 00:00:00 2001 From: Fabian Henneke Date: Wed, 21 Aug 2019 11:17:59 +0200 Subject: [PATCH] udev: Add id program and rule for FIDO security tokens Add a fido_id program meant to be run for devices in the hidraw subsystem via an IMPORT directive. The program parses the HID report descriptor and assigns the ID_SECURITY_TOKEN environment variable if a declared usage matches the FIDO_CTAPHID_USAGE declared in the FIDO CTAP specification. This replaces the previous approach of whitelisting all known security token models manually. This commit is accompanied by a test suite and a fuzzer target for the descriptor parsing routine. Fixes: #11996. (cherry picked from commit d45ee2f31a8358db0accde2e7c81777cedadc3c2) Resolves: #1753369 --- rules/60-fido-id.rules | 7 ++ rules/meson.build | 1 + src/fuzz/fuzz-fido-id-desc.c | 23 +++++++ src/fuzz/fuzz-fido-id-desc.dict | 6 ++ src/fuzz/meson.build | 4 ++ src/test/meson.build | 4 ++ src/test/test-fido-id-desc.c | 85 +++++++++++++++++++++++ src/udev/fido_id/fido_id.c | 103 ++++++++++++++++++++++++++++ src/udev/fido_id/fido_id_desc.c | 92 +++++++++++++++++++++++++ src/udev/fido_id/fido_id_desc.h | 8 +++ src/udev/meson.build | 3 + test/fuzz/fuzz-fido-id-desc/crash0 | 1 + test/fuzz/fuzz-fido-id-desc/crash1 | 1 + test/fuzz/fuzz-fido-id-desc/report0 | Bin 0 -> 71 bytes test/fuzz/fuzz-fido-id-desc/report1 | Bin 0 -> 34 bytes 15 files changed, 338 insertions(+) create mode 100644 rules/60-fido-id.rules create mode 100644 src/fuzz/fuzz-fido-id-desc.c create mode 100644 src/fuzz/fuzz-fido-id-desc.dict create mode 100644 src/test/test-fido-id-desc.c create mode 100644 src/udev/fido_id/fido_id.c create mode 100644 src/udev/fido_id/fido_id_desc.c create mode 100644 src/udev/fido_id/fido_id_desc.h create mode 100644 test/fuzz/fuzz-fido-id-desc/crash0 create mode 100644 test/fuzz/fuzz-fido-id-desc/crash1 create mode 100644 test/fuzz/fuzz-fido-id-desc/report0 create mode 100644 test/fuzz/fuzz-fido-id-desc/report1 diff --git a/rules/60-fido-id.rules b/rules/60-fido-id.rules new file mode 100644 index 0000000000..fcf5079704 --- /dev/null +++ b/rules/60-fido-id.rules @@ -0,0 +1,7 @@ +# do not edit this file, it will be overwritten on update + +ACTION=="remove", GOTO="fido_id_end" + +SUBSYSTEM=="hidraw", IMPORT{program}="fido_id" + +LABEL="fido_id_end" diff --git a/rules/meson.build b/rules/meson.build index b6aae596b6..6363f8bf2e 100644 --- a/rules/meson.build +++ b/rules/meson.build @@ -7,6 +7,7 @@ rules = files(''' 60-cdrom_id.rules 60-drm.rules 60-evdev.rules + 60-fido-id.rules 60-input-id.rules 60-persistent-alsa.rules 60-persistent-input.rules diff --git a/src/fuzz/fuzz-fido-id-desc.c b/src/fuzz/fuzz-fido-id-desc.c new file mode 100644 index 0000000000..cf98dee044 --- /dev/null +++ b/src/fuzz/fuzz-fido-id-desc.c @@ -0,0 +1,23 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include +#include +#include + +#include "fido_id/fido_id_desc.h" +#include "fuzz.h" +#include "log.h" + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + /* We don't want to fill the logs with messages about parse errors. + * Disable most logging if not running standalone */ + if (!getenv("SYSTEMD_LOG_LEVEL")) + log_set_max_level(LOG_CRIT); + + if (size > HID_MAX_DESCRIPTOR_SIZE) + return 0; + (void) is_fido_security_token_desc(data, size); + + return 0; +} diff --git a/src/fuzz/fuzz-fido-id-desc.dict b/src/fuzz/fuzz-fido-id-desc.dict new file mode 100644 index 0000000000..d2d2679e18 --- /dev/null +++ b/src/fuzz/fuzz-fido-id-desc.dict @@ -0,0 +1,6 @@ +"\xfe" +"\x00" +"\x01" +"\xf1" +"\xd0" +"\xf1\xd0\x00\x01" diff --git a/src/fuzz/meson.build b/src/fuzz/meson.build index 1dbe28e57e..483a952421 100644 --- a/src/fuzz/meson.build +++ b/src/fuzz/meson.build @@ -47,4 +47,8 @@ fuzzers += [ [libsystemd_journal_remote, libshared], []], + [['src/fuzz/fuzz-fido-id-desc.c', + 'src/udev/fido_id/fido_id_desc.c'], + [], + []] ] diff --git a/src/test/meson.build b/src/test/meson.build index 0998f59897..4259421f98 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -663,6 +663,10 @@ tests += [ [['src/test/test-bus-util.c'], [], []], + [['src/test/test-fido-id-desc.c', + 'src/udev/fido_id/fido_id_desc.c'], + [], + []], ] ############################################################ diff --git a/src/test/test-fido-id-desc.c b/src/test/test-fido-id-desc.c new file mode 100644 index 0000000000..cf55dd3266 --- /dev/null +++ b/src/test/test-fido-id-desc.c @@ -0,0 +1,85 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include + +#include "fido_id/fido_id_desc.h" +#include "macro.h" + +static void test_is_fido_security_token_desc__fido(void) { + static const uint8_t FIDO_HID_DESC_1[] = { + 0x06, 0xd0, 0xf1, 0x09, 0x01, 0xa1, 0x01, 0x09, 0x20, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, + 0x08, 0x95, 0x40, 0x81, 0x02, 0x09, 0x21, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, + 0x40, 0x91, 0x02, 0xc0, + }; + assert_se(is_fido_security_token_desc(FIDO_HID_DESC_1, sizeof(FIDO_HID_DESC_1)) > 0); + + static const uint8_t FIDO_HID_DESC_2[] = { + 0x05, 0x01, 0x09, 0x06, 0xa1, 0x01, 0x05, 0x07, 0x19, 0xe0, 0x29, 0xe7, 0x15, 0x00, 0x25, + 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x05, + 0x75, 0x01, 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91, + 0x01, 0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, 0x00, 0x29, 0x65, + 0x81, 0x00, 0x09, 0x03, 0x75, 0x08, 0x95, 0x08, 0xb1, 0x02, 0xc0, + 0x06, 0xd0, 0xf1, 0x09, 0x01, 0xa1, 0x01, 0x09, 0x20, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, + 0x08, 0x95, 0x40, 0x81, 0x02, 0x09, 0x21, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, + 0x40, 0x91, 0x02, 0xc0, + }; + assert_se(is_fido_security_token_desc(FIDO_HID_DESC_2, sizeof(FIDO_HID_DESC_2)) > 0); +} + +static void test_is_fido_security_token_desc__non_fido(void) { + /* Wrong usage page */ + static const uint8_t NON_FIDO_HID_DESC_1[] = { + 0x06, 0xd0, 0xf0, 0x09, 0x01, 0xa1, 0x01, 0x09, 0x20, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, + 0x08, 0x95, 0x40, 0x81, 0x02, 0x09, 0x21, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, + 0x40, 0x91, 0x02, 0xc0, + }; + assert_se(is_fido_security_token_desc(NON_FIDO_HID_DESC_1, sizeof(NON_FIDO_HID_DESC_1)) == 0); + + /* Wrong usage */ + static const uint8_t NON_FIDO_HID_DESC_2[] = { + 0x06, 0xd0, 0xf1, 0x09, 0x02, 0xa1, 0x01, 0x09, 0x20, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, + 0x08, 0x95, 0x40, 0x81, 0x02, 0x09, 0x21, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, + 0x40, 0x91, 0x02, 0xc0, + }; + assert_se(is_fido_security_token_desc(NON_FIDO_HID_DESC_2, sizeof(NON_FIDO_HID_DESC_2)) == 0); + + static const uint8_t NON_FIDO_HID_DESC_3[] = { + 0x05, 0x01, 0x09, 0x06, 0xa1, 0x01, 0x05, 0x07, 0x19, 0xe0, 0x29, 0xe7, 0x15, 0x00, 0x25, + 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x05, + 0x75, 0x01, 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91, + 0x01, 0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, 0x00, 0x29, 0x65, + 0x81, 0x00, 0x09, 0x03, 0x75, 0x08, 0x95, 0x08, 0xb1, 0x02, 0xc0, + }; + assert_se(is_fido_security_token_desc(NON_FIDO_HID_DESC_3, sizeof(NON_FIDO_HID_DESC_3)) == 0); +} + +static void test_is_fido_security_token_desc__invalid(void) { + /* Size coded on 1 byte, but no byte given */ + static const uint8_t INVALID_HID_DESC_1[] = { 0x01 }; + assert_se(is_fido_security_token_desc(INVALID_HID_DESC_1, sizeof(INVALID_HID_DESC_1)) < 0); + + /* Size coded on 2 bytes, but only 1 byte given */ + static const uint8_t INVALID_HID_DESC_2[] = { 0x02, 0x01 }; + assert_se(is_fido_security_token_desc(INVALID_HID_DESC_2, sizeof(INVALID_HID_DESC_2)) < 0); + + /* Size coded on 4 bytes, but only 3 bytes given */ + static const uint8_t INVALID_HID_DESC_3[] = { 0x03, 0x01, 0x02, 0x03 }; + assert_se(is_fido_security_token_desc(INVALID_HID_DESC_3, sizeof(INVALID_HID_DESC_3)) < 0); + + /* Long item without a size byte */ + static const uint8_t INVALID_HID_DESC_4[] = { 0xfe }; + assert_se(is_fido_security_token_desc(INVALID_HID_DESC_4, sizeof(INVALID_HID_DESC_4)) < 0); + + /* Usage pages are coded on at most 2 bytes */ + static const uint8_t INVALID_HID_DESC_5[] = { 0x07, 0x01, 0x02, 0x03, 0x04 }; + assert_se(is_fido_security_token_desc(INVALID_HID_DESC_5, sizeof(INVALID_HID_DESC_5)) < 0); +} + +int main(int argc, char *argv[]) { + test_is_fido_security_token_desc__fido(); + test_is_fido_security_token_desc__non_fido(); + test_is_fido_security_token_desc__invalid(); + + return EXIT_SUCCESS; +} diff --git a/src/udev/fido_id/fido_id.c b/src/udev/fido_id/fido_id.c new file mode 100644 index 0000000000..7e1cc804f2 --- /dev/null +++ b/src/udev/fido_id/fido_id.c @@ -0,0 +1,103 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +/* + * Identifies FIDO CTAP1 ("U2F")/CTAP2 security tokens based on the usage declared in their report + * descriptor and outputs suitable environment variables. + * + * Inspired by Andrew Lutomirski's 'u2f-hidraw-policy.c' + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "sd-device.h" + +#include "device-internal.h" +#include "device-private.h" +#include "device-util.h" +#include "fd-util.h" +#include "fido_id_desc.h" +#include "log.h" +#include "macro.h" +#include "path-util.h" +#include "string-util.h" +#include "udev-util.h" + +static int run(int argc, char **argv) { + _cleanup_(sd_device_unrefp) struct sd_device *device = NULL; + _cleanup_free_ char *desc_path = NULL; + _cleanup_close_ int fd = -1; + + struct sd_device *hid_device; + const char *sys_path; + uint8_t desc[HID_MAX_DESCRIPTOR_SIZE]; + ssize_t desc_len; + + int r; + + log_set_target(LOG_TARGET_AUTO); + udev_parse_config(); + log_parse_environment(); + log_open(); + + if (argc > 2) + return log_error_errno(EINVAL, "Usage: %s [SYSFS_PATH]", program_invocation_short_name); + + if (argc == 1) { + r = device_new_from_strv(&device, environ); + if (r < 0) + return log_error_errno(r, "Failed to get current device from environment: %m"); + } else { + r = sd_device_new_from_syspath(&device, argv[1]); + if (r < 0) + return log_error_errno(r, "Failed to get device from syspath: %m"); + } + + r = sd_device_get_parent(device, &hid_device); + if (r < 0) + return log_device_error_errno(device, r, "Failed to get parent HID device: %m"); + + r = sd_device_get_syspath(hid_device, &sys_path); + if (r < 0) + return log_device_error_errno(hid_device, r, "Failed to get syspath for HID device: %m"); + + desc_path = path_join(NULL, sys_path, "report_descriptor"); + if (!desc_path) + return log_oom(); + + fd = open(desc_path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (fd < 0) + return log_device_error_errno(hid_device, errno, + "Failed to open report descriptor at '%s': %m", desc_path); + + desc_len = read(fd, desc, sizeof(desc)); + if (desc_len < 0) + return log_device_error_errno(hid_device, errno, + "Failed to read report descriptor at '%s': %m", desc_path); + if (desc_len == 0) + return log_device_debug_errno(hid_device, EINVAL, + "Empty report descriptor at '%s'.", desc_path); + + r = is_fido_security_token_desc(desc, desc_len); + if (r < 0) + return log_device_debug_errno(hid_device, r, + "Failed to parse report descriptor at '%s'.", desc_path); + if (r > 0) { + printf("ID_FIDO_TOKEN=1\n"); + printf("ID_SECURITY_TOKEN=1\n"); + } + + return 0; +} + +int main(int argc, char *argv[]) { + int r; + + r = run(argc, argv); + + return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS; +} diff --git a/src/udev/fido_id/fido_id_desc.c b/src/udev/fido_id/fido_id_desc.c new file mode 100644 index 0000000000..bbfcf93709 --- /dev/null +++ b/src/udev/fido_id/fido_id_desc.c @@ -0,0 +1,92 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +/* Inspired by Andrew Lutomirski's 'u2f-hidraw-policy.c' */ + +#include +#include +#include +#include + +#include "fido_id_desc.h" + +#define HID_RPTDESC_FIRST_BYTE_LONG_ITEM 0xfeu +#define HID_RPTDESC_TYPE_GLOBAL 0x1u +#define HID_RPTDESC_TYPE_LOCAL 0x2u +#define HID_RPTDESC_TAG_USAGE_PAGE 0x0u +#define HID_RPTDESC_TAG_USAGE 0x0u + +/* + * HID usage for FIDO CTAP1 ("U2F") and CTAP2 security tokens. + * https://fidoalliance.org/specs/fido-u2f-v1.0-ps-20141009/fido-u2f-u2f_hid.h-v1.0-ps-20141009.txt + * https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#usb-discovery + * https://www.usb.org/sites/default/files/hutrr48.pdf + */ +#define FIDO_FULL_USAGE_CTAPHID 0xf1d00001u + +/* + * Parses a HID report descriptor and identifies FIDO CTAP1 ("U2F")/CTAP2 security tokens based on their + * declared usage. + * A positive return value indicates that the report descriptor belongs to a FIDO security token. + * https://www.usb.org/sites/default/files/documents/hid1_11.pdf (Section 6.2.2) + */ +int is_fido_security_token_desc(const uint8_t *desc, size_t desc_len) { + uint32_t usage = 0; + + for (size_t pos = 0; pos < desc_len; ) { + uint8_t tag, type, size_code; + size_t size; + uint32_t value; + + /* Report descriptors consists of short items (1-5 bytes) and long items (3-258 bytes). */ + if (desc[pos] == HID_RPTDESC_FIRST_BYTE_LONG_ITEM) { + /* No long items are defined in the spec; skip them. + * The length of the data in a long item is contained in the byte after the long + * item tag. The header consists of three bytes: special long item tag, length, + * actual tag. */ + if (pos + 1 >= desc_len) + return -EINVAL; + pos += desc[pos + 1] + 3; + continue; + } + + /* The first byte of a short item encodes tag, type and size. */ + tag = desc[pos] >> 4; /* Bits 7 to 4 */ + type = (desc[pos] >> 2) & 0x3; /* Bits 3 and 2 */ + size_code = desc[pos] & 0x3; /* Bits 1 and 0 */ + /* Size is coded as follows: + * 0 -> 0 bytes, 1 -> 1 byte, 2 -> 2 bytes, 3 -> 4 bytes + */ + size = size_code < 3 ? size_code : 4; + /* Consume header byte. */ + pos++; + + /* Extract the item value coded on size bytes. */ + if (pos + size > desc_len) + return -EINVAL; + value = 0; + for (size_t i = 0; i < size; i++) + value |= (uint32_t) desc[pos + i] << (8 * i); + /* Consume value bytes. */ + pos += size; + + if (type == HID_RPTDESC_TYPE_GLOBAL && tag == HID_RPTDESC_TAG_USAGE_PAGE) { + /* A usage page is a 16 bit value coded on at most 16 bits. */ + if (size > 2) + return -EINVAL; + /* A usage page sets the upper 16 bits of a following usage. */ + usage = (value & 0x0000ffffu) << 16; + } + + if (type == HID_RPTDESC_TYPE_LOCAL && tag == HID_RPTDESC_TAG_USAGE) { + /* A usage is a 32 bit value, but is prepended with the current usage page if + * coded on less than 4 bytes (that is, at most 2 bytes). */ + if (size == 4) + usage = value; + else + usage = (usage & 0xffff0000u) | (value & 0x0000ffffu); + if (usage == FIDO_FULL_USAGE_CTAPHID) + return 1; + } + } + + return 0; +} diff --git a/src/udev/fido_id/fido_id_desc.h b/src/udev/fido_id/fido_id_desc.h new file mode 100644 index 0000000000..c813a3a454 --- /dev/null +++ b/src/udev/fido_id/fido_id_desc.h @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#pragma once + +#include +#include + +int is_fido_security_token_desc(const uint8_t *desc, size_t desc_len); diff --git a/src/udev/meson.build b/src/udev/meson.build index 3bcd2bd3d7..5931a6da7d 100644 --- a/src/udev/meson.build +++ b/src/udev/meson.build @@ -160,6 +160,9 @@ libudev_core = static_library( foreach prog : [['ata_id/ata_id.c'], ['cdrom_id/cdrom_id.c'], ['collect/collect.c'], + ['fido_id/fido_id.c', + 'fido_id/fido_id_desc.c', + 'fido_id/fido_id_desc.h'], ['scsi_id/scsi_id.c', 'scsi_id/scsi_id.h', 'scsi_id/scsi_serial.c', diff --git a/test/fuzz/fuzz-fido-id-desc/crash0 b/test/fuzz/fuzz-fido-id-desc/crash0 new file mode 100644 index 0000000000..e066656502 --- /dev/null +++ b/test/fuzz/fuzz-fido-id-desc/crash0 @@ -0,0 +1 @@ +Ì \ No newline at end of file diff --git a/test/fuzz/fuzz-fido-id-desc/crash1 b/test/fuzz/fuzz-fido-id-desc/crash1 new file mode 100644 index 0000000000..aef3e18335 --- /dev/null +++ b/test/fuzz/fuzz-fido-id-desc/crash1 @@ -0,0 +1 @@ +øûøûûÜ \ No newline at end of file diff --git a/test/fuzz/fuzz-fido-id-desc/report0 b/test/fuzz/fuzz-fido-id-desc/report0 new file mode 100644 index 0000000000000000000000000000000000000000..48757cba682ffddd5a1ddd8988bb8bcdc7db0a7a GIT binary patch literal 71 zcmZQ&