Skip to content

Commit

Permalink
Event-based Transport/Token API / USB HID rewrite (kanidm#324)
Browse files Browse the repository at this point in the history
* WIP: windows usb hid

* basic windows HID works

* break things

* windows usb hotplug works!

* cleanup error handlers

* another stray error

* guard USB support

* document and tidy some things

* add platforms

* start macos support

* start wrapping types

* mac: hanging thread

* mac: needs a little more wires...

* event propagates

* mac: basics working, next is cleanup

* mac: unregister event handlers from correct thread

* mac: add synthetic enumeration_completed event

* mac: cleanup and improve tests

* more cleaning

* mac: more tidying, removed unneeded code...

* mac: reorganising functions

* mac: implement get_devices, remove debugging messages, more tidying

* mac: move IOHIDManager methods into impl

* mac: move methods into IOHIDHevice

* mac: replace enums

* mac: un-rename iohid symbols

* remove unused import

* start implementing linux

* rewrite descriptor parser to iterator model

* linux: update notes, remove unneeded code

* linux: implement device discovery

* linux: denoise

* use kernel definitions for types

* tidying, start rewriting fido-key-manager

* clean up some types, document linux behaviour

* nfc: start refactoring, add `info --watch` mode

* nfc: less noise

* fix up some NFC stuff, ignore silly readers

* nfc: improve enumeration

* start implementing bluetooth support again, fixing up a stupid macos PIV problem

* macos: clean up a bunch of imports and usage of std::thread

* delete a bunch of unused code

* split platform-specific USB HID code into its own library (WIP)

* tidy tidy

* exclude fido-hid-rs from first doc pass

* fixup linux issues, move errors into own file

* more tidying

* spawn_blocking on linux

* cleanup mac stuff

* clean up some dependency chains

* add dbus

* fix up clippy

* fake bluetooth events

* start migrating authenticate example

* WIP: macos broken, refactoring...

* refactor mac code to keep manager alive

* cleanup more mac stuff

* mac: delete more unneeded code

* typo

* fixup windows

* fixup linux

* docs updates, bluetooth tweaking

* fixup linux crosscompile

* linux again

* more linux

* Migrate NFC conformance test to new API

* tidy up conformance tests

* update cable_tunnel example for new API

* start implementing selection, roll back Transport name changes

* improve some windows things

* document selection

* type fun

* migrate more comands

* blah

* start openssl notes

* fido-key-manager: migrate fingerprints to new api

* add remaining fido-key-manager commands

* handle wine NFC stubs

* quiet clippy

* nfc stubs fix

* format code,

* fix up a bunch of broken

* cleanup more lint issues
  • Loading branch information
micolous authored and kikuomax committed Nov 2, 2024
1 parent 214503e commit d171ac3
Show file tree
Hide file tree
Showing 39 changed files with 3,832 additions and 779 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
- if: runner.os != 'windows'
run: |
sudo apt-get update && \
sudo apt-get -y install libudev-dev libpcsclite-dev libusb-1.0-0-dev
sudo apt-get -y install libdbus-1-dev libpcsclite-dev libudev-dev libusb-1.0-0-dev
- if: runner.os == 'windows'
uses: johnwason/vcpkg-action@v4
Expand Down
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ members = [
# The actually library.
"webauthn-rs",
# Authenticator interactions
"fido-hid-rs",
"webauthn-authenticator-rs",
# caBLE tunnel server
"cable-tunnel-server/backend",
Expand Down Expand Up @@ -40,6 +41,7 @@ exclude = [
[workspace.dependencies]
base64urlsafedata = { path = "./base64urlsafedata" }
cable-tunnel-server-common = { path = "./cable-tunnel-server/common", version = "0.1.0" }
fido-hid-rs = { path = "./fido-hid-rs" }
webauthn-authenticator-rs = { path = "./webauthn-authenticator-rs" }
webauthn-rs = { path = "./webauthn-rs" }
webauthn-rs-core = { path = "./webauthn-rs-core" }
Expand All @@ -65,6 +67,7 @@ tide = "0.16"
thiserror = "^1.0.37"
tokio = { version = "1.22.0", features = ["sync", "test-util", "macros", "rt-multi-thread", "time"] }
tokio-native-tls = "^0.3.1"
tokio-stream = { version = "0.1", features = ["sync"] }
tokio-tungstenite = { version = "^0.18.0", features = ["native-tls"] }
tracing = "^0.1.35"
tracing-subscriber = { version = "0.3", features = ["env-filter", "std", "fmt"] }
Expand Down
48 changes: 48 additions & 0 deletions fido-hid-rs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
[package]
name = "fido-hid-rs"
version = "0.5.0-dev"
edition = "2021"
rust-version = "1.66.0"
license = "MPL-2.0"
description = "USB HID library for FIDO authenticators"
repository = "https://github.com/kanidm/webauthn-rs"


# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
async-trait = "0.1.58"
bitflags = "1.3.2"
futures.workspace = true
thiserror.workspace = true
tokio.workspace = true
tokio-stream.workspace = true
tracing.workspace = true

[build-dependencies]
# Actually only required when targeting Linux, but Cargo doesn't support
# build-dependencies for a single target, and you might be cross-compiling.
bindgen = "0.65.1"

[dev-dependencies]
tracing-subscriber.workspace = true

# descriptors tests
hex.workspace = true
num-derive = "0.3"
num-traits = "0.2"

[target.'cfg(target_os = "windows")'.dependencies]
lazy_static = "1.4.0"
windows = { version = "0.41.0", features = ["Win32_Foundation", "Win32_Devices_HumanInterfaceDevice", "Devices_Enumeration", "Devices_HumanInterfaceDevice", "Foundation", "Foundation_Collections", "Storage", "Storage_Streams" ] }

[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "0.9"
libc = "0.2"
mach2 = "0.4"

[target.'cfg(target_os = "linux")'.dependencies]
nix = { version = "0.26.2", features = ["ioctl", "poll"] }
num-derive = "0.3"
num-traits = "0.2"
udev = "0.7.0"
32 changes: 32 additions & 0 deletions fido-hid-rs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# fido-hid-rs

`fido-hid-rs` implements a minimal set of platform-specific USB HID bindings for
communicating with FIDO authenticators.

> **Important:** this library is an _internal implementation detail_ of
> [webauthn-authenticator-rs][0] to work around Cargo limitations.
>
> **This library has no guarantees of API stability, and is not intended for use
> by other parties.**
>
> If you want to interface with USB HID FIDO authenticators, use
> [webauthn-authenticator-rs][0] instead of this library.
>
> If you're looking for a general-purpose Rust USB HID library, try [hidapi][].
This library currently targets (and is regularly tested on):

* Linux on `x86_64` (target version TBC)
* macOS 13 and later on `arm64` (Apple silicon) and `x86_64`
* Windows 10 on `x86_64` and Windows 11 on `arm64` and `x86_64`

We only test on the **current** service pack or point release of these operating
systems.

Other platforms (and older releases of these operating systems) are supported on
a "passive" basis only: it might work, but we generally don't have the
appropriate hardware available, and rely on users to notify us when things go
wrong and provide patches! ♥️

[0]: ../webauthn-authenticator-rs
[hidapi]: https://docs.rs/hidapi/latest/hidapi/
31 changes: 31 additions & 0 deletions fido-hid-rs/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use std::{env, path::PathBuf};

const LINUX_WRAPPER_H: &str = "src/linux/wrapper.h";

fn linux_headers() {
println!("cargo:rerun-if-changed={LINUX_WRAPPER_H}");
let bindings = bindgen::builder()
.header(LINUX_WRAPPER_H)
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
.derive_debug(false)
.derive_default(true)
.allowlist_type("hidraw_report_descriptor")
.allowlist_type("hidraw_devinfo")
.allowlist_var("HID_MAX_DESCRIPTOR_SIZE")
.allowlist_var("BUS_(USB|BLUETOOTH|VIRTUAL)")
.generate()
.expect("unable to generate bindings");

let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("linux_wrapper.rs"))
.expect("Couldn't write bindings!");
}

fn main() {
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();

if target_os == "linux" {
linux_headers();
}
}
211 changes: 211 additions & 0 deletions fido-hid-rs/src/descriptors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
//! USB HID report descriptor parser.
//!
//! ## References
//!
//! * [Device Class Definition for Human Interface Devices, version 1.11][0],
//! §6.2.2 "Report Descriptor"
//!
//! [0]: https://www.usb.org/sites/default/files/documents/hid1_11.pdf
use num_derive::FromPrimitive;
use num_traits::FromPrimitive;

use crate::{FIDO_USAGE_PAGE, FIDO_USAGE_U2FHID};

/// HID descriptor tags; shifted right by 2 bits (removing the `bSize` field).
#[derive(FromPrimitive)]
#[repr(u8)]
enum Tag {
// Main items
Input = 0b1000_00,
Output = 0b1001_00,
Feature = 0b1011_00,
Collection = 0b1010_00,
EndCollection = 0b1100_00,

// Global items
UsagePage = 0b0000_01,
LogicalMinimum = 0b0001_01,
LogicalMaximum = 0b0010_01,
PhysicalMinimum = 0b0011_01,
PhysicalMaximum = 0b0100_01,
UnitExponent = 0b0101_01,
Unit = 0b0110_01,
ReportSize = 0b0111_01,
ReportID = 0b1000_01,
ReportCount = 0b1001_01,
Push = 0b1010_01,
Pop = 0b1011_01,

// Local items
Usage = 0b0000_10,
UsageMinimum = 0b0001_10,
UsageMaximum = 0b0010_10,
DesignatorIndex = 0b0011_10,
DesignatorMinimum = 0b0100_10,
DesignatorMaximum = 0b0101_10,
StringIndex = 0b0111_10,
StringMinimum = 0b1000_10,
StringMaximum = 0b1001_10,
Delimiter = 0b1010_10,
}

/// Item in a report descriptor.
struct DescriptorItem<'a> {
/// The tag of the item, if known.
tag: Option<Tag>,

/// The value of the item.
value: &'a [u8],
}

/// Iterator-based report descriptor parser.
///
/// This returns each [item] in a descriptor, without regard to its context.
///
/// ## Limitations
///
/// This only fully supports short items. This will parse long items, but skip
/// the tag.
///
/// [item]: DescriptorItem
struct DescriptorIterator<'a> {
i: &'a [u8],
}

impl<'a> Iterator for DescriptorIterator<'a> {
type Item = DescriptorItem<'a>;

fn next(&mut self) -> Option<Self::Item> {
if self.i.is_empty() {
return None;
}

let tag;
let value;

let mut i0 = self.i[0];
if i0 == 0xfe {
// Long item: 0xfe [size] [tag] [data...]
if self.i.len() < 3 {
// Not enough bytes to get the long item length and tag
return None;
}

let length = usize::from(self.i[1]);
if self.i.len() < length + 3 {
// Not enough bytes to get long item value
return None;
}
warn!("long tags are not supported");
tag = None;
(value, self.i) = self.i[3..].split_at(length);
} else {
// Short item: [tag | type | size] [data...]
let mut length = usize::from(i0 & 0x03);
if length == 0x03 {
length += 1;
}
i0 >>= 2;

tag = Tag::from_u8(i0);
// if tag.is_none() {
// warn!("unknown short tag: 0b{i0:b}",);
// }
if self.i.len() < length + 1 {
// Not enough bytes to get short item value
return None;
}
(value, self.i) = self.i[1..].split_at(length);
}

Some(DescriptorItem { tag, value })
}
}

/// Parses a USB HID Report Descriptor to determine whether it is a FIDO
/// authenticator.
///
/// This only handles [`Tag::UsagePage`] and [`Tag::Usage`], so will only
/// support simple descriptors. This is should be sufficient for USB FIDO
/// authethicators.
pub fn is_fido_authenticator(descriptor: &[u8]) -> bool {
let mut descriptor = DescriptorIterator { i: descriptor };
let mut current_usage_page = 0u16;
while let Some(item) = descriptor.next() {
// trace!("item: {item:?}");
match item.tag {
Some(Tag::UsagePage) => {
if let Ok(usage_page) = item.value.try_into() {
current_usage_page = u16::from_le_bytes(usage_page);
}
}

Some(Tag::Usage) => {
if current_usage_page == FIDO_USAGE_PAGE {
// 1 or 2 byte usage page; expect the current usage page to be FIDO
if item.value.len() == 1 && u16::from(item.value[0]) == FIDO_USAGE_U2FHID {
return true;
}

if let Ok(usage) = item.value.try_into() {
if u16::from_le_bytes(usage) == FIDO_USAGE_U2FHID {
return true;
}
}
}

if let Ok(usage) = item.value.try_into() {
// 4 byte usage page; doesn't matter what the current usage page is
let usage_and_page = u32::from_le_bytes(usage);
let usage_page = (usage_and_page >> 16) as u16;
let usage = (usage_and_page & 0xffff) as u16;

if usage_page == FIDO_USAGE_PAGE && usage == FIDO_USAGE_U2FHID {
return true;
}
}
}

_ => continue,
}
}
return false;
}

#[cfg(test)]
mod test {
use super::*;

macro_rules! fido_descriptor_tests {
($($name:ident: $expected:expr, $value:expr;)*) => {
$(
#[test]
fn $name() -> std::result::Result<(), Box<dyn std::error::Error>> {
let _ = tracing_subscriber::fmt().try_init();

let descriptor = hex::decode($value)?;
assert_eq!($expected, is_fido_authenticator(&descriptor));
Ok(())
}
)*
}
}

fido_descriptor_tests! {
// Used by Feitian and Token2; 2 byte usage page + 1 byte usage
feitian_token2: true, "06d0f10901a1010920150026ff007508954081020921150026ff00750895409102c0";

feitian_otp: false, "05010906a101050719e029e71500250175019508810295017508810395057501050819012905910295017503910395067508150025650507190029658100090375089540b102c0";
keyboard1: false, "05010906a101050719e029e71500250175019508810295017508810195037501050819012903910295057501910195067508150026ff00050719002aff008100c0";
keyboard2: false, "0601000980a10185011981298315002501950375018102950175058101c0050c0901a1018503150025017501950819b529b809cd09e209e909ea81020a83010a8a010a92010a94010a21021a23022a250281020a26020a27020a2a029503810295058101c00600ff0901a1018502250115007501950b1af1002afb008102950175058101150026ff7f092075109501b102c0";
mouse1: false, "05010902a1010901a100050919012908150025019508750181020600ff0940950275081581257f810205010938950181060930093116018026ff7f751095028106c0c0";
mouse2: false, "0680ff0980a10185801a00382a0738150025019508750181028520092095017508b102858e098eb102c0";

invalid_1_byte: false, "01";
invalid_2_byte: false, "0201";
invalid_4_byte: false, "02010203";
invalid_long: false, "fe";
invalid_long_usage_page: false, "0701020304";
}
}
25 changes: 25 additions & 0 deletions fido-hid-rs/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use thiserror::Error;

pub type Result<T> = std::result::Result<T, HidError>;

#[derive(Debug, Error, PartialEq, Eq, PartialOrd, Ord)]
pub enum HidError {
#[error("I/O error communicating with device: {0}")]
IoError(String),
#[error("internal error, likely library bug")]
Internal,
#[error("attempted to communicate with a closed device")]
Closed,
#[error("device sent an unexpected message length")]
InvalidMessageLength,
#[error("could not send data to device")]
SendError,
#[error("permission denied")]
PermissionDenied,
}

impl From<std::io::Error> for HidError {
fn from(v: std::io::Error) -> Self {
Self::IoError(v.to_string())
}
}
Loading

0 comments on commit d171ac3

Please sign in to comment.