diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..094e802 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Install Rust toolchain + run: | + rustup show + + - name: Install build dependencies + run: > + sudo apt-get update -y -qq && + sudo apt-get install -y -qq llvm libclang-dev + + + - name: Check all targets + run: | + cargo check --workspace --all-targets --no-default-features + cargo check --workspace --all-targets --no-default-features --features ctaphid + cargo check --workspace --all-targets --no-default-features --features ccid + cargo check --workspace --all-targets + cargo check --workspace --all-targets --all-features + + - name: Check formatting + run: cargo fmt --all -- --check + + - name: Check clippy lints + run: cargo clippy --all-features --all-targets -- --deny warnings + + - name: Check documentation + run: RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features diff --git a/Cargo.lock b/Cargo.lock index abd9316..97832ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,19 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "admin-app" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67d9f4831720ac3f95d708922b71cbec0d085548affb935f4af35395af28366c" -dependencies = [ - "apdu-dispatch", - "ctaphid-dispatch", - "delog", - "iso7816", - "trussed", -] - [[package]] name = "aead" version = "0.5.2" @@ -521,26 +508,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "fido-authenticator" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda469ecf5b58ba898e1a6e68f99529b568e24490f45ced8222240d10d7c6508" -dependencies = [ - "apdu-dispatch", - "ctap-types", - "ctaphid-dispatch", - "delog", - "heapless", - "interchange 0.2.2", - "iso7816", - "littlefs2 0.3.2", - "serde", - "serde-indexed", - "serde_cbor", - "trussed", -] - [[package]] name = "flexiber" version = "0.1.0" @@ -773,22 +740,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "littlefs2" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc089501e32d62b3e4d809a29b9e00d6211a197440602d69f304f1e4e82136b" -dependencies = [ - "bitflags", - "cstr_core", - "cty", - "delog", - "generic-array", - "heapless", - "littlefs2-sys", - "serde", -] - [[package]] name = "littlefs2" version = "0.4.0" @@ -1517,7 +1468,7 @@ dependencies = [ "hex-literal", "hmac 0.12.1", "interchange 0.3.0", - "littlefs2 0.4.0", + "littlefs2", "nb 1.1.0", "p256-cortex-m4", "postcard", @@ -1535,13 +1486,11 @@ dependencies = [ name = "trussed-usbip" version = "0.0.1" dependencies = [ - "admin-app", "apdu-dispatch", "clap", "clap-num", "ctaphid-dispatch", "delog", - "fido-authenticator", "interchange 0.3.0", "log", "pretty_env_logger", diff --git a/Cargo.toml b/Cargo.toml index 311c9a7..3189906 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,21 +24,13 @@ clap = { version = "3.0.0", features = ["derive"] } clap-num = "1.0.0" delog = { version = "0.1.6", features = ["std-log"] } pretty_env_logger = "0.4.0" -trussed = { version = "0.1", features = ["clients-3"] } - -# applications -admin-app = { version = "0.1", features = ["log-all"] } -fido-authenticator = { version = "0.1", features = ["dispatch", "log-all"] } +trussed = { version = "0.1", features = ["clients-1"] } [features] default = ["ctaphid", "ccid"] ctaphid = ["ctaphid-dispatch", "usbd-ctaphid"] ccid = ["apdu-dispatch", "usbd-ccid"] -[[example]] -name = "fido" -required-features = ["ctaphid"] - [patch.crates-io] trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "51e68500d7601d04f884f5e95567d14b9018a6cb" } @@ -46,4 +38,3 @@ usbd-ctaphid = { git = "https://github.com/trussed-dev/usbd-ctaphid", rev = "e9c usbd-ccid = { git = "https://github.com/trussed-dev/usbd-ccid", tag = "0.3.0" } ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } apdu-dispatch = { git = "https://github.com/trussed-dev/apdu-dispatch.git", rev = "b72d5eb9f4d7a3f107a78a2f0e41f3c403f4c7a4" } - diff --git a/Makefile b/Makefile index 2d1e784..41ee69b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -EXAMPLE_NAME := fido +EXAMPLE_NAME := dummy all: | start-sim attach finish-message diff --git a/README.md b/README.md index ac1d510..34547af 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,23 @@ -# USB/IP Simulation +# trussed-usbip -This runner allows using USB/IP as a means to simulate device connection -to the OS, and should allow faster development of the embedded applications. +This crate facilitates simulation of Trussed devices using USB/IP. +It should only be used for development and testing. Remarks: -- Extensible with CTAP apps: currently FIDO and Admin are active; -- Does not work with Firefox at the moment; -- Allows to inject own FIDO certificates, and device properties; - Requires multiple `usbip attach` calls to make it work [1]. +- Works best with CTAPHID. CCID is supported but often unstable. [1] https://github.com/Sawchord/usbip-device#known-bugs +## Examples + +[`examples/dummy.rs`](`examples/dummy.rs`) contains a very simple example that +shows how to run a simulated Trussed device. +For a more complex example, see the [usbip runner][] of the Nitrokey 3 that +provides all features of the Nitrokey 3. + +[usbip runner]: https://github.com/Nitrokey/nitrokey-3-firmware/tree/main/runners/usbip + ## Setup USB/IP tools are required to work, as well as kernel supporting it. diff --git a/examples/dummy.rs b/examples/dummy.rs new file mode 100644 index 0000000..3a44190 --- /dev/null +++ b/examples/dummy.rs @@ -0,0 +1,139 @@ +//! USB/IP simulation of a Trussed device. +//! +//! This example contains a dummy app that responds with random data to the CTAPHID vendor command +//! 0x60. It can be tested with `nitropy nk3 list` and `nitropy nk3 rng`. +//! +//! For a more complete example, see the [usbip runner][] for the Nitrokey 3. +//! +//! [usbip runner]: https://github.com/Nitrokey/nitrokey-3-firmware/tree/main/runners/usbip + +use std::path::PathBuf; + +#[cfg(feature = "ccid")] +use apdu_dispatch::command::SIZE as ApduCommandSize; +#[cfg(feature = "ctaphid")] +use ctaphid_dispatch::{ + command::{Command, VendorCommand}, + types::{AppResult, Error, Message}, +}; + +use clap::Parser; +use clap_num::maybe_hex; +use trussed::{ + backend::CoreOnly, + client::{Client, ClientBuilder}, + service::Service, + syscall, + types::Vec, + virt::{self, Platform, StoreProvider}, +}; +use trussed_usbip::Syscall; + +/// USP/IP based virtualization a Trussed device. +#[derive(Parser, Debug)] +#[clap(about, version, author)] +struct Args { + /// USB Name string + #[clap(short, long, default_value = "Trussed")] + name: String, + + /// USB Manufacturer string + #[clap(short, long, default_value = "Trussed")] + manufacturer: String, + + /// Trussed state file + #[clap(long, default_value = "trussed-state.bin")] + state_file: PathBuf, + + /// USB VID id + #[clap(short, long, parse(try_from_str=maybe_hex), default_value_t = 0x20a0)] + vid: u16, + + /// USB PID id + #[clap(short, long, parse(try_from_str=maybe_hex), default_value_t = 0x42b2)] + pid: u16, +} + +struct DummyApp { + client: C, +} + +impl DummyApp { + fn rng(&mut self, response: &mut Vec) { + let bytes = syscall!(self.client.random_bytes(57)).bytes; + response.extend_from_slice(&bytes).unwrap(); + } +} + +#[cfg(feature = "ctaphid")] +const CTAPHID_COMMAND_RNG: Command = Command::Vendor(VendorCommand::H60); + +#[cfg(feature = "ctaphid")] +impl<'a, C: Client> ctaphid_dispatch::app::App<'a> for DummyApp { + fn commands(&self) -> &'static [Command] { + &[CTAPHID_COMMAND_RNG] + } + + fn call(&mut self, command: Command, _request: &Message, response: &mut Message) -> AppResult { + match command { + CTAPHID_COMMAND_RNG => self.rng(response), + _ => return Err(Error::InvalidCommand), + } + Ok(()) + } +} + +struct Apps { + dummy: DummyApp, +} + +impl<'a, S: StoreProvider> trussed_usbip::Apps<'a, S, CoreOnly> + for Apps> +{ + type Data = (); + + fn new(service: &mut Service, CoreOnly>, syscall: Syscall, _data: ()) -> Self { + let client = ClientBuilder::new("dummy") + .prepare(service) + .unwrap() + .build(syscall); + let dummy = DummyApp { client }; + Self { dummy } + } + + #[cfg(feature = "ctaphid")] + fn with_ctaphid_apps( + &mut self, + f: impl FnOnce(&mut [&mut dyn ctaphid_dispatch::app::App<'a>]) -> T, + ) -> T { + f(&mut [&mut self.dummy]) + } + + #[cfg(feature = "ccid")] + fn with_ccid_apps( + &mut self, + f: impl FnOnce(&mut [&mut dyn apdu_dispatch::app::App]) -> T, + ) -> T { + f(&mut []) + } +} + +fn main() { + pretty_env_logger::init(); + + let args = Args::parse(); + + let store = virt::Filesystem::new(args.state_file); + let options = trussed_usbip::Options { + manufacturer: Some(args.manufacturer), + product: Some(args.name), + serial_number: None, + vid: args.vid, + pid: args.pid, + }; + + log::info!("Initializing Trussed"); + trussed_usbip::Builder::new(store, options) + .build::>() + .exec(|_| ()); +} diff --git a/examples/fido.rs b/examples/fido.rs deleted file mode 100644 index 15d1367..0000000 --- a/examples/fido.rs +++ /dev/null @@ -1,197 +0,0 @@ -use std::path::{Path, PathBuf}; - -#[cfg(feature = "ccid")] -use apdu_dispatch::command::SIZE as ApduCommandSize; - -use clap::Parser; -use clap_num::maybe_hex; -use log::info; -use trussed::platform::{consent, reboot, ui}; -use trussed::{backend::Dispatch, virt, Client, Platform}; -use trussed_usbip::ClientBuilder; - -use fido_authenticator::TrussedRequirements; -use usbd_ctaphid::constants::MESSAGE_SIZE; - -pub type FidoConfig = fido_authenticator::Config; - -/// USP/IP based virtualization of the Nitrokey 3 / Solo2 device. -/// Supports FIDO application at the moment. -#[derive(Parser, Debug)] -#[clap(about, version, author)] -struct Args { - /// USB Name string - #[clap(short, long, default_value = "FIDO authenticator")] - name: String, - - /// USB Manufacturer string - #[clap(short, long, default_value = "Simulation")] - manufacturer: String, - - /// USB Serial string - #[clap(long, default_value = "SIM SIM SIM")] - serial: String, - - /// Trussed state file - #[clap(long, default_value = "trussed-state.bin")] - state_file: PathBuf, - - /// FIDO attestation key - #[clap(long)] - fido_key: Option, - - /// FIDO attestation cert - #[clap(long)] - fido_cert: Option, - - /// USB VID id - #[clap(short, long, parse(try_from_str=maybe_hex), default_value_t = 0x20a0)] - vid: u16, - /// USB PID id - #[clap(short, long, parse(try_from_str=maybe_hex), default_value_t = 0x42b2)] - pid: u16, -} - -struct Reboot; - -impl admin_app::Reboot for Reboot { - fn reboot() -> ! { - unimplemented!(); - } - - fn reboot_to_firmware_update() -> ! { - unimplemented!(); - } - - fn reboot_to_firmware_update_destructive() -> ! { - unimplemented!(); - } - - fn locked() -> bool { - false - } -} - -struct UserInterface { - start_time: std::time::Instant, -} - -impl UserInterface { - fn new() -> Self { - Self { - start_time: std::time::Instant::now(), - } - } -} - -impl trussed::platform::UserInterface for UserInterface { - /// Prompt user to type a word for confirmation - fn check_user_presence(&mut self) -> consent::Level { - // use std::io::Read as _; - // This is not nice - we should "peek" and return Level::None - // if there is no key pressed yet (unbuffered read from stdin). - // Couldn't get this to work (without pulling in ncurses or similar). - // std::io::stdin().bytes().next(); - consent::Level::Normal - } - - fn set_status(&mut self, status: ui::Status) { - info!("Set status: {:?}", status); - - if status == ui::Status::WaitingForUserPresence { - info!(">>>> Received confirmation request. Confirming automatically."); - } - } - - fn refresh(&mut self) {} - - fn uptime(&mut self) -> core::time::Duration { - self.start_time.elapsed() - } - - fn reboot(&mut self, to: reboot::To) -> ! { - info!("Restart! ({:?})", to); - std::process::exit(25); - } -} - -struct Apps { - fido: fido_authenticator::Authenticator, - admin: admin_app::App, -} - -impl trussed_usbip::Apps for Apps { - type Data = (); - - fn new>(builder: &B, _data: ()) -> Self { - let fido = fido_authenticator::Authenticator::new( - builder.build("fido", &[]), - fido_authenticator::Conforming {}, - fido_authenticator::Config { - max_msg_size: MESSAGE_SIZE, - skip_up_timeout: None, - }, - ); - let admin = admin_app::App::new(builder.build("admin", &[]), [0; 16], 0); - Self { fido, admin } - } - - fn with_ctaphid_apps( - &mut self, - f: impl FnOnce(&mut [&mut dyn ctaphid_dispatch::app::App]) -> T, - ) -> T { - f(&mut [&mut self.fido, &mut self.admin]) - } - - #[cfg(feature = "ccid")] - fn with_ccid_apps( - &mut self, - f: impl FnOnce(&mut [&mut dyn apdu_dispatch::app::App]) -> T, - ) -> T { - f(&mut []) - } -} - -fn main() { - pretty_env_logger::init(); - - let args = Args::parse(); - - let store = virt::Filesystem::new(args.state_file); - let options = trussed_usbip::Options { - manufacturer: Some(args.manufacturer), - product: Some(args.name), - serial_number: Some(args.serial), - vid: args.vid, - pid: args.pid, - }; - - log::info!("Initializing Trussed"); - trussed_usbip::Builder::new(store, options) - .init_platform(move |platform| { - let ui: Box = - Box::new(UserInterface::new()); - platform.user_interface().set_inner(ui); - - if let Some(fido_key) = &args.fido_key { - store_file(platform, fido_key, "fido/sec/00"); - } - if let Some(fido_cert) = &args.fido_cert { - store_file(platform, fido_cert, "fido/x5c/00"); - } - }) - .build::>() - .exec(|_| ()); -} - -fn store_file(platform: &impl Platform, host_file: &Path, device_file: &str) { - log::info!("Writing {} to file system", device_file); - let data = std::fs::read(host_file).expect("failed to read file"); - trussed::store::store( - platform.store(), - trussed::types::Location::Internal, - &trussed::types::PathBuf::from(device_file), - &data, - ) - .expect("failed to store file"); -}