diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e9538a2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: HID runner + +on: + push: + branches: ["*"] + pull_request: + +env: + CARGO_TERM_COLOR: always + +jobs: + framing-tests: + runs-on: ubuntu-latest + env: + CARGO_TARGET_DIR: ${{ github.workspace }}/target + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libclang-dev + - name: Run ctaphid framing tests + run: cargo test -p pc-hid-runner --lib + + hid-integration: + runs-on: ubuntu-latest + needs: framing-tests + env: + CARGO_TARGET_DIR: ${{ github.workspace }}/target + PC_HID_RUNNER_E2E: "1" + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libclang-dev libudev-dev fido2-tools + sudo modprobe uhid || true + - name: Build HID runner + run: cargo build -p pc-hid-runner + - name: Run hid_integration test + run: sudo --preserve-env=PATH,CARGO_HOME,RUSTUP_HOME,CARGO_TARGET_DIR,PC_HID_RUNNER_E2E cargo test -p pc-hid-runner --test hid_integration + - name: Run fido2-token smoke test + run: sudo --preserve-env=PATH,CARGO_HOME,RUSTUP_HOME,CARGO_TARGET_DIR,PC_HID_RUNNER_E2E cargo test -p pc-hid-runner --test fido2_token + - name: Run end-to-end script + run: sudo --preserve-env=PATH,CARGO_HOME,RUSTUP_HOME,CARGO_TARGET_DIR,PC_HID_RUNNER_E2E ci/hid-e2e.sh diff --git a/Cargo.toml b/Cargo.toml index e7f7608..7ee4ea7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,9 @@ members = [ "trussed-mldsa", "authenticator", "trussed-mlkem", - "pc-usbip-runner" + "pc-usbip-runner", + "host-runner", + "pc-hid-runner" ] resolver = "2" diff --git a/ci/hid-e2e.sh b/ci/hid-e2e.sh new file mode 100755 index 0000000..77e2bf2 --- /dev/null +++ b/ci/hid-e2e.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ "${PC_HID_RUNNER_E2E:-}" != "1" ]]; then + echo "PC_HID_RUNNER_E2E not set; skipping HID end-to-end script" + exit 0 +fi + +if [[ "$(uname -s)" != "Linux" ]]; then + echo "HID end-to-end script only runs on Linux" + exit 0 +fi + +if [[ $EUID -ne 0 ]]; then + echo "This script must be executed as root (access to /dev/uhid required)" + exit 1 +fi + +if ! command -v fido2-token >/dev/null 2>&1; then + echo "fido2-token not found in PATH" + exit 1 +fi + +if [[ ! -x target/debug/pc-hid-runner ]]; then + echo "Build the pc-hid-runner binary first (cargo build -p pc-hid-runner)" + exit 1 +fi + +serial="E2E-$(date +%s)" +runner="target/debug/pc-hid-runner" + +modprobe uhid >/dev/null 2>&1 || true + +echo "Starting pc-hid-runner with serial ${serial}" +"${runner}" --serial "${serial}" --product "HID E2E Test" --manufacturer "CI" & +runner_pid=$! +trap 'kill ${runner_pid} 2>/dev/null || true' EXIT + +find_hidraw() { + local tries=60 + while (( tries > 0 )); do + for entry in /sys/class/hidraw/hidraw*; do + [[ -e "${entry}" ]] || continue + if grep -q "HID_UNIQ=${serial}" "${entry}/device/uevent" 2>/dev/null; then + echo "/dev/$(basename "${entry}")" + return 0 + fi + done + sleep 0.2 + tries=$((tries - 1)) + done + return 1 +} + +hidraw_node=$(find_hidraw) +if [[ -z "${hidraw_node}" ]]; then + echo "Timed out waiting for hidraw node" + exit 1 +fi + +echo "Running fido2-token -L" +list_output=$(fido2-token -L) +if [[ $? -ne 0 ]]; then + echo "fido2-token -L failed" + exit 1 +fi + +echo "${list_output}" | grep -q "${serial}" || { + echo "fido2-token -L output did not include serial ${serial}"; + exit 1; +} + +echo "Running fido2-token -I ${hidraw_node}" +if ! fido2-token -I "${hidraw_node}"; then + echo "fido2-token -I failed" + exit 1 +fi + +echo "HID end-to-end script completed successfully" diff --git a/docs/hid-runner-validation.md b/docs/hid-runner-validation.md new file mode 100644 index 0000000..dfd36b5 --- /dev/null +++ b/docs/hid-runner-validation.md @@ -0,0 +1,25 @@ +# HID Runner Manual Validation + +The automated tests cover packet framing and basic CTAP2 echo flows, but each +release should also verify the transport with a real WebAuthn relying party. + +1. Build the runner and start it with the default options: + ```bash + cargo build -p pc-hid-runner + sudo target/debug/pc-hid-runner + ``` +2. Confirm the device is discoverable by the OS and tooling: + ```bash + sudo fido2-token -L + sudo fido2-token -I /dev/hidrawX # replace with the device path from -L + ``` +3. Visit [https://webauthn.io](https://webauthn.io) in a Chromium-based browser. + Use **"Register"** to create a credential and **"Authenticate"** to exercise + the existing credential. The runner defaults to automatic user presence, so + both operations should complete without additional prompts. +4. Repeat the authenticate flow with the `--manual-user-presence` flag to ensure + the transport surfaces keepalive status updates correctly. + +For environments without direct access to `/dev/uhid`, you can enable the +optional end-to-end checks by exporting `PC_HID_RUNNER_E2E=1` and running the +integration tests or the `ci/hid-e2e.sh` helper script under `sudo`. diff --git a/host-runner/Cargo.toml b/host-runner/Cargo.toml new file mode 100644 index 0000000..ac8c8b0 --- /dev/null +++ b/host-runner/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "trussed-host-runner" +version = "0.1.0" +edition = "2021" + +[dependencies] +littlefs2-core = "0.1" +log = { version = "0.4.14", default-features = false } +rand_chacha = { version = "0.3", default-features = false } +rand_core = { version = "0.6", features = ["getrandom"] } +trussed = { version = "0.1", default-features = false, features = ["log-all", "virt"] } + +# optional features +ctaphid-dispatch = { version = "0.3", optional = true, features = ["log-all"] } +apdu-dispatch = { version = "0.3", optional = true } + +[features] +default = ["ctaphid"] +ctaphid = ["ctaphid-dispatch"] +ccid = ["apdu-dispatch"] diff --git a/host-runner/src/lib.rs b/host-runner/src/lib.rs new file mode 100644 index 0000000..1d54187 --- /dev/null +++ b/host-runner/src/lib.rs @@ -0,0 +1,511 @@ +use std::{ + any::Any, + marker::PhantomData, + ops::{Deref, DerefMut}, + sync::{ + atomic::{AtomicBool, Ordering}, + mpsc::{self, RecvTimeoutError, Sender}, + Arc, Mutex, + }, + thread, + time::{Duration, Instant}, +}; + +use littlefs2_core::DynFilesystem; +use rand_chacha::ChaCha8Rng; +use rand_core::SeedableRng as _; +use trussed::{ + backend::{CoreOnly, Dispatch}, + pipe::ServiceEndpoint, + platform, + service::Service, + store, + virt::UserInterface, + ClientImplementation, +}; + +#[cfg(feature = "ccid")] +pub use apdu_dispatch; +#[cfg(feature = "ctaphid")] +pub use ctaphid_dispatch; + +static IS_WAITING: AtomicBool = AtomicBool::new(false); + +pub fn set_waiting(waiting: bool) { + IS_WAITING.store(waiting, Ordering::Relaxed) +} + +#[derive(Clone, Debug, Default)] +pub struct ShutdownListener { + flag: Arc, +} + +impl ShutdownListener { + pub fn new(flag: Arc) -> Self { + Self { flag } + } + + pub fn should_stop(&self) -> bool { + self.flag.load(Ordering::Relaxed) + } +} + +#[derive(Clone, Debug)] +pub struct ShutdownSignal { + flag: Arc, +} + +impl ShutdownSignal { + pub fn request_shutdown(&self) { + self.flag.store(true, Ordering::Relaxed); + } +} + +pub fn shutdown_channel() -> (ShutdownSignal, ShutdownListener) { + let flag = Arc::new(AtomicBool::new(false)); + ( + ShutdownSignal { + flag: Arc::clone(&flag), + }, + ShutdownListener { flag }, + ) +} + +pub type Client = ClientImplementation<'static, Syscall, D>; + +pub type InitPlatform = Box; + +#[derive(Clone, Copy, Debug)] +pub struct DeviceClass { + pub class: u8, + pub sub_class: u8, + pub protocol: u8, +} + +impl DeviceClass { + pub const fn new(class: u8, sub_class: u8, protocol: u8) -> Self { + Self { + class, + sub_class, + protocol, + } + } + + pub const fn per_interface() -> Self { + Self::new(0x00, 0x00, 0x00) + } + + pub const fn hid() -> Self { + Self::new(0x03, 0x00, 0x00) + } + + pub const fn composite() -> Self { + Self::new(0xEF, 0x02, 0x01) + } +} + +pub struct Options { + pub manufacturer: Option, + pub product: Option, + pub serial_number: Option, + pub vid: u16, + pub pid: u16, + pub device_class: Option, +} + +impl Options { + pub fn resolved_device_class(&self, ctaphid_enabled: bool, ccid_enabled: bool) -> DeviceClass { + self.device_class + .unwrap_or_else(|| infer_device_class(ctaphid_enabled, ccid_enabled)) + } +} + +pub trait Apps<'interrupt, D: Dispatch> { + type Data; + + fn new( + service: &mut Service, + endpoints: &mut Vec>, + syscall: Syscall, + data: Self::Data, + ) -> Self; + + #[cfg(feature = "ctaphid")] + fn with_ctaphid_apps( + &mut self, + f: impl FnOnce(&mut [&mut dyn ctaphid_dispatch::app::App<'interrupt, N>]) -> T, + ) -> T; + + #[cfg(feature = "ccid")] + fn with_ccid_apps( + &mut self, + f: impl FnOnce(&mut [&mut dyn apdu_dispatch::app::App]) -> T, + ) -> T; +} + +#[derive(Copy, Clone)] +pub struct Store { + pub ifs: &'static dyn DynFilesystem, + pub efs: &'static dyn DynFilesystem, + pub vfs: &'static dyn DynFilesystem, +} + +impl store::Store for Store { + fn ifs(&self) -> &'static dyn DynFilesystem { + self.ifs + } + + fn efs(&self) -> &'static dyn DynFilesystem { + self.efs + } + + fn vfs(&self) -> &'static dyn DynFilesystem { + self.vfs + } +} + +pub struct Platform { + rng: ChaCha8Rng, + store: Store, + ui: UserInterface, +} + +impl Platform { + pub fn new(store: Store) -> Self { + Self { + store, + rng: ChaCha8Rng::from_entropy(), + ui: UserInterface::new(), + } + } +} + +impl platform::Platform for Platform { + type R = ChaCha8Rng; + type S = Store; + type UI = UserInterface; + + fn user_interface(&mut self) -> &mut Self::UI { + &mut self.ui + } + + fn rng(&mut self) -> &mut Self::R { + &mut self.rng + } + + fn store(&self) -> Self::S { + self.store + } +} + +pub struct Runner { + options: Options, + dispatch: D, + _marker: PhantomData, +} + +impl<'interrupt, D: Dispatch, A: Apps<'interrupt, D>> Runner +where + D::BackendId: Send + Sync, + D::Context: Send + Sync, +{ + pub fn builder(options: Options) -> Builder { + Builder::new(options) + } + + pub fn exec(self, platform: Platform, data: A::Data, mut transport: Box) { + self.run_with_shutdown(platform, data, transport, ShutdownListener::default()); + } + + pub fn run_with_shutdown( + self, + platform: Platform, + data: A::Data, + mut transport: Box, + shutdown: ShutdownListener, + ) { + let registration = transport.register(&self.options); + let runtime = Arc::new(Mutex::new(registration)); + + let mut service = Service::with_dispatch(platform, self.dispatch); + let mut endpoints = Vec::new(); + let (syscall_sender, syscall_receiver) = mpsc::channel(); + let syscall = Syscall(syscall_sender); + let mut apps = A::new(&mut service, &mut endpoints, syscall, data); + + log::info!("Ready for work"); + + thread::scope(|s| { + let runtime_for_poll = Arc::clone(&runtime); + let shutdown_poll = shutdown.clone(); + s.spawn(move || { + let mut transport = transport; + let _epoch = Instant::now(); + #[cfg(feature = "ctaphid")] + let mut timeout_ctaphid = Timeout::new(); + #[cfg(feature = "ccid")] + let mut timeout_ccid = Timeout::new(); + + loop { + if shutdown_poll.should_stop() { + break; + } + let mut guard = runtime_for_poll + .lock() + .expect("transport runtime mutex poisoned"); + let mut handled_usb_event = transport.poll(guard.as_mut()); + + #[cfg(feature = "ctaphid")] + { + let (started_processing, keepalive) = transport + .ctaphid_keepalive(guard.as_mut(), IS_WAITING.load(Ordering::Relaxed)); + timeout_ctaphid.update(_epoch, started_processing, || keepalive); + } + + #[cfg(feature = "ccid")] + { + let (started_processing, keepalive) = + transport.ccid_keepalive(guard.as_mut()); + timeout_ccid.update(_epoch, started_processing, || keepalive); + } + + drop(guard); + + if !handled_usb_event { + thread::yield_now(); + } + } + }); + + // trussed task + let shutdown_service = shutdown.clone(); + s.spawn(move || { + while !shutdown_service.should_stop() { + match syscall_receiver.recv_timeout(Duration::from_millis(10)) { + Ok(_) => service.process(&mut endpoints), + Err(RecvTimeoutError::Timeout) => continue, + Err(RecvTimeoutError::Disconnected) => break, + } + } + }); + + // apps task + let shutdown_main = shutdown; + loop { + if shutdown_main.should_stop() { + break; + } + let mut dispatched = false; + + #[cfg(feature = "ctaphid")] + { + let mut guard = runtime.lock().expect("transport runtime mutex poisoned"); + if let Some(mut dispatch) = guard.ctaphid_dispatch() { + let ctaphid_did_work = apps.with_ctaphid_apps(|apps| { + let mut did_work = false; + while dispatch.poll(apps) { + did_work = true; + } + did_work + }); + dispatched |= ctaphid_did_work; + } + } + + #[cfg(feature = "ccid")] + { + let mut guard = runtime.lock().expect("transport runtime mutex poisoned"); + if let Some(mut dispatch) = guard.ccid_dispatch() { + let ccid_did_work = apps.with_ccid_apps(|apps| { + let mut did_work = false; + while dispatch.poll(apps).is_some() { + did_work = true; + } + did_work + }); + dispatched |= ccid_did_work; + } + } + + if !dispatched { + thread::yield_now(); + } + } + }); + } +} + +pub struct Builder { + options: Options, + dispatch: D, +} + +impl Builder { + pub fn new(options: Options) -> Self { + Self { + options, + dispatch: Default::default(), + } + } +} + +impl Builder { + pub fn dispatch(self, dispatch: E) -> Builder { + Builder { + options: self.options, + dispatch, + } + } +} + +impl Builder { + pub fn build<'interrupt, A: Apps<'interrupt, D>>(self) -> Runner { + Runner { + options: self.options, + dispatch: self.dispatch, + _marker: Default::default(), + } + } +} + +#[derive(Clone)] +pub struct Syscall(Sender<()>); + +impl trussed::client::Syscall for Syscall { + fn syscall(&mut self) { + log::debug!("syscall"); + self.0.send(()).ok(); + } +} + +pub trait TransportRuntime: Send { + fn as_any_mut(&mut self) -> &mut dyn Any; + + #[cfg(feature = "ctaphid")] + fn ctaphid_dispatch<'interrupt>(&mut self) -> Option>; + + #[cfg(feature = "ccid")] + fn ccid_dispatch(&mut self) -> Option>; +} + +#[cfg(feature = "ctaphid")] +pub struct CtaphidDispatchRef<'a, 'interrupt> { + dispatch: &'a mut ctaphid_dispatch::Dispatch< + 'a, + 'interrupt, + { ctaphid_dispatch::DEFAULT_MESSAGE_SIZE }, + >, +} + +#[cfg(feature = "ctaphid")] +impl<'a, 'interrupt> CtaphidDispatchRef<'a, 'interrupt> { + pub fn new( + dispatch: &'a mut ctaphid_dispatch::Dispatch< + 'a, + 'interrupt, + { ctaphid_dispatch::DEFAULT_MESSAGE_SIZE }, + >, + ) -> Self { + Self { dispatch } + } +} + +#[cfg(feature = "ctaphid")] +impl<'a, 'interrupt> Deref for CtaphidDispatchRef<'a, 'interrupt> { + type Target = + ctaphid_dispatch::Dispatch<'a, 'interrupt, { ctaphid_dispatch::DEFAULT_MESSAGE_SIZE }>; + + fn deref(&self) -> &Self::Target { + self.dispatch + } +} + +#[cfg(feature = "ctaphid")] +impl<'a, 'interrupt> DerefMut for CtaphidDispatchRef<'a, 'interrupt> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.dispatch + } +} + +#[cfg(feature = "ccid")] +pub struct CcidDispatchRef<'a> { + dispatch: &'a mut apdu_dispatch::dispatch::ApduDispatch<'a>, +} + +#[cfg(feature = "ccid")] +impl<'a> CcidDispatchRef<'a> { + pub fn new(dispatch: &'a mut apdu_dispatch::dispatch::ApduDispatch<'a>) -> Self { + Self { dispatch } + } +} + +#[cfg(feature = "ccid")] +impl<'a> Deref for CcidDispatchRef<'a> { + type Target = apdu_dispatch::dispatch::ApduDispatch<'a>; + + fn deref(&self) -> &Self::Target { + self.dispatch + } +} + +#[cfg(feature = "ccid")] +impl<'a> DerefMut for CcidDispatchRef<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.dispatch + } +} + +pub trait Transport: Send { + fn register(&mut self, options: &Options) -> Box; + + fn poll(&mut self, runtime: &mut dyn TransportRuntime) -> bool; + + #[cfg(feature = "ctaphid")] + fn ctaphid_keepalive( + &mut self, + runtime: &mut dyn TransportRuntime, + waiting: bool, + ) -> (Option, Option); + + #[cfg(feature = "ccid")] + fn ccid_keepalive( + &mut self, + runtime: &mut dyn TransportRuntime, + ) -> (Option, Option); +} + +#[derive(Default)] +pub struct Timeout(Option); + +impl Timeout { + fn new() -> Self { + Self::default() + } + + fn update Option>( + &mut self, + epoch: Instant, + keepalive: Option, + f: F, + ) { + if let Some(timeout) = self.0 { + if epoch.elapsed() >= timeout { + self.0 = f().map(|duration| epoch.elapsed() + duration); + } + } else if let Some(duration) = keepalive { + self.0 = Some(epoch.elapsed() + duration); + } + } +} + +fn infer_device_class(ctaphid_enabled: bool, ccid_enabled: bool) -> DeviceClass { + let interface_count = ctaphid_enabled as u8 + ccid_enabled as u8; + + if ctaphid_enabled && interface_count == 1 { + DeviceClass::hid() + } else if interface_count > 1 { + DeviceClass::composite() + } else { + DeviceClass::per_interface() + } +} diff --git a/pc-hid-runner/Cargo.toml b/pc-hid-runner/Cargo.toml new file mode 100644 index 0000000..39edfba --- /dev/null +++ b/pc-hid-runner/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "pc-hid-runner" +version = "0.1.0" +edition = "2021" + +[features] +default = ["ctaphid"] +ctaphid = ["trussed-host-runner/ctaphid"] + +[dependencies] +log = "0.4" +trussed-host-runner = { path = "../host-runner" } +uhid-virt = "0.0.8" +nix = { version = "0.28", default-features = false, features = ["poll"] } +heapless-bytes = "0.3" +libc = "0.2" +authenticator = { path = "../authenticator" } +clap = { version = "3.0.0", features = ["derive"] } +clap-num = "1.0.0" +littlefs2 = "0.6" +littlefs2-core = "0.1" +pretty_env_logger = "0.4.0" +trussed = "0.1" +ctrlc = "3" + +[target.'cfg(target_os = "linux")'.dev-dependencies] +anyhow = "1" +hidapi = { version = "2.6", default-features = false, features = ["linux-static-hidraw"] } +rand = "0.8" + diff --git a/pc-hid-runner/src/ctaphid.rs b/pc-hid-runner/src/ctaphid.rs new file mode 100644 index 0000000..ed63ad6 --- /dev/null +++ b/pc-hid-runner/src/ctaphid.rs @@ -0,0 +1,603 @@ +use std::io::{self, ErrorKind}; + +use ctaphid_dispatch::{self, app::Command, DEFAULT_MESSAGE_SIZE}; +use heapless_bytes::Bytes; +use log::{error, warn}; +use trussed_host_runner::set_waiting; + +pub const PACKET_SIZE: usize = 64; +pub const VERSION_MAJOR: u8 = 0; +pub const VERSION_MINOR: u8 = 1; +pub const VERSION_BUILD: u8 = 0; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct Request { + pub channel: u32, + pub command: Command, + pub length: u16, + pub timestamp: u32, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct Response { + pub channel: u32, + pub command: Command, + pub length: u16, +} + +impl Response { + pub fn from_request(request: Request, len: usize) -> Self { + Self { + channel: request.channel, + command: request.command, + length: len as u16, + } + } + + pub fn error_from_request(request: Request) -> Self { + Self { + channel: request.channel, + command: Command::Error, + length: 1, + } + } + + pub fn error_on_channel(channel: u32) -> Self { + Self { + channel, + command: Command::Error, + length: 1, + } + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +struct MessageState { + next_sequence: u8, + transmitted: usize, +} + +impl Default for MessageState { + fn default() -> Self { + Self { + next_sequence: 0, + transmitted: PACKET_SIZE - 7, + } + } +} + +impl MessageState { + fn absorb_packet(&mut self) { + self.next_sequence = self.next_sequence.wrapping_add(1); + self.transmitted += PACKET_SIZE - 5; + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum State { + Idle, + Receiving((Request, MessageState)), + WaitingOnAuthenticator(Request), + WaitingToSend(Response), + Sending((Response, MessageState)), +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum AuthenticatorError { + ChannelBusy, + InvalidChannel, + InvalidCommand, + InvalidLength, + InvalidSeq, + Timeout, +} + +impl From for u8 { + fn from(err: AuthenticatorError) -> Self { + match err { + AuthenticatorError::InvalidCommand => 0x01, + AuthenticatorError::InvalidLength => 0x03, + AuthenticatorError::InvalidSeq => 0x04, + AuthenticatorError::Timeout => 0x05, + AuthenticatorError::ChannelBusy => 0x06, + AuthenticatorError::InvalidChannel => 0x0B, + } + } +} + +pub trait PacketWriter { + fn write_packet(&mut self, packet: &[u8; PACKET_SIZE]) -> io::Result<()>; +} + +pub struct HidFramer<'pipe, const N: usize = { DEFAULT_MESSAGE_SIZE }> { + requester: ctaphid_dispatch::Requester<'pipe, N>, + buffer: [u8; N], + state: State, + last_channel: u32, + started_processing: bool, + needs_keepalive: bool, + last_millis: u32, + implements: u8, +} + +impl<'pipe, const N: usize> HidFramer<'pipe, N> { + pub fn new(requester: ctaphid_dispatch::Requester<'pipe, N>) -> Self { + Self { + requester, + buffer: [0u8; N], + state: State::Idle, + last_channel: 0, + started_processing: false, + needs_keepalive: false, + last_millis: 0, + implements: 0x05, + } + } + + pub fn reset(&mut self) { + self.state = State::Idle; + self.started_processing = false; + self.needs_keepalive = false; + let _ = self.requester.cancel(); + set_waiting(false); + } + + pub fn handle_packet( + &mut self, + writer: &mut W, + packet: &[u8; PACKET_SIZE], + now: u32, + ) { + let channel = u32::from_be_bytes(packet[..4].try_into().unwrap()); + let is_initialization = (packet[4] >> 7) != 0; + + if is_initialization { + let command_byte = packet[4] & !0x80; + let command = match Command::try_from(command_byte) { + Ok(command) => command, + Err(_) => { + self.start_sending_error_on_channel( + writer, + channel, + AuthenticatorError::InvalidCommand, + ); + return; + } + }; + + let length = u16::from_be_bytes([packet[5], packet[6]]); + if length as usize > self.buffer.len() { + self.start_sending_error_on_channel( + writer, + channel, + AuthenticatorError::InvalidLength, + ); + return; + } + + self.buffer[..(length as usize).min(PACKET_SIZE - 7)].copy_from_slice(&packet[7..]); + + let request = Request { + channel, + command, + length, + timestamp: now, + }; + + if length as usize <= PACKET_SIZE - 7 { + self.dispatch_request(writer, request); + } else { + self.state = State::Receiving((request, MessageState::default())); + } + } else { + match &mut self.state { + State::Receiving((request, state)) => { + if packet[4] != state.next_sequence { + self.start_sending_error(writer, *request, AuthenticatorError::InvalidSeq); + return; + } + + let payload_length = request.length as usize; + if state.transmitted + (PACKET_SIZE - 5) < payload_length { + self.buffer[state.transmitted..][..PACKET_SIZE - 5] + .copy_from_slice(&packet[5..]); + state.absorb_packet(); + } else { + let missing = payload_length - state.transmitted; + self.buffer[state.transmitted..payload_length] + .copy_from_slice(&packet[5..][..missing]); + self.dispatch_request(writer, *request); + } + } + _ => { + warn!("unexpected continuation packet"); + } + } + } + } + + fn dispatch_request(&mut self, writer: &mut W, request: Request) { + match request.command { + Command::Init => { + if request.length != 8 { + self.start_sending_error(writer, request, AuthenticatorError::InvalidLength); + return; + } + + self.last_channel = self.last_channel.wrapping_add(1); + self.buffer[8..12].copy_from_slice(&self.last_channel.to_be_bytes()); + self.buffer[12] = 2; + self.buffer[13] = VERSION_MAJOR; + self.buffer[14] = VERSION_MINOR; + self.buffer[15] = VERSION_BUILD; + self.buffer[16] = self.implements; + + let response = Response::from_request(request, 17); + self.start_sending(writer, response); + } + Command::Ping => { + let response = Response::from_request(request, request.length as usize); + self.start_sending(writer, response); + } + Command::Cancel => { + self.cancel_ongoing_activity(); + set_waiting(false); + } + Command::Error => { + self.start_sending_error(writer, request, AuthenticatorError::InvalidCommand); + } + _ => { + self.needs_keepalive = matches!(request.command, Command::Cbor | Command::Msg); + let _ = self.requester.take_response(); + + match self.requester.request(( + request.command, + Bytes::from_slice(&self.buffer[..request.length as usize]).unwrap(), + )) { + Ok(()) => { + self.state = State::WaitingOnAuthenticator(request); + self.started_processing = true; + if self.needs_keepalive { + set_waiting(true); + } + } + Err(_) => { + self.send_error_now(writer, request, AuthenticatorError::ChannelBusy); + } + } + } + } + } + + pub fn cancel_ongoing_activity(&mut self) { + if let State::WaitingOnAuthenticator(_) = self.state { + let _ = self.requester.cancel(); + } + self.state = State::Idle; + set_waiting(false); + } + + pub fn did_start_processing(&mut self) -> bool { + if self.started_processing { + self.started_processing = false; + true + } else { + false + } + } + + pub fn send_keepalive( + &mut self, + writer: &mut W, + waiting_for_user: bool, + ) -> io::Result { + if let State::WaitingOnAuthenticator(request) = self.state { + if !self.needs_keepalive { + return Ok(false); + } + + let mut packet = [0u8; PACKET_SIZE]; + packet[..4].copy_from_slice(&request.channel.to_be_bytes()); + packet[4] = 0x80 | Command::KeepAlive.into(); + packet[5..7].copy_from_slice(&1u16.to_be_bytes()); + packet[7] = if waiting_for_user { 0x02 } else { 0x01 }; + + writer.write_packet(&packet)?; + return Ok(true); + } + + Ok(false) + } + + pub fn check_timeout(&mut self, writer: &mut W, now: u32) { + let last = self.last_millis; + self.last_millis = now; + if let State::Receiving((request, _)) = &mut self.state { + if now.wrapping_sub(request.timestamp) > 550 { + let request = *request; + self.start_sending_error(writer, request, AuthenticatorError::Timeout); + self.state = State::Idle; + } else if now.wrapping_sub(last) > 200 { + request.timestamp = now; + } + } + } + + pub fn handle_response(&mut self, writer: &mut W) { + if let State::WaitingOnAuthenticator(request) = self.state { + if let Ok(response) = self.requester.response() { + match &response.0 { + Err(ctaphid_dispatch::app::Error::InvalidCommand) => { + self.start_sending_error( + writer, + request, + AuthenticatorError::InvalidCommand, + ); + } + Err(ctaphid_dispatch::app::Error::InvalidLength) => { + self.start_sending_error( + writer, + request, + AuthenticatorError::InvalidLength, + ); + } + Err(ctaphid_dispatch::app::Error::NoResponse) => {} + Ok(message) => { + if message.len() > self.buffer.len() { + self.start_sending_error( + writer, + request, + AuthenticatorError::InvalidLength, + ); + } else { + self.buffer[..message.len()].copy_from_slice(message); + let response = Response::from_request(request, message.len()); + self.start_sending(writer, response); + } + } + } + } + } + } + + fn start_sending(&mut self, writer: &mut W, response: Response) { + self.state = State::WaitingToSend(response); + set_waiting(false); + let _ = self.maybe_write_packet(writer); + } + + fn start_sending_error( + &mut self, + writer: &mut W, + request: Request, + error: AuthenticatorError, + ) { + self.start_sending_error_on_channel(writer, request.channel, error); + } + + fn start_sending_error_on_channel( + &mut self, + writer: &mut W, + channel: u32, + error: AuthenticatorError, + ) { + self.buffer[0] = error.into(); + let response = Response::error_on_channel(channel); + self.start_sending(writer, response); + } + + fn send_error_now( + &mut self, + writer: &mut W, + request: Request, + error: AuthenticatorError, + ) { + let prev_state = std::mem::replace(&mut self.state, State::Idle); + let prev = self.buffer[0]; + self.buffer[0] = error.into(); + let response = Response::error_from_request(request); + self.start_sending(writer, response); + let _ = self.maybe_write_packet(writer); + self.state = prev_state; + self.buffer[0] = prev; + } + + pub fn maybe_write_packet(&mut self, writer: &mut W) -> bool { + match self.state.clone() { + State::WaitingToSend(response) => { + let mut packet = [0u8; PACKET_SIZE]; + packet[..4].copy_from_slice(&response.channel.to_be_bytes()); + packet[4] = response.command.into_u8() | 0x80; + packet[5..7].copy_from_slice(&response.length.to_be_bytes()); + + let fits = 7 + response.length as usize <= PACKET_SIZE; + if fits { + packet[7..7 + response.length as usize] + .copy_from_slice(&self.buffer[..response.length as usize]); + } else { + packet[7..].copy_from_slice(&self.buffer[..PACKET_SIZE - 7]); + } + + match writer.write_packet(&packet) { + Ok(()) => { + if fits { + self.state = State::Idle; + } else { + self.state = State::Sending((response, MessageState::default())); + } + true + } + Err(err) if err.kind() == ErrorKind::WouldBlock => false, + Err(err) => { + error!("failed to write HID packet: {err}"); + false + } + } + } + State::Sending((response, mut state)) => { + let mut packet = [0u8; PACKET_SIZE]; + packet[..4].copy_from_slice(&response.channel.to_be_bytes()); + packet[4] = state.next_sequence; + + let sent = state.transmitted; + let remaining = response.length as usize - sent; + let last_packet = 5 + remaining <= PACKET_SIZE; + if last_packet { + packet[5..5 + remaining].copy_from_slice(&self.buffer[sent..][..remaining]); + } else { + packet[5..].copy_from_slice(&self.buffer[sent..][..PACKET_SIZE - 5]); + } + + match writer.write_packet(&packet) { + Ok(()) => { + if last_packet { + self.state = State::Idle; + } else { + state.absorb_packet(); + self.state = State::Sending((response, state)); + } + true + } + Err(err) if err.kind() == ErrorKind::WouldBlock => false, + Err(err) => { + error!("failed to write HID packet: {err}"); + false + } + } + } + _ => false, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ctaphid_dispatch::{Channel, InterchangeResponse}; + + #[derive(Default)] + struct TestWriter { + packets: Vec<[u8; PACKET_SIZE]>, + } + + impl PacketWriter for TestWriter { + fn write_packet(&mut self, packet: &[u8; PACKET_SIZE]) -> io::Result<()> { + self.packets.push(*packet); + Ok(()) + } + } + + fn make_channel() -> ( + ctaphid_dispatch::Requester<'static, N>, + ctaphid_dispatch::Responder<'static, N>, + ) { + let channel = Box::leak(Box::new(Channel::::new())); + channel.split().unwrap() + } + + fn build_init_packet(channel: u32, nonce: [u8; 8]) -> [u8; PACKET_SIZE] { + let mut packet = [0u8; PACKET_SIZE]; + packet[..4].copy_from_slice(&channel.to_be_bytes()); + packet[4] = 0x80 | Command::Init.into(); + packet[5..7].copy_from_slice(&(nonce.len() as u16).to_be_bytes()); + packet[7..7 + nonce.len()].copy_from_slice(&nonce); + packet + } + + #[test] + fn init_allocates_channel_and_echoes_nonce() { + let (requester, _responder) = make_channel::(); + let mut framer = HidFramer::new(requester); + let mut writer = TestWriter::default(); + + let nonce = [0xAA; 8]; + let packet = build_init_packet(0xFFFF_FFFF, nonce); + framer.handle_packet(&mut writer, &packet, 0); + assert!(framer.maybe_write_packet(&mut writer)); + + assert_eq!(writer.packets.len(), 1); + let response = writer.packets[0]; + assert_eq!(&response[..4], &0xFFFF_FFFFu32.to_be_bytes()); + assert_eq!(response[4], 0x80 | Command::Init.into()); + assert_eq!(u16::from_be_bytes([response[5], response[6]]), 17); + assert_eq!(&response[7..15], &nonce); + let assigned_channel = u32::from_be_bytes(response[15..19].try_into().unwrap()); + assert_ne!(assigned_channel, 0); + } + + #[test] + fn fragmented_cbor_request_dispatches_full_payload() { + let (requester, mut responder) = make_channel::(); + let mut framer = HidFramer::new(requester); + let mut writer = TestWriter::default(); + + let mut payload = [0u8; 80]; + for (i, byte) in payload.iter_mut().enumerate() { + *byte = i as u8; + } + + let mut first = [0u8; PACKET_SIZE]; + first[..4].copy_from_slice(&1u32.to_be_bytes()); + first[4] = 0x80 | Command::Cbor.into(); + first[5..7].copy_from_slice(&(payload.len() as u16).to_be_bytes()); + first[7..].copy_from_slice(&payload[..PACKET_SIZE - 7]); + framer.handle_packet(&mut writer, &first, 1); + + let mut cont = [0u8; PACKET_SIZE]; + cont[..4].copy_from_slice(&1u32.to_be_bytes()); + cont[4] = 0; + let remaining = payload.len() - (PACKET_SIZE - 7); + cont[5..5 + remaining].copy_from_slice(&payload[PACKET_SIZE - 7..]); + framer.handle_packet(&mut writer, &cont, 2); + + let (command, bytes) = responder.take_request().expect("request"); + assert_eq!(command, Command::Cbor); + assert_eq!(&bytes[..payload.len()], &payload); + + let response = Bytes::from_slice(b"OK").unwrap(); + responder + .respond(InterchangeResponse(Ok(response))) + .unwrap(); + + framer.handle_response(&mut writer); + assert!(framer.maybe_write_packet(&mut writer)); + + assert_eq!(writer.packets.len(), 1); + let packet = writer.packets[0]; + assert_eq!(&packet[..4], &1u32.to_be_bytes()); + assert_eq!(packet[4], 0x80 | Command::Cbor.into()); + assert_eq!(u16::from_be_bytes([packet[5], packet[6]]), 2); + assert_eq!(&packet[7..9], b"OK"); + } + + #[test] + fn parallel_channel_request_reports_busy() { + let (requester, mut responder) = make_channel::(); + let mut framer = HidFramer::new(requester); + let mut writer = TestWriter::default(); + + let mut msg = [0u8; PACKET_SIZE]; + msg[..4].copy_from_slice(&1u32.to_be_bytes()); + msg[4] = 0x80 | Command::Cbor.into(); + msg[5..7].copy_from_slice(&1u16.to_be_bytes()); + msg[7] = 0xA5; + framer.handle_packet(&mut writer, &msg, 1); + + let (command, bytes) = responder.take_request().expect("request"); + assert_eq!(command, Command::Cbor); + assert_eq!(bytes[0], 0xA5); + + let mut second = [0u8; PACKET_SIZE]; + second[..4].copy_from_slice(&2u32.to_be_bytes()); + second[4] = 0x80 | Command::Cbor.into(); + second[5..7].copy_from_slice(&1u16.to_be_bytes()); + second[7] = 0x01; + framer.handle_packet(&mut writer, &second, 2); + + assert!(!writer.packets.is_empty()); + let packet = writer.packets.last().unwrap(); + assert_eq!(&packet[..4], &2u32.to_be_bytes()); + assert_eq!(packet[4], 0x80 | Command::Error.into()); + assert_eq!(packet[7], AuthenticatorError::ChannelBusy.into()); + } +} diff --git a/pc-hid-runner/src/lib.rs b/pc-hid-runner/src/lib.rs new file mode 100644 index 0000000..f911c89 --- /dev/null +++ b/pc-hid-runner/src/lib.rs @@ -0,0 +1,53 @@ +#![allow(clippy::too_many_arguments)] + +use trussed_host_runner::Transport; + +#[cfg(feature = "ctaphid")] +mod ctaphid; + +#[cfg(target_os = "linux")] +mod linux; + +#[cfg(target_os = "linux")] +pub use linux::*; + +#[cfg(not(target_os = "linux"))] +pub struct LinuxUhidTransport; + +#[cfg(not(target_os = "linux"))] +impl LinuxUhidTransport { + pub fn new() -> Self { + Self + } +} + +#[cfg(not(target_os = "linux"))] +impl Transport for LinuxUhidTransport { + fn register( + &mut self, + _options: &trussed_host_runner::Options, + ) -> Box { + panic!("LinuxUhidTransport is only supported on Linux targets"); + } + + fn poll(&mut self, _runtime: &mut dyn trussed_host_runner::TransportRuntime) -> bool { + panic!("LinuxUhidTransport is only supported on Linux targets"); + } + + #[cfg(feature = "ctaphid")] + fn ctaphid_keepalive( + &mut self, + _runtime: &mut dyn trussed_host_runner::TransportRuntime, + _waiting: bool, + ) -> (Option, Option) { + panic!("LinuxUhidTransport is only supported on Linux targets"); + } + + #[cfg(feature = "ccid")] + fn ccid_keepalive( + &mut self, + _runtime: &mut dyn trussed_host_runner::TransportRuntime, + ) -> (Option, Option) { + panic!("LinuxUhidTransport is only supported on Linux targets"); + } +} diff --git a/pc-hid-runner/src/linux.rs b/pc-hid-runner/src/linux.rs new file mode 100644 index 0000000..124c1f8 --- /dev/null +++ b/pc-hid-runner/src/linux.rs @@ -0,0 +1,540 @@ +use std::{ + any::Any, + convert::{TryFrom, TryInto}, + fs::{self, OpenOptions}, + io::{self, ErrorKind, Read, Write}, + os::unix::{ + fs::OpenOptionsExt, + io::{AsRawFd, RawFd}, + }, + path::PathBuf, + ptr::NonNull, + time::{Duration, Instant}, + vec::Vec, +}; + +use log::{error, info, warn}; +use nix::poll::{poll, PollFd, PollFlags}; +use trussed_host_runner::{ + ctaphid_dispatch::{self, Channel, Dispatch, DEFAULT_MESSAGE_SIZE}, + CtaphidDispatchRef, Options, Transport, TransportRuntime, +}; +use uhid_virt::{ + Bus, CreateParams, DevFlags, InputEvent, OutputEvent, ReportType, StreamError, UHID_EVENT_SIZE, +}; + +use crate::ctaphid::{HidFramer, PacketWriter as CtaphidPacketWriter, PACKET_SIZE}; +const REPORT_ID: u8 = 0; +const KEEPALIVE_PERIOD: Duration = Duration::from_millis(250); + +const FIDO_HID_REPORT_DESCRIPTOR: [u8; 34] = [ + 0x06, + 0xD0, + 0xF1, // Usage Page (FIDO Alliance) + 0x09, + 0x01, // Usage (FIDO Device) + 0xA1, + 0x01, // Collection (Application) + 0x09, + 0x03, // Usage (Input report) + 0x15, + 0x00, // Logical Minimum (0) + 0x26, + 0xFF, + 0x00, // Logical Maximum (255) + 0x75, + 0x08, // Report Size (8 bits) + 0x95, + PACKET_SIZE as u8, // Report Count (64 fields) + 0x81, + 0x08, // Input (Data, Variable, Absolute) + 0x09, + 0x04, // Usage (Output report) + 0x15, + 0x00, // Logical Minimum (0) + 0x26, + 0xFF, + 0x00, // Logical Maximum (255) + 0x75, + 0x08, // Report Size (8 bits) + 0x95, + PACKET_SIZE as u8, // Report Count (64 fields) + 0x91, + 0x08, // Output (Data, Variable, Absolute) + 0xC0, // End Collection +]; + +pub struct LinuxUhidTransport; + +impl LinuxUhidTransport { + pub fn new() -> Self { + Self + } +} + +impl Transport for LinuxUhidTransport { + fn register(&mut self, options: &Options) -> Box { + let channel = Box::new(Channel::<{ DEFAULT_MESSAGE_SIZE }>::new()); + let channel_ref: &'static Channel<{ DEFAULT_MESSAGE_SIZE }> = Box::leak(channel); + let (requester, responder) = channel_ref.split().expect("channel split"); + let dispatch = Dispatch::new(responder); + + let device = UhidDevice::create(options).expect("failed to create UHID device"); + + Box::new(LinuxUhidRuntime::new( + device, + channel_ref, + requester, + dispatch, + )) + } + + fn poll(&mut self, runtime: &mut dyn TransportRuntime) -> bool { + runtime + .as_any_mut() + .downcast_mut::() + .expect("linux uhid runtime downcast") + .poll() + } + + #[cfg(feature = "ctaphid")] + fn ctaphid_keepalive( + &mut self, + runtime: &mut dyn TransportRuntime, + waiting: bool, + ) -> (Option, Option) { + runtime + .as_any_mut() + .downcast_mut::() + .expect("linux uhid runtime downcast") + .ctaphid_keepalive(waiting) + } + + #[cfg(feature = "ccid")] + fn ccid_keepalive( + &mut self, + _runtime: &mut dyn TransportRuntime, + ) -> (Option, Option) { + (None, None) + } +} + +struct LinuxUhidRuntime { + device: UhidDevice, + pipe: HidFramer<'static, { DEFAULT_MESSAGE_SIZE }>, + dispatch: Option>, + channel: Option>>, + epoch: Instant, +} + +impl LinuxUhidRuntime { + fn new( + device: UhidDevice, + channel: &'static Channel<{ DEFAULT_MESSAGE_SIZE }>, + requester: ctaphid_dispatch::Requester<'static, { DEFAULT_MESSAGE_SIZE }>, + dispatch: Dispatch<'static, 'static, { DEFAULT_MESSAGE_SIZE }>, + ) -> Self { + let mut runtime = Self { + device, + pipe: HidFramer::new(requester), + dispatch: Some(dispatch), + channel: Some(NonNull::from(channel)), + epoch: Instant::now(), + }; + + runtime.device.log_registration(); + + runtime + } + + fn elapsed_millis(&self) -> u32 { + let elapsed = self.epoch.elapsed(); + elapsed.as_millis().min(u32::MAX as u128) as u32 + } + + fn poll(&mut self) -> bool { + let now = self.elapsed_millis(); + self.pipe.check_timeout(&mut self.device, now); + + let mut handled = false; + let mut poll_fd = [PollFd::new( + self.device.raw_fd(), + PollFlags::POLLIN | PollFlags::POLLOUT, + )]; + + if let Ok(events) = poll(&mut poll_fd, 0) { + if events > 0 { + if let Some(flags) = poll_fd[0].revents() { + if flags.contains(PollFlags::POLLIN) { + handled |= self.drain_kernel_events(); + } + + if flags.contains(PollFlags::POLLOUT) { + handled |= self.pipe.maybe_write_packet(&mut self.device); + } + } + } + } + + self.pipe.handle_response(&mut self.device); + handled |= self.pipe.maybe_write_packet(&mut self.device); + + handled + } + + fn drain_kernel_events(&mut self) -> bool { + let mut handled = false; + loop { + match self.device.read_event() { + Ok(Some(event)) => { + handled = true; + self.handle_event(event); + } + Ok(None) => break, + Err(err) if err.kind() == ErrorKind::WouldBlock => break, + Err(err) => { + error!("error reading /dev/uhid: {err}"); + break; + } + } + } + handled + } + + fn handle_event(&mut self, event: OutputEvent) { + match event { + OutputEvent::Start { dev_flags } => { + self.device.update_flags(&dev_flags); + self.pipe.reset(); + } + OutputEvent::Stop => { + info!("UHID device stopped"); + self.pipe.reset(); + } + OutputEvent::Open => { + info!("UHID device opened"); + self.device.log_hidraw_nodes(); + } + OutputEvent::Close => { + info!("UHID device closed"); + self.pipe.reset(); + } + OutputEvent::Output { data } => { + if let Some(packet) = self.device.decode_packet(&data) { + self.pipe + .handle_packet(&mut self.device, &packet, self.elapsed_millis()); + } else { + warn!("ignoring malformed HID output frame ({} bytes)", data.len()); + } + } + OutputEvent::GetReport { + id, + report_number, + report_type, + } => { + info!("UHID GET_REPORT id={id} report={report_number} type={report_type:?}"); + let _ = self + .device + .write_get_report_reply(id, 0, Vec::new()) + .map_err(|err| error!("failed to reply to GET_REPORT: {err}")); + } + OutputEvent::SetReport { + id, + report_number, + report_type, + data, + } => { + info!( + "UHID SET_REPORT id={id} report={report_number} type={report_type:?} len={}", + data.len() + ); + let _ = self + .device + .write_set_report_reply(id, 0) + .map_err(|err| error!("failed to ack SET_REPORT: {err}")); + } + } + } + + #[cfg(feature = "ctaphid")] + fn ctaphid_keepalive(&mut self, waiting: bool) -> (Option, Option) { + let started = if self.pipe.did_start_processing() { + Some(KEEPALIVE_PERIOD) + } else { + None + }; + + let keepalive = match self.pipe.send_keepalive(&mut self.device, waiting) { + Ok(true) => Some(KEEPALIVE_PERIOD), + Ok(false) => None, + Err(err) => { + error!("failed to send keepalive: {err}"); + None + } + }; + + (started, keepalive) + } +} + +impl TransportRuntime for LinuxUhidRuntime { + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + #[cfg(feature = "ctaphid")] + fn ctaphid_dispatch<'interrupt>(&mut self) -> Option> { + self.dispatch + .as_mut() + .map(|dispatch| CtaphidDispatchRef::new(dispatch)) + } + + #[cfg(feature = "ccid")] + fn ccid_dispatch(&mut self) -> Option> { + None + } +} + +impl Drop for LinuxUhidRuntime { + fn drop(&mut self) { + self.dispatch.take(); + if let Some(ptr) = self.channel.take() { + unsafe { + drop(Box::from_raw(ptr.as_ptr())); + } + } + if let Err(err) = self.device.destroy() { + error!("failed to destroy UHID device: {err}"); + } + } +} + +struct UhidDevice { + file: std::fs::File, + input_numbered: bool, + output_numbered: bool, + feature_numbered: bool, + name: String, + uniq: String, + vid: u16, + pid: u16, +} + +impl UhidDevice { + fn create(options: &Options) -> io::Result { + let mut open = OpenOptions::new(); + open.read(true) + .write(true) + .custom_flags(libc::O_CLOEXEC | libc::O_NONBLOCK); + let mut file = open.open("/dev/uhid")?; + + let name = options + .product + .clone() + .unwrap_or_else(|| "Trussed HID Authenticator".to_string()); + let phys = options + .manufacturer + .clone() + .unwrap_or_else(|| "trussed-host".to_string()); + let uniq = options + .serial_number + .clone() + .unwrap_or_else(|| "000000000000".to_string()); + + let params = CreateParams { + name: name.clone(), + phys, + uniq: uniq.clone(), + bus: Bus::USB, + vendor: options.vid.into(), + product: options.pid.into(), + version: 0, + country: 0, + rd_data: FIDO_HID_REPORT_DESCRIPTOR.to_vec(), + }; + + let event: [u8; UHID_EVENT_SIZE] = InputEvent::Create(params).into(); + file.write_all(&event)?; + + Ok(Self { + file, + input_numbered: false, + output_numbered: false, + feature_numbered: false, + name, + uniq, + vid: options.vid, + pid: options.pid, + }) + } + + fn raw_fd(&self) -> RawFd { + self.file.as_raw_fd() + } + + fn update_flags(&mut self, flags: &[DevFlags]) { + self.input_numbered = flags.contains(&DevFlags::InputReportsNumbered); + self.output_numbered = flags.contains(&DevFlags::OutputReportsNumbered); + self.feature_numbered = flags.contains(&DevFlags::FeatureReportsNumbered); + } + + fn read_event(&mut self) -> io::Result> { + let mut buffer = [0u8; UHID_EVENT_SIZE]; + match self.file.read_exact(&mut buffer) { + Ok(()) => match OutputEvent::try_from(buffer) { + Ok(event) => Ok(Some(event)), + Err(StreamError::UnknownEventType(kind)) => { + warn!("unknown UHID event type {kind}"); + Ok(None) + } + Err(StreamError::Io(err)) => Err(err), + }, + Err(err) if err.kind() == ErrorKind::WouldBlock => Ok(None), + Err(err) if err.kind() == ErrorKind::Interrupted => Ok(None), + Err(err) => Err(err), + } + } + + fn write_event(&mut self, event: InputEvent<'_>) -> io::Result<()> { + let raw: [u8; UHID_EVENT_SIZE] = event.into(); + self.file.write_all(&raw) + } + + fn send_packet(&mut self, payload: &[u8; PACKET_SIZE]) -> io::Result<()> { + let mut report = [0u8; PACKET_SIZE + 1]; + let mut offset = 0; + if self.input_numbered { + report[0] = REPORT_ID; + offset = 1; + } + report[offset..offset + PACKET_SIZE].copy_from_slice(payload); + self.write_event(InputEvent::Input { + data: &report[..offset + PACKET_SIZE], + }) + } + + fn write_get_report_reply(&mut self, id: u32, err: u16, data: Vec) -> io::Result<()> { + self.write_event(InputEvent::GetReportReply { id, err, data }) + } + + fn write_set_report_reply(&mut self, id: u32, err: u16) -> io::Result<()> { + self.write_event(InputEvent::SetReportReply { id, err }) + } + + fn decode_packet(&self, data: &[u8]) -> Option<[u8; PACKET_SIZE]> { + if data.len() < PACKET_SIZE { + return None; + } + + let needs_full = if self.output_numbered { + PACKET_SIZE + 1 + } else { + PACKET_SIZE + }; + + if data.len() < needs_full { + return None; + } + + let offset = if self.output_numbered { + if data[0] != REPORT_ID { + warn!("unexpected report id {}", data[0]); + } + 1 + } else if data.len() == PACKET_SIZE + 1 { + if data[0] != REPORT_ID { + warn!("unexpected report id {}", data[0]); + } + 1 + } else { + 0 + }; + + if data.len() < offset + PACKET_SIZE { + return None; + } + + let mut packet = [0u8; PACKET_SIZE]; + packet.copy_from_slice(&data[offset..offset + PACKET_SIZE]); + Some(packet) + } + + fn destroy(&mut self) -> io::Result<()> { + self.write_event(InputEvent::Destroy) + } + + fn log_registration(&self) { + info!( + "Registered UHID device '{}' vid=0x{vid:04x} pid=0x{pid:04x} on /dev/uhid", + self.name, + vid = self.vid, + pid = self.pid, + ); + self.log_hidraw_nodes(); + } + + fn hidraw_nodes(&self) -> io::Result> { + let mut matches = Vec::new(); + let dir = match fs::read_dir("/sys/class/hidraw") { + Ok(dir) => dir, + Err(err) => return Err(err), + }; + + let target = format!("HID_UNIQ={}", self.uniq); + let target_str = target.as_str(); + + for entry in dir { + let entry = match entry { + Ok(entry) => entry, + Err(err) => { + warn!("failed to enumerate hidraw entry: {err}"); + continue; + } + }; + let mut path = entry.path(); + path.push("device/uevent"); + let data = match fs::read_to_string(&path) { + Ok(data) => data, + Err(err) => { + warn!("failed to read {}: {err}", path.display()); + continue; + } + }; + if data.lines().any(|line| line.trim() == target_str) { + if let Some(name) = entry.file_name().to_str() { + matches.push(PathBuf::from("/dev").join(name)); + } + } + } + + Ok(matches) + } + + fn log_hidraw_nodes(&self) { + match self.hidraw_nodes() { + Ok(nodes) if nodes.is_empty() => { + info!( + "Waiting for hidraw node (looking for HID_UNIQ={})", + self.uniq + ); + } + Ok(nodes) => { + let joined = nodes + .iter() + .map(|path| path.display().to_string()) + .collect::>() + .join(", "); + info!("hidraw nodes available: {joined}"); + } + Err(err) => { + warn!("unable to enumerate hidraw nodes: {err}"); + } + } + } +} + +impl CtaphidPacketWriter for UhidDevice { + fn write_packet(&mut self, payload: &[u8; PACKET_SIZE]) -> io::Result<()> { + self.send_packet(payload) + } +} diff --git a/pc-hid-runner/src/main.rs b/pc-hid-runner/src/main.rs new file mode 100644 index 0000000..0b1ab34 --- /dev/null +++ b/pc-hid-runner/src/main.rs @@ -0,0 +1,237 @@ +#![cfg_attr(not(target_os = "linux"), allow(unused_imports))] + +#[cfg(not(target_os = "linux"))] +fn main() { + eprintln!("pc-hid-runner currently only supports Linux targets (needs /dev/uhid)"); + std::process::exit(1); +} + +#[cfg(target_os = "linux")] +fn main() { + pretty_env_logger::init(); + + ctrlc::set_handler(|| { + log::info!("Received Ctrl+C, shutting down"); + std::process::exit(0); + }) + .expect("failed to install Ctrl+C handler"); + + let args = Args::parse(); + let aaguid = parse_aaguid(&args.aaguid).expect("invalid AAGUID"); + + log::info!( + "Starting HID runner with VID=0x{vid:04x} PID=0x{pid:04x}", + vid = args.vid, + pid = args.pid + ); + + let store = Store { + ifs: ram_filesystem(), + efs: ram_filesystem(), + vfs: ram_filesystem(), + }; + + let options = Options { + manufacturer: Some(args.manufacturer), + product: Some(args.product), + serial_number: Some(args.serial), + vid: args.vid, + pid: args.pid, + device_class: Some(DeviceClass::hid()), + }; + + let data = AppData { + aaguid, + auto_user_presence: !args.manual_user_presence, + }; + + log::info!("Initializing Trussed"); + log::info!("Press Ctrl+C to exit"); + + let platform = Platform::new(store); + Builder::new(options).build::>().exec( + platform, + data, + Box::new(LinuxUhidTransport::new()), + ); +} + +#[cfg(target_os = "linux")] +use authenticator::ctap::CtapApp; +#[cfg(target_os = "linux")] +use clap::Parser; +#[cfg(target_os = "linux")] +use clap_num::maybe_hex; +#[cfg(target_os = "linux")] +use littlefs2::{ + const_ram_storage, + fs::{Allocation, Filesystem}, +}; +#[cfg(target_os = "linux")] +use littlefs2_core::{path, DynFilesystem}; +#[cfg(target_os = "linux")] +use pc_hid_runner::LinuxUhidTransport; +#[cfg(target_os = "linux")] +use trussed::{ + backend::{CoreOnly, NoId}, + client::Client, + pipe::{ServiceEndpoint, TrussedChannel}, + service::Service, + types::{CoreContext, NoData}, +}; +#[cfg(target_os = "linux")] +use trussed_host_runner::{apdu_dispatch, ctaphid_dispatch}; +#[cfg(target_os = "linux")] +use trussed_host_runner::{ + set_waiting, Apps as RunnerApps, Builder, Client as RunnerClient, DeviceClass, Options, + Platform, Store, Syscall, +}; + +#[cfg(target_os = "linux")] +#[derive(Parser, Debug)] +#[clap(about, version, author)] +struct Args { + /// USB product + #[clap( + short = 'n', + long, + default_value = "Feitian FIDO2 Software Authenticator (ML-DSA)" + )] + product: String, + + /// USB manufacturer + #[clap(short, long, default_value = "Feitian Technologies Co., Ltd.")] + manufacturer: String, + + /// USB serial number + #[clap(long, default_value = "FEITIAN-PQC-001")] + serial: String, + + /// Authenticator state file (reserved for future persistence) + #[clap(long, default_value = "trussed-state.bin")] + _state_file: std::path::PathBuf, + + /// USB VID + #[clap(short, long, parse(try_from_str = maybe_hex), default_value_t = 0x1998)] + vid: u16, + + /// USB PID + #[clap(short, long, parse(try_from_str = maybe_hex), default_value_t = 0x0616)] + pid: u16, + + /// Authenticator AAGUID + #[clap(long, default_value = "4645495449414E980616525A30310000")] + aaguid: String, + + /// Require user gestures instead of automatically satisfying presence checks + #[clap(long)] + manual_user_presence: bool, +} + +#[cfg(target_os = "linux")] +#[derive(Clone, Copy)] +struct AppData { + aaguid: [u8; 16], + auto_user_presence: bool, +} + +#[cfg(target_os = "linux")] +struct Apps { + ctap: CtapApp, +} + +#[cfg(target_os = "linux")] +impl<'a> RunnerApps<'a, CoreOnly> for Apps> { + type Data = AppData; + + fn new( + _service: &mut Service, + endpoints: &mut Vec>, + syscall: Syscall, + data: Self::Data, + ) -> Self { + static CHANNEL: TrussedChannel = TrussedChannel::new(); + let (requester, responder) = CHANNEL.split().expect("Trussed channel split"); + let context = CoreContext::new(path!("authenticator").into()); + endpoints.push(ServiceEndpoint::new(responder, context, &[])); + let client = RunnerClient::new(requester, syscall, None); + let mut ctap = CtapApp::new(client, data.aaguid); + ctap.set_auto_user_presence(data.auto_user_presence); + ctap.set_keepalive_callback(set_waiting); + Self { ctap } + } + + #[cfg(feature = "ctaphid")] + fn with_ctaphid_apps( + &mut self, + f: impl FnOnce(&mut [&mut dyn trussed_host_runner::ctaphid_dispatch::app::App<'a, N>]) -> T, + ) -> T { + f(&mut [&mut self.ctap]) + } + + #[cfg(feature = "ccid")] + fn with_ccid_apps( + &mut self, + f: impl FnOnce(&mut [&mut dyn trussed_host_runner::apdu_dispatch::app::App]) -> T, + ) -> T { + f(&mut []) + } +} + +#[cfg(target_os = "linux")] +const_ram_storage!(RamStorage, 512 * 128); + +#[cfg(target_os = "linux")] +fn ram_filesystem() -> &'static dyn DynFilesystem { + let storage = Box::leak(Box::new(RamStorage::new())); + Filesystem::format(storage).expect("failed to format RAM filesystem"); + let alloc = Box::leak(Box::new(Allocation::new())); + let fs = Filesystem::mount(alloc, storage).expect("failed to mount RAM filesystem"); + Box::leak(Box::new(fs)) +} + +#[cfg(target_os = "linux")] +fn parse_aaguid(input: &str) -> Result<[u8; 16], String> { + let mut cleaned = input.to_owned(); + cleaned.retain(|c| c != '-'); + if cleaned.len() != 32 { + return Err(format!("expected 32 hex characters, got {}", cleaned.len())); + } + let mut out = [0u8; 16]; + for (idx, chunk) in cleaned.as_bytes().chunks(2).enumerate() { + let hex = std::str::from_utf8(chunk).map_err(|_| "invalid UTF-8 in AAGUID".to_string())?; + out[idx] = + u8::from_str_radix(hex, 16).map_err(|_| format!("invalid hex at byte {}", idx))?; + } + Ok(out) +} + +#[cfg(target_os = "linux")] +#[cfg(test)] +mod tests { + use super::parse_aaguid; + + #[test] + fn parses_plain_hex() { + let input = "00112233445566778899aabbccddeeff"; + assert_eq!( + parse_aaguid(input).unwrap(), + [ + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, + 0xEE, 0xFF + ] + ); + } + + #[test] + fn parses_hyphenated_hex() { + let input = "00112233-4455-6677-8899-aabbccddeeff"; + assert_eq!( + parse_aaguid(input).unwrap(), + [ + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, + 0xEE, 0xFF + ] + ); + } +} diff --git a/pc-hid-runner/tests/common/mod.rs b/pc-hid-runner/tests/common/mod.rs new file mode 100644 index 0000000..eeb82e3 --- /dev/null +++ b/pc-hid-runner/tests/common/mod.rs @@ -0,0 +1,248 @@ +#![cfg(target_os = "linux")] + +use std::{ + fs::OpenOptions, + io, + path::PathBuf, + thread, + time::{Duration, Instant}, +}; + +use anyhow::{anyhow, Context, Result}; +use authenticator::ctap::CtapApp; +use hidapi::{HidApi, HidDevice}; +use littlefs2::{ + const_ram_storage, + fs::{Allocation, Filesystem}, +}; +use littlefs2_core::{path, DynFilesystem}; +use rand::{rngs::OsRng, RngCore}; +use trussed::{ + backend::{CoreOnly, NoId}, + client::Client, + pipe::{ServiceEndpoint, TrussedChannel}, + service::Service, + types::{CoreContext, NoData}, +}; +use trussed_host_runner::{ + set_waiting, shutdown_channel, Apps as RunnerApps, Builder, Client as RunnerClient, + DeviceClass, Options, Platform, Store, Syscall, +}; + +use pc_hid_runner::LinuxUhidTransport; + +pub const VID: u16 = 0x1998; +pub const PID: u16 = 0x0616; +const AAGUID_HEX: &str = "4645495449414E980616525A30310000"; + +#[derive(Clone, Copy)] +struct AppData { + aaguid: [u8; 16], + auto_user_presence: bool, +} + +struct Apps { + ctap: CtapApp, +} + +impl<'a> RunnerApps<'a, CoreOnly> for Apps> { + type Data = AppData; + + fn new( + _service: &mut Service, + endpoints: &mut Vec>, + syscall: Syscall, + data: Self::Data, + ) -> Self { + static CHANNEL: TrussedChannel = TrussedChannel::new(); + let (requester, responder) = CHANNEL.split().expect("Trussed channel split"); + let context = CoreContext::new(path!("authenticator").into()); + endpoints.push(ServiceEndpoint::new(responder, context, &[])); + let client = RunnerClient::new(requester, syscall, None); + let mut ctap = CtapApp::new(client, data.aaguid); + ctap.set_auto_user_presence(data.auto_user_presence); + ctap.set_keepalive_callback(set_waiting); + Self { ctap } + } + + #[cfg(feature = "ctaphid")] + fn with_ctaphid_apps( + &mut self, + f: impl FnOnce(&mut [&mut dyn trussed_host_runner::ctaphid_dispatch::app::App<'a, N>]) -> T, + ) -> T { + f(&mut [&mut self.ctap]) + } + + #[cfg(feature = "ccid")] + fn with_ccid_apps( + &mut self, + f: impl FnOnce(&mut [&mut dyn trussed_host_runner::apdu_dispatch::app::App]) -> T, + ) -> T { + f(&mut []) + } +} + +const_ram_storage!(RamStorage, 512 * 128); + +fn ram_filesystem() -> Result<&'static dyn DynFilesystem> { + let storage = Box::leak(Box::new(RamStorage::new())); + Filesystem::format(storage).context("format RAM filesystem")?; + let alloc = Box::leak(Box::new(Allocation::new())); + let fs = Filesystem::mount(alloc, storage).context("mount RAM filesystem")?; + Ok(Box::leak(Box::new(fs))) +} + +fn parse_aaguid(input: &str) -> Result<[u8; 16]> { + let mut cleaned = input.to_string(); + cleaned.retain(|c| c != '-'); + if cleaned.len() != 32 { + return Err(anyhow!("invalid AAGUID length: {}", cleaned.len())); + } + let mut out = [0u8; 16]; + for (chunk, slot) in cleaned.as_bytes().chunks(2).zip(out.iter_mut()) { + let hi = (chunk[0] as char) + .to_digit(16) + .ok_or_else(|| anyhow!("invalid hex"))?; + let lo = (chunk[1] as char) + .to_digit(16) + .ok_or_else(|| anyhow!("invalid hex"))?; + *slot = ((hi << 4) | lo) as u8; + } + Ok(out) +} + +pub fn ensure_uhid_access() -> io::Result<()> { + OpenOptions::new() + .read(true) + .write(true) + .open("/dev/uhid")?; + Ok(()) +} + +pub struct TestRunner { + serial: String, + signal: trussed_host_runner::ShutdownSignal, + handle: Option>, +} + +impl TestRunner { + pub fn start() -> Result { + ensure_uhid_access().context("/dev/uhid not accessible")?; + + let store = Store { + ifs: ram_filesystem()?, + efs: ram_filesystem()?, + vfs: ram_filesystem()?, + }; + + let mut serial_bytes = [0u8; 8]; + OsRng.fill_bytes(&mut serial_bytes); + let serial = format!("TEST-HID-{:016x}", u64::from_be_bytes(serial_bytes)); + + let options = Options { + manufacturer: Some("Test Manufacturer".to_string()), + product: Some("Trussed HID Test Authenticator".to_string()), + serial_number: Some(serial.clone()), + vid: VID, + pid: PID, + device_class: Some(DeviceClass::hid()), + }; + + let aaguid = parse_aaguid(AAGUID_HEX)?; + let data = AppData { + aaguid, + auto_user_presence: true, + }; + + let (signal, listener) = shutdown_channel(); + let platform = Platform::new(store); + + let handle = thread::spawn(move || { + let runner = Builder::new(options).build::>(); + runner.run_with_shutdown( + platform, + data, + Box::new(LinuxUhidTransport::new()), + listener, + ); + }); + + Ok(Self { + serial, + signal, + handle: Some(handle), + }) + } + + pub fn serial(&self) -> &str { + &self.serial + } + + pub fn wait_for_device(&self, timeout: Duration) -> Result { + let api = HidApi::new().context("create hidapi instance")?; + let deadline = Instant::now() + timeout; + loop { + api.refresh_devices().ok(); + for device in api.device_list() { + if device.vendor_id() == VID + && device.product_id() == PID + && device + .serial_number() + .map(|s| s == self.serial) + .unwrap_or(false) + { + return device.open_device(&api).context("open hidapi device"); + } + } + if Instant::now() >= deadline { + return Err(anyhow!("timed out waiting for HID device")); + } + thread::sleep(Duration::from_millis(100)); + } + } + + pub fn wait_for_hidraw_nodes(&self, timeout: Duration) -> Result> { + let deadline = Instant::now() + timeout; + loop { + match hidraw_nodes(&self.serial) { + Ok(nodes) if !nodes.is_empty() => return Ok(nodes), + Ok(_) => {} + Err(err) => return Err(err.into()), + } + if Instant::now() >= deadline { + return Err(anyhow!("timed out waiting for hidraw nodes")); + } + thread::sleep(Duration::from_millis(100)); + } + } +} + +impl Drop for TestRunner { + fn drop(&mut self) { + self.signal.request_shutdown(); + if let Some(handle) = self.handle.take() { + let _ = handle.join(); + } + } +} + +fn hidraw_nodes(serial: &str) -> io::Result> { + let mut matches = Vec::new(); + let Ok(entries) = std::fs::read_dir("/sys/class/hidraw") else { + return Ok(matches); + }; + let needle = format!("HID_UNIQ={}", serial); + for entry in entries.flatten() { + let mut path = entry.path(); + path.push("device/uevent"); + let Ok(data) = std::fs::read_to_string(&path) else { + continue; + }; + if data.lines().any(|line| line.trim() == needle) { + if let Some(name) = entry.file_name().to_str() { + matches.push(PathBuf::from("/dev").join(name)); + } + } + } + Ok(matches) +} diff --git a/pc-hid-runner/tests/fido2_token.rs b/pc-hid-runner/tests/fido2_token.rs new file mode 100644 index 0000000..35161c1 --- /dev/null +++ b/pc-hid-runner/tests/fido2_token.rs @@ -0,0 +1,60 @@ +#![cfg(target_os = "linux")] + +mod common; + +use std::{env, process::Command, time::Duration}; + +use anyhow::{anyhow, Context, Result}; +use common::TestRunner; + +#[test] +fn fido2_token_lists_and_inspects_device() -> Result<()> { + if env::var("PC_HID_RUNNER_E2E").is_err() { + eprintln!("PC_HID_RUNNER_E2E not set; skipping fido2-token smoke test"); + return Ok(()); + } + + let runner = TestRunner::start().context("start HID runner")?; + let _ = runner + .wait_for_device(Duration::from_secs(5)) + .context("open hid device")?; + + let nodes = runner + .wait_for_hidraw_nodes(Duration::from_secs(5)) + .context("locate hidraw nodes")?; + let hidraw = nodes + .first() + .ok_or_else(|| anyhow!("no hidraw nodes for device"))?; + + let list = Command::new("fido2-token") + .arg("-L") + .output() + .context("execute fido2-token -L")?; + if !list.status.success() { + return Err(anyhow!( + "fido2-token -L failed: {}", + String::from_utf8_lossy(&list.stderr) + )); + } + let listing = String::from_utf8_lossy(&list.stdout); + if !listing.contains(runner.serial()) { + return Err(anyhow!( + "fido2-token -L output did not mention serial {}", + runner.serial() + )); + } + + let info = Command::new("fido2-token") + .arg("-I") + .arg(hidraw) + .output() + .context("execute fido2-token -I")?; + if !info.status.success() { + return Err(anyhow!( + "fido2-token -I failed: {}", + String::from_utf8_lossy(&info.stderr) + )); + } + + Ok(()) +} diff --git a/pc-hid-runner/tests/hid_integration.rs b/pc-hid-runner/tests/hid_integration.rs new file mode 100644 index 0000000..c1f80ba --- /dev/null +++ b/pc-hid-runner/tests/hid_integration.rs @@ -0,0 +1,193 @@ +#![cfg(target_os = "linux")] + +mod common; + +use std::time::Duration; + +use anyhow::{anyhow, Context, Result}; +use common::TestRunner; +use hidapi::HidDevice; + +const PACKET_SIZE: usize = 64; +const BROADCAST_CID: u32 = 0xFFFF_FFFF; +const CMD_PING: u8 = 0x01; +const CMD_INIT: u8 = 0x06; +const CMD_CBOR: u8 = 0x10; +const CMD_KEEPALIVE: u8 = 0x3B; + +#[test] +fn hid_runner_handles_ping_and_cbor() -> Result<()> { + let runner = TestRunner::start().context("start HID runner")?; + let mut device = runner + .wait_for_device(Duration::from_secs(5)) + .context("open hid device")?; + + let (channel, init_reply) = ctaphid_init(&mut device)?; + assert_eq!(init_reply.nonce.len(), 8); + assert_eq!(init_reply.channel, channel); + + let mut ping_payload = Vec::with_capacity(120); + for idx in 0..120u16 { + ping_payload.push((idx % 251) as u8); + } + let ping_response = ctaphid_request(&mut device, channel, CMD_PING, &ping_payload)?; + assert_eq!(ping_response, ping_payload, "ping response matches payload"); + + let cbor_response = ctaphid_request(&mut device, channel, CMD_CBOR, &[0x04])?; + assert!( + !cbor_response.is_empty(), + "CBOR response should include status and payload" + ); + assert_eq!(cbor_response[0], 0x00, "CBOR status is CTAP2_OK"); + + Ok(()) +} + +struct InitReply { + nonce: [u8; 8], + channel: u32, +} + +fn ctaphid_init(device: &mut HidDevice) -> Result<(u32, InitReply)> { + use rand::{rngs::OsRng, RngCore}; + + let mut nonce = [0u8; 8]; + OsRng.fill_bytes(&mut nonce); + + let response = ctaphid_request(device, BROADCAST_CID, CMD_INIT, &nonce)?; + if response.len() < 17 { + return Err(anyhow!("short INIT response: {} bytes", response.len())); + } + if response[..8] != nonce { + return Err(anyhow!("INIT nonce mismatch")); + } + let new_cid = u32::from_be_bytes(response[8..12].try_into().unwrap()); + Ok(( + new_cid, + InitReply { + nonce, + channel: new_cid, + }, + )) +} + +fn ctaphid_request( + device: &mut HidDevice, + channel: u32, + command: u8, + payload: &[u8], +) -> Result> { + send_request(device, channel, command, payload).context("write CTAPHID request")?; + read_response(device, channel).context("read CTAPHID response") +} + +fn send_request(device: &mut HidDevice, channel: u32, command: u8, payload: &[u8]) -> Result<()> { + let mut frame = [0u8; PACKET_SIZE]; + frame[..4].copy_from_slice(&channel.to_be_bytes()); + frame[4] = 0x80 | command; + frame[5..7].copy_from_slice(&(payload.len() as u16).to_be_bytes()); + let mut sent = 0usize; + let header_payload = payload.len().min(PACKET_SIZE - 7); + frame[7..7 + header_payload].copy_from_slice(&payload[..header_payload]); + write_packet(device, &frame)?; + sent += header_payload; + + let mut sequence = 0u8; + while sent < payload.len() { + let mut cont = [0u8; PACKET_SIZE]; + cont[..4].copy_from_slice(&channel.to_be_bytes()); + cont[4] = sequence; + sequence = sequence.wrapping_add(1); + let chunk = (payload.len() - sent).min(PACKET_SIZE - 5); + cont[5..5 + chunk].copy_from_slice(&payload[sent..sent + chunk]); + write_packet(device, &cont)?; + sent += chunk; + } + + Ok(()) +} + +fn read_response(device: &mut HidDevice, channel: u32) -> Result> { + let mut buffer = Vec::new(); + let mut expected_len: Option = None; + let mut sequence = 0u8; + loop { + let packet = read_packet(device, Duration::from_millis(500))?; + let packet_cid = u32::from_be_bytes(packet[..4].try_into().unwrap()); + if packet_cid != channel { + continue; + } + + let header = packet[4]; + if header & 0x80 != 0 { + let cmd = header & 0x7F; + if cmd == CMD_KEEPALIVE { + expected_len = None; + buffer.clear(); + continue; + } + let len = u16::from_be_bytes([packet[5], packet[6]]) as usize; + buffer.clear(); + let chunk = len.min(PACKET_SIZE - 7); + buffer.extend_from_slice(&packet[7..7 + chunk]); + expected_len = Some(len); + sequence = 0; + if buffer.len() >= len { + buffer.truncate(len); + return Ok(buffer); + } + } else { + if expected_len.is_none() { + continue; + } + if header != sequence { + return Err(anyhow!( + "unexpected continuation sequence: got {} expected {}", + header, + sequence + )); + } + sequence = sequence.wrapping_add(1); + let len = expected_len.unwrap(); + let already = buffer.len(); + if already >= len { + continue; + } + let chunk = (len - already).min(PACKET_SIZE - 5); + buffer.extend_from_slice(&packet[5..5 + chunk]); + if buffer.len() >= len { + buffer.truncate(len); + return Ok(buffer); + } + } + } +} + +fn read_packet(device: &mut HidDevice, timeout: Duration) -> Result<[u8; PACKET_SIZE]> { + let mut buf = [0u8; PACKET_SIZE + 1]; + loop { + let len = device + .read_timeout(&mut buf, timeout.as_millis() as i32) + .context("read from hid device")?; + if len == PACKET_SIZE { + let mut packet = [0u8; PACKET_SIZE]; + packet.copy_from_slice(&buf[..PACKET_SIZE]); + return Ok(packet); + } else if len == PACKET_SIZE + 1 { + let mut packet = [0u8; PACKET_SIZE]; + packet.copy_from_slice(&buf[1..=PACKET_SIZE]); + return Ok(packet); + } else if len == 0 { + continue; + } else { + return Err(anyhow!("unexpected HID packet length: {}", len)); + } + } +} + +fn write_packet(device: &mut HidDevice, packet: &[u8; PACKET_SIZE]) -> Result<()> { + let mut report = [0u8; PACKET_SIZE + 1]; + report[1..].copy_from_slice(packet); + device.write(&report).context("write HID report")?; + Ok(()) +} diff --git a/pc-usbip-runner/Cargo.toml b/pc-usbip-runner/Cargo.toml index 4bb9357..dcc66ca 100644 --- a/pc-usbip-runner/Cargo.toml +++ b/pc-usbip-runner/Cargo.toml @@ -6,11 +6,8 @@ edition = "2021" [dependencies] interchange = "0.3.0" -littlefs2-core = "0.1" log = { version = "0.4.14", default-features = false } -rand_chacha = { version = "0.3", default-features = false } -rand_core = { version = "0.6", features = ["getrandom"] } -trussed = { version = "0.1", default-features = false, features = ["log-all", "virt"] } +trussed-host-runner = { path = "../host-runner", default-features = false } usb-device = { version = "0.2.7", default-features = false } usbip-device = "0.1.5" @@ -34,6 +31,6 @@ authenticator = { path = "../authenticator" } [features] default = ["ctaphid"] -ctaphid = ["ctaphid-dispatch", "usbd-ctaphid"] -ccid = ["apdu-dispatch", "usbd-ccid"] +ctaphid = ["ctaphid-dispatch", "usbd-ctaphid", "trussed-host-runner/ctaphid"] +ccid = ["apdu-dispatch", "usbd-ccid", "trussed-host-runner/ccid"] diff --git a/pc-usbip-runner/examples/authenticator.rs b/pc-usbip-runner/examples/authenticator.rs index 7502efb..2eca268 100644 --- a/pc-usbip-runner/examples/authenticator.rs +++ b/pc-usbip-runner/examples/authenticator.rs @@ -166,6 +166,7 @@ fn main() { aaguid, auto_user_presence: !args.manual_user_presence, }, + Box::new(trussed_usbip::UsbipTransport), ); } diff --git a/pc-usbip-runner/src/ccid.rs b/pc-usbip-runner/src/ccid.rs index bb26cec..da28791 100644 --- a/pc-usbip-runner/src/ccid.rs +++ b/pc-usbip-runner/src/ccid.rs @@ -1,11 +1,7 @@ -use std::time::{Duration, Instant}; - use apdu_dispatch::dispatch::ApduDispatch; use apdu_dispatch::interchanges::Data; use usb_device::bus::{UsbBus, UsbBusAllocator}; -use usbd_ccid::{Ccid, Status}; - -use super::Timeout; +use usbd_ccid::Ccid; pub fn setup<'bus, 'pipe, B: UsbBus>( bus_allocator: &'bus UsbBusAllocator, @@ -17,20 +13,3 @@ pub fn setup<'bus, 'pipe, B: UsbBus>( let apdu_dispatch = ApduDispatch::new(ccid_rp, contactless.split().unwrap().1); (ccid, apdu_dispatch) } - -pub fn keepalive( - ccid: &mut Ccid<'_, '_, B, N>, - timeout: &mut Timeout, - epoch: Instant, -) { - timeout.update(epoch, map_status(ccid.did_start_processing()), || { - map_status(ccid.send_wait_extension()) - }); -} - -fn map_status(status: Status) -> Option { - match status { - Status::ReceivedData(ms) => Some(Duration::from_millis(ms.0.into())), - Status::Idle => None, - } -} diff --git a/pc-usbip-runner/src/ctaphid.rs b/pc-usbip-runner/src/ctaphid.rs index aca475a..b01fb8d 100644 --- a/pc-usbip-runner/src/ctaphid.rs +++ b/pc-usbip-runner/src/ctaphid.rs @@ -1,13 +1,6 @@ -use std::{ - sync::atomic::Ordering, - time::{Duration, Instant}, -}; - use ctaphid_dispatch::Dispatch; use usb_device::bus::{UsbBus, UsbBusAllocator}; -use usbd_ctaphid::{types::Status, CtapHid}; - -use super::{Timeout, IS_WAITING}; +use usbd_ctaphid::CtapHid; pub fn setup<'bus, 'pipe, 'interrupt, B: UsbBus, const N: usize>( bus_allocator: &'bus UsbBusAllocator, @@ -24,20 +17,3 @@ pub fn setup<'bus, 'pipe, 'interrupt, B: UsbBus, const N: usize>( let ctaphid_dispatch = Dispatch::new(ctaphid_rp); (ctaphid, ctaphid_dispatch) } - -pub fn keepalive( - ctaphid: &mut CtapHid<'_, '_, '_, B, N>, - timeout: &mut Timeout, - epoch: Instant, -) { - timeout.update(epoch, map_status(ctaphid.did_start_processing()), || { - map_status(ctaphid.send_keepalive(IS_WAITING.load(Ordering::Relaxed))) - }); -} - -fn map_status(status: Status) -> Option { - match status { - Status::ReceivedData(ms) => Some(Duration::from_millis(ms.0.into())), - Status::Idle => None, - } -} diff --git a/pc-usbip-runner/src/lib.rs b/pc-usbip-runner/src/lib.rs index dc50081..242488a 100644 --- a/pc-usbip-runner/src/lib.rs +++ b/pc-usbip-runner/src/lib.rs @@ -1,342 +1,306 @@ +#![allow(clippy::type_complexity)] + #[cfg(feature = "ccid")] mod ccid; #[cfg(feature = "ctaphid")] mod ctaphid; -use std::{ - marker::PhantomData, - sync::{ - atomic::{AtomicBool, Ordering}, - mpsc::{self, Sender}, - }, - thread, - time::{Duration, Instant}, -}; +use std::{any::Any, ptr::NonNull, time::Duration}; -use littlefs2_core::DynFilesystem; -use rand_chacha::ChaCha8Rng; -use rand_core::SeedableRng as _; -use trussed::{ - backend::{CoreOnly, Dispatch}, - pipe::ServiceEndpoint, - platform, - service::Service, - store, - virt::UserInterface, - ClientImplementation, -}; +#[cfg(feature = "ccid")] +use trussed_host_runner::CcidDispatchRef; +#[cfg(feature = "ctaphid")] +use trussed_host_runner::CtaphidDispatchRef; +use trussed_host_runner::{ctaphid_dispatch, Options, Transport, TransportRuntime}; use usb_device::{ - bus::{UsbBus, UsbBusAllocator}, + bus::UsbBusAllocator, + class::UsbClass, device::{UsbDevice, UsbDeviceBuilder, UsbVidPid}, }; use usbip_device::UsbIpBus; -static IS_WAITING: AtomicBool = AtomicBool::new(false); - -pub fn set_waiting(waiting: bool) { - IS_WAITING.store(waiting, Ordering::Relaxed) -} - -pub type Client = ClientImplementation<'static, Syscall, D>; +#[cfg(feature = "ccid")] +use { + apdu_dispatch::{dispatch::ApduDispatch, interchanges::Data}, + interchange, + usbd_ccid::Ccid, +}; +#[cfg(feature = "ctaphid")] +use {ctaphid_dispatch::Channel, usbd_ctaphid::CtapHid}; -pub type InitPlatform = Box; +pub use trussed_host_runner::*; -#[derive(Clone, Copy, Debug)] -pub struct DeviceClass { - pub class: u8, - pub sub_class: u8, - pub protocol: u8, -} +const USB_SPEED_SUPER: u8 = 2; +#[cfg(feature = "ccid")] +const CCID_BUFFER_SIZE: usize = 3072; -impl DeviceClass { - pub const fn new(class: u8, sub_class: u8, protocol: u8) -> Self { - Self { - class, - sub_class, - protocol, - } - } +pub struct UsbipTransport; - pub const fn per_interface() -> Self { - Self::new(0x00, 0x00, 0x00) - } +impl Transport for UsbipTransport { + fn register(&mut self, options: &Options) -> Box { + let mut usbip_bus = UsbIpBus::new(); + usbip_bus.set_device_speed(USB_SPEED_SUPER); + let allocator = Box::new(UsbBusAllocator::new(usbip_bus)); + let allocator_ref: &'static mut UsbBusAllocator = Box::leak(allocator); + let allocator_ptr = NonNull::from(allocator_ref); - pub const fn hid() -> Self { - Self::new(0x03, 0x00, 0x00) - } + let usb_device = Box::new(build_device(allocator_ref, options)); + let usb_device_ref: &'static mut UsbDevice<'static, UsbIpBus> = Box::leak(usb_device); + let usb_device_ptr = NonNull::from(usb_device_ref); - pub const fn composite() -> Self { - Self::new(0xEF, 0x02, 0x01) - } -} - -pub struct Options { - pub manufacturer: Option, - pub product: Option, - pub serial_number: Option, - pub vid: u16, - pub pid: u16, - pub device_class: Option, -} + #[cfg(feature = "ctaphid")] + let (ctaphid_ptr, ctaphid_dispatch, channel_ptr) = { + let channel = Box::new(Channel::<{ ctaphid_dispatch::DEFAULT_MESSAGE_SIZE }>::new()); + let channel_ref: &'static Channel<{ ctaphid_dispatch::DEFAULT_MESSAGE_SIZE }> = + Box::leak(channel); + let (ctaphid, dispatch) = ctaphid::setup(allocator_ref, channel_ref); + let ctaphid_ref: &'static mut CtapHid< + 'static, + 'static, + 'static, + UsbIpBus, + { ctaphid_dispatch::DEFAULT_MESSAGE_SIZE }, + > = Box::leak(Box::new(ctaphid)); + ( + NonNull::from(ctaphid_ref), + Some(dispatch), + NonNull::from(channel_ref), + ) + }; -impl Options { - fn vid_pid(&self) -> UsbVidPid { - UsbVidPid(self.vid, self.pid) + #[cfg(feature = "ccid")] + let (ccid_ptr, apdu_dispatch, contact_ptr, contactless_ptr) = { + let contact = Box::new(interchange::Channel::::new()); + let contactless = Box::new(interchange::Channel::::new()); + let contact_ref: &'static interchange::Channel = Box::leak(contact); + let contactless_ref: &'static interchange::Channel = Box::leak(contactless); + let (ccid, dispatch) = ccid::setup(allocator_ref, contact_ref, contactless_ref); + let ccid_ref: &'static mut Ccid<'static, 'static, UsbIpBus, CCID_BUFFER_SIZE> = + Box::leak(Box::new(ccid)); + ( + NonNull::from(ccid_ref), + Some(dispatch), + NonNull::from(contact_ref), + NonNull::from(contactless_ref), + ) + }; + + Box::new(UsbipRuntime { + allocator: allocator_ptr, + usb_device: usb_device_ptr, + #[cfg(feature = "ctaphid")] + ctaphid: Some(ctaphid_ptr), + #[cfg(feature = "ctaphid")] + ctaphid_dispatch, + #[cfg(feature = "ctaphid")] + ctap_channel: Some(channel_ptr), + #[cfg(feature = "ccid")] + ccid: Some(ccid_ptr), + #[cfg(feature = "ccid")] + apdu_dispatch, + #[cfg(feature = "ccid")] + contact: Some(contact_ptr), + #[cfg(feature = "ccid")] + contactless: Some(contactless_ptr), + }) } - fn resolved_device_class(&self, ctaphid_enabled: bool, ccid_enabled: bool) -> DeviceClass { - self.device_class - .unwrap_or_else(|| infer_device_class(ctaphid_enabled, ccid_enabled)) + fn poll(&mut self, runtime: &mut dyn TransportRuntime) -> bool { + let runtime = runtime + .as_any_mut() + .downcast_mut::() + .expect("usbip runtime downcast"); + runtime.poll() } -} - -pub trait Apps<'interrupt, D: Dispatch> { - type Data; - - fn new( - service: &mut Service, - endpoints: &mut Vec>, - syscall: Syscall, - data: Self::Data, - ) -> Self; #[cfg(feature = "ctaphid")] - fn with_ctaphid_apps( + fn ctaphid_keepalive( &mut self, - f: impl FnOnce(&mut [&mut dyn ctaphid_dispatch::app::App<'interrupt, N>]) -> T, - ) -> T; + runtime: &mut dyn TransportRuntime, + waiting: bool, + ) -> (Option, Option) { + let runtime = runtime + .as_any_mut() + .downcast_mut::() + .expect("usbip runtime downcast"); + runtime.ctaphid_keepalive(waiting) + } #[cfg(feature = "ccid")] - fn with_ccid_apps( + fn ccid_keepalive( &mut self, - f: impl FnOnce(&mut [&mut dyn apdu_dispatch::app::App]) -> T, - ) -> T; + runtime: &mut dyn TransportRuntime, + ) -> (Option, Option) { + let runtime = runtime + .as_any_mut() + .downcast_mut::() + .expect("usbip runtime downcast"); + runtime.ccid_keepalive() + } } -// virt::Store uses non-static references. To be able to use the usbip runner with apps that -// require direct access to the store, e. g. provisioner-app, we use a custom store implementation -// with static lifetimes here. -#[derive(Copy, Clone)] -pub struct Store { - pub ifs: &'static dyn DynFilesystem, - pub efs: &'static dyn DynFilesystem, - pub vfs: &'static dyn DynFilesystem, +struct UsbipRuntime { + allocator: NonNull>, + usb_device: NonNull>, + #[cfg(feature = "ctaphid")] + ctaphid: Option< + NonNull< + CtapHid< + 'static, + 'static, + 'static, + UsbIpBus, + { ctaphid_dispatch::DEFAULT_MESSAGE_SIZE }, + >, + >, + >, + #[cfg(feature = "ctaphid")] + ctaphid_dispatch: Option< + ctaphid_dispatch::Dispatch<'static, 'static, { ctaphid_dispatch::DEFAULT_MESSAGE_SIZE }>, + >, + #[cfg(feature = "ctaphid")] + ctap_channel: Option>>, + #[cfg(feature = "ccid")] + ccid: Option>>, + #[cfg(feature = "ccid")] + apdu_dispatch: Option>, + #[cfg(feature = "ccid")] + contact: Option>>, + #[cfg(feature = "ccid")] + contactless: Option>>, } -impl store::Store for Store { - fn ifs(&self) -> &'static dyn DynFilesystem { - self.ifs - } - - fn efs(&self) -> &'static dyn DynFilesystem { - self.efs - } - - fn vfs(&self) -> &'static dyn DynFilesystem { - self.vfs - } -} +impl UsbipRuntime { + fn poll(&mut self) -> bool { + let usb_device = unsafe { self.usb_device.as_mut() }; + let mut handled = false; + + #[cfg(all(feature = "ctaphid", feature = "ccid"))] + { + let ctaphid = unsafe { self.ctaphid.expect("ctaphid missing").as_mut() }; + let ccid = unsafe { self.ccid.expect("ccid missing").as_mut() }; + let mut classes: [&mut dyn UsbClass; 2] = [ctaphid, ccid]; + while usb_device.poll(&mut classes) { + handled = true; + } + } -pub struct Platform { - rng: ChaCha8Rng, - store: Store, - ui: UserInterface, -} + #[cfg(all(feature = "ctaphid", not(feature = "ccid")))] + { + let ctaphid = unsafe { self.ctaphid.expect("ctaphid missing").as_mut() }; + let mut classes: [&mut dyn UsbClass; 1] = [ctaphid]; + while usb_device.poll(&mut classes) { + handled = true; + } + } -impl Platform { - pub fn new(store: Store) -> Self { - Self { - store, - rng: ChaCha8Rng::from_entropy(), - ui: UserInterface::new(), + #[cfg(all(feature = "ccid", not(feature = "ctaphid")))] + { + let ccid = unsafe { self.ccid.expect("ccid missing").as_mut() }; + let mut classes: [&mut dyn UsbClass; 1] = [ccid]; + while usb_device.poll(&mut classes) { + handled = true; + } } - } -} -impl platform::Platform for Platform { - type R = ChaCha8Rng; - type S = Store; - type UI = UserInterface; + #[cfg(not(any(feature = "ctaphid", feature = "ccid")))] + while usb_device.poll(&mut []) { + handled = true; + } - fn user_interface(&mut self) -> &mut Self::UI { - &mut self.ui + handled } - fn rng(&mut self) -> &mut Self::R { - &mut self.rng - } + #[cfg(feature = "ctaphid")] + fn ctaphid_keepalive(&mut self, waiting: bool) -> (Option, Option) { + use usbd_ctaphid::types::Status; - fn store(&self) -> Self::S { - self.store - } -} + let ctaphid = unsafe { self.ctaphid.expect("ctaphid missing").as_mut() }; -pub struct Runner { - options: Options, - dispatch: D, - _marker: PhantomData, -} + let map_status = |status: Status| match status { + Status::ReceivedData(ms) => Some(Duration::from_millis(ms.0.into())), + Status::Idle => None, + }; -impl<'interrupt, D: Dispatch, A: Apps<'interrupt, D>> Runner -where - D::BackendId: Send + Sync, - D::Context: Send + Sync, -{ - pub fn builder(options: Options) -> Builder { - Builder::new(options) + ( + map_status(ctaphid.did_start_processing()), + map_status(ctaphid.send_keepalive(waiting)), + ) } - pub fn exec(self, platform: Platform, data: A::Data) { - // To change IP or port see usbip-device-0.1.4/src/handler.rs:26 - let usbip_bus = UsbIpBus::new(); - usbip_bus.set_device_speed(2); - let bus_allocator = UsbBusAllocator::new(usbip_bus); + #[cfg(feature = "ccid")] + fn ccid_keepalive(&mut self) -> (Option, Option) { + use usbd_ccid::Status; - #[cfg(feature = "ctaphid")] - let ctap_channel = ctaphid_dispatch::Channel::new(); - #[cfg(feature = "ctaphid")] - let (mut ctaphid, mut ctaphid_dispatch) = ctaphid::setup::< - _, - { ctaphid_dispatch::DEFAULT_MESSAGE_SIZE }, - >(&bus_allocator, &ctap_channel); + let ccid = unsafe { self.ccid.expect("ccid missing").as_mut() }; - #[cfg(feature = "ccid")] - let (contact, contactless) = Default::default(); - #[cfg(feature = "ccid")] - let (mut ccid, mut apdu_dispatch) = ccid::setup(&bus_allocator, &contact, &contactless); - - let mut usb_device = build_device(&bus_allocator, &self.options); - let mut service = Service::with_dispatch(platform, self.dispatch); - let mut endpoints = Vec::new(); - let (syscall_sender, syscall_receiver) = mpsc::channel(); - let syscall = Syscall(syscall_sender); - let mut apps = A::new(&mut service, &mut endpoints, syscall, data); - - log::info!("Ready for work"); - thread::scope(|s| { - // usb poll + keepalive task - s.spawn(move || { - let _epoch = Instant::now(); - #[cfg(feature = "ctaphid")] - let mut timeout_ctaphid = Timeout::new(); - #[cfg(feature = "ccid")] - let mut timeout_ccid = Timeout::new(); - - loop { - let mut handled_usb_event = false; - while usb_device.poll(&mut [ - #[cfg(feature = "ctaphid")] - &mut ctaphid, - #[cfg(feature = "ccid")] - &mut ccid, - ]) { - handled_usb_event = true; - } - - #[cfg(feature = "ctaphid")] - ctaphid::keepalive(&mut ctaphid, &mut timeout_ctaphid, _epoch); - #[cfg(feature = "ccid")] - ccid::keepalive(&mut ccid, &mut timeout_ccid, _epoch); - - // USB/IP adds host-side latency. Yield instead of sleeping so control - // transfers can complete with minimal turnaround time. - if !handled_usb_event { - thread::yield_now(); - } - } - }); + let map_status = |status: Status| match status { + Status::ReceivedData(ms) => Some(Duration::from_millis(ms.0.into())), + Status::Idle => None, + }; + + ( + map_status(ccid.did_start_processing()), + map_status(ccid.send_wait_extension()), + ) + } +} - // trussed task - s.spawn(move || { - for _ in syscall_receiver.iter() { - service.process(&mut endpoints) +impl Drop for UsbipRuntime { + fn drop(&mut self) { + unsafe { + #[cfg(feature = "ctaphid")] + { + self.ctaphid_dispatch.take(); + if let Some(ctaphid) = self.ctaphid.take() { + drop(Box::from_raw(ctaphid.as_ptr())); } - }); - - // apps task - loop { - let mut dispatched = false; - - #[cfg(feature = "ctaphid")] - { - let ctaphid_did_work = apps.with_ctaphid_apps(|apps| { - let mut did_work = false; - while ctaphid_dispatch.poll(apps) { - did_work = true; - } - did_work - }); - dispatched |= ctaphid_did_work; + if let Some(channel) = self.ctap_channel.take() { + drop(Box::from_raw(channel.as_ptr())); } + } - #[cfg(feature = "ccid")] - { - let ccid_did_work = apps.with_ccid_apps(|apps| { - let mut did_work = false; - while apdu_dispatch.poll(apps).is_some() { - did_work = true; - } - did_work - }); - dispatched |= ccid_did_work; + #[cfg(feature = "ccid")] + { + self.apdu_dispatch.take(); + if let Some(ccid) = self.ccid.take() { + drop(Box::from_raw(ccid.as_ptr())); } - - if !dispatched { - thread::yield_now(); + if let Some(contact) = self.contact.take() { + drop(Box::from_raw(contact.as_ptr())); + } + if let Some(contactless) = self.contactless.take() { + drop(Box::from_raw(contactless.as_ptr())); } } - }); - } -} -pub struct Builder { - options: Options, - dispatch: D, -} - -impl Builder { - pub fn new(options: Options) -> Self { - Self { - options, - dispatch: Default::default(), + drop(Box::from_raw(self.usb_device.as_ptr())); + drop(Box::from_raw(self.allocator.as_ptr())); } } } -impl Builder { - pub fn dispatch(self, dispatch: E) -> Builder { - Builder { - options: self.options, - dispatch, - } +impl TransportRuntime for UsbipRuntime { + fn as_any_mut(&mut self) -> &mut dyn Any { + self } -} -impl Builder { - pub fn build<'interrupt, A: Apps<'interrupt, D>>(self) -> Runner { - Runner { - options: self.options, - dispatch: self.dispatch, - _marker: Default::default(), - } + #[cfg(feature = "ctaphid")] + fn ctaphid_dispatch<'interrupt>(&mut self) -> Option> { + self.ctaphid_dispatch.as_mut().map(CtaphidDispatchRef::new) } -} - -#[derive(Clone)] -pub struct Syscall(Sender<()>); -impl trussed::client::Syscall for Syscall { - fn syscall(&mut self) { - log::debug!("syscall"); - self.0.send(()).ok(); + #[cfg(feature = "ccid")] + fn ccid_dispatch(&mut self) -> Option> { + self.apdu_dispatch.as_mut().map(CcidDispatchRef::new) } } -fn build_device<'a, B: UsbBus>( - bus_allocator: &'a UsbBusAllocator, - options: &'a Options, -) -> UsbDevice<'a, B> { - let mut usb_builder = UsbDeviceBuilder::new(bus_allocator, options.vid_pid()); +fn build_device( + bus_allocator: &'static UsbBusAllocator, + options: &Options, +) -> UsbDevice<'static, UsbIpBus> { + let mut usb_builder = UsbDeviceBuilder::new(bus_allocator, UsbVidPid(options.vid, options.pid)); if let Some(manufacturer) = &options.manufacturer { usb_builder = usb_builder.manufacturer(manufacturer); } @@ -366,39 +330,3 @@ fn build_device<'a, B: UsbBus>( .device_protocol(device_class.protocol) .build() } - -fn infer_device_class(ctaphid_enabled: bool, ccid_enabled: bool) -> DeviceClass { - let interface_count = ctaphid_enabled as u8 + ccid_enabled as u8; - - if ctaphid_enabled && interface_count == 1 { - DeviceClass::hid() - } else if interface_count > 1 { - DeviceClass::composite() - } else { - DeviceClass::per_interface() - } -} - -#[derive(Default)] -pub struct Timeout(Option); - -impl Timeout { - fn new() -> Self { - Self::default() - } - - fn update Option>( - &mut self, - epoch: Instant, - keepalive: Option, - f: F, - ) { - if let Some(timeout) = self.0 { - if epoch.elapsed() >= timeout { - self.0 = f().map(|duration| epoch.elapsed() + duration); - } - } else if let Some(duration) = keepalive { - self.0 = Some(epoch.elapsed() + duration); - } - } -}