diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 066a13e..f64853a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: if: matrix.target == 'x86_64-unknown-linux-gnu' - name: Run tests - run: cargo test --verbose + run: cargo test --verbose --features dispatch if: matrix.target == 'x86_64-unknown-linux-gnu' - name: Check formatting diff --git a/Cargo.toml b/Cargo.toml index 75464f0..5a7df98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,11 +45,19 @@ log-warn = [] log-error = [] [dev-dependencies] +ciborium = { version = "0.2.2" } +ctaphid = { version = "0.3.1", default-features = false } +delog = { version = "0.1.6", features = ["std-log"] } env_logger = "0.11.0" +interchange = "0.3.0" +log = "0.4.21" # quickcheck = "1" rand = "0.8.4" +serde_cbor = { version = "0.11.0", features = ["std"] } trussed = { version = "0.1", features = ["virt"] } +trussed-hkdf = { version = "0.1", features = ["virt"] } trussed-usbip = { version = "0.0.1", default-features = false, features = ["ctaphid"] } +trussed-staging = { version = "0.2.0", default-features = false, features = ["chunked"] } usbd-ctaphid = "0.1.0" [package.metadata.docs.rs] @@ -64,5 +72,6 @@ serde-indexed = { git = "https://github.com/sosthene-nitrokey/serde-indexed.git" trussed = { git = "https://github.com/Nitrokey/trussed.git", tag = "v0.1.0-nitrokey.18" } trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "chunked-v0.1.0" } trussed-hkdf = { git = "https://github.com/Nitrokey/trussed-hkdf-backend.git", tag = "v0.1.0" } +trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "v0.2.0" } trussed-usbip = { git = "https://github.com/Nitrokey/pc-usbip-runner.git", tag = "v0.0.1-nitrokey.1" } usbd-ctaphid = { git = "https://github.com/Nitrokey/usbd-ctaphid.git", tag = "v0.1.0-nitrokey.2" } diff --git a/examples/usbip.rs b/examples/usbip.rs index 280eeb5..b8cb34e 100644 --- a/examples/usbip.rs +++ b/examples/usbip.rs @@ -4,10 +4,14 @@ //! USB/IP runner for opcard. //! Run with cargo run --example usbip --features trussed/virt,dispatch -use trussed::backend::{BackendId, CoreOnly}; -use trussed::types::Location; -use trussed::virt::{self, Client, Ram, UserInterface}; -use trussed::{ClientImplementation, Platform}; +use trussed::api::{reply, request}; +use trussed::backend::BackendId; +use trussed::serde_extensions::{ExtensionDispatch, ExtensionId, ExtensionImpl}; +use trussed::service::ServiceResources; +use trussed::types::{Location, NoData}; +use trussed::virt::{self, Ram}; +use trussed::{ClientImplementation, Error, Platform}; +use trussed_hkdf::{HkdfBackend, HkdfExtension}; use trussed_usbip::ClientBuilder; const MANUFACTURER: &str = "Nitrokey"; @@ -15,17 +19,113 @@ const PRODUCT: &str = "Nitrokey 3"; const VID: u16 = 0x20a0; const PID: u16 = 0x42b2; -pub use trussed_hkdf::virt::Dispatcher; +#[derive(Default)] +struct Context { + #[cfg(feature = "chunked")] + staging: trussed_staging::StagingContext, +} -type VirtClient = ClientImplementation, Dispatcher>; +#[derive(Default)] +struct Dispatch { + #[cfg(feature = "chunked")] + staging: trussed_staging::StagingBackend, +} + +impl ExtensionDispatch for Dispatch { + type BackendId = Backend; + type Context = Context; + type ExtensionId = Extension; + + fn extension_request( + &mut self, + backend: &Self::BackendId, + extension: &Self::ExtensionId, + ctx: &mut trussed::types::Context, + request: &request::SerdeExtension, + resources: &mut ServiceResources

, + ) -> Result { + match backend { + #[cfg(feature = "chunked")] + Backend::Staging => match extension { + Extension::Chunked => self.staging.extension_request_serialized( + &mut ctx.core, + &mut ctx.backends.staging, + request, + resources, + ), + _ => Err(Error::RequestNotAvailable), + }, + Backend::Hkdf => match extension { + Extension::Hkdf => HkdfBackend.extension_request_serialized( + &mut ctx.core, + &mut NoData, + request, + resources, + ), + #[cfg(feature = "chunked")] + _ => Err(Error::RequestNotAvailable), + }, + } + } +} + +#[cfg(feature = "chunked")] +impl ExtensionId for Dispatch { + type Id = Extension; + + const ID: Extension = Extension::Chunked; +} + +impl ExtensionId for Dispatch { + type Id = Extension; + + const ID: Extension = Extension::Hkdf; +} + +enum Backend { + #[cfg(feature = "chunked")] + Staging, + Hkdf, +} + +enum Extension { + #[cfg(feature = "chunked")] + Chunked, + Hkdf, +} + +impl From for u8 { + fn from(extension: Extension) -> u8 { + match extension { + #[cfg(feature = "chunked")] + Extension::Chunked => 0, + Extension::Hkdf => 1, + } + } +} + +impl TryFrom for Extension { + type Error = Error; + + fn try_from(id: u8) -> Result { + match id { + #[cfg(feature = "chunked")] + 0 => Ok(Self::Chunked), + 1 => Ok(Self::Hkdf), + _ => Err(Error::InternalError), + } + } +} + +type VirtClient = ClientImplementation, Dispatch>; struct FidoApp { fido: fido_authenticator::Authenticator, } -impl trussed_usbip::Apps<'static, VirtClient, Dispatcher> for FidoApp { +impl trussed_usbip::Apps<'static, VirtClient, Dispatch> for FidoApp { type Data = (); - fn new>(builder: &B, _data: ()) -> Self { + fn new>(builder: &B, _data: ()) -> Self { let large_blogs = Some(fido_authenticator::LargeBlobsConfig { location: Location::External, #[cfg(feature = "chunked")] @@ -34,7 +134,15 @@ impl trussed_usbip::Apps<'static, VirtClient, Dispatcher> for FidoApp { FidoApp { fido: fido_authenticator::Authenticator::new( - builder.build("fido", &[BackendId::Core]), + builder.build( + "fido", + &[ + BackendId::Core, + BackendId::Custom(Backend::Hkdf), + #[cfg(feature = "chunked")] + BackendId::Custom(Backend::Staging), + ], + ), fido_authenticator::Conforming {}, fido_authenticator::Config { max_msg_size: usbd_ctaphid::constants::MESSAGE_SIZE, @@ -66,7 +174,7 @@ fn main() { pid: PID, }; trussed_usbip::Builder::new(virt::Ram::default(), options) - .dispatch(Dispatcher) + .dispatch(Dispatch::default()) .build::() .exec(|_platform| {}); } diff --git a/tests/basic.rs b/tests/basic.rs new file mode 100644 index 0000000..0e2030a --- /dev/null +++ b/tests/basic.rs @@ -0,0 +1,44 @@ +#![cfg(feature = "dispatch")] + +mod virt; +mod webauthn; + +use std::collections::BTreeMap; + +use ciborium::Value; +use ctap_types::ctap2::Operation; + +use webauthn::{MakeCredentialRequest, PubKeyCredParam, Rp, User}; + +#[test] +fn test_ping() { + virt::run_ctaphid(|device| { + device.ping(&[0xf1, 0xd0]).unwrap(); + }); +} + +#[test] +fn test_get_info() { + virt::run_ctap2(|device| { + let reply: BTreeMap = device.call(Operation::GetInfo, &Value::Null); + let versions: Vec = reply.get(&1).unwrap().deserialized().unwrap(); + assert!(versions.contains(&"FIDO_2_0".to_owned())); + assert!(versions.contains(&"FIDO_2_1".to_owned())); + }); +} + +#[test] +fn test_make_credential() { + virt::run_ctap2(|device| { + let rp = Rp::new("example.com"); + let user = User::new(b"id123") + .name("john.doe") + .display_name("John Doe"); + let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -7)]; + let request = MakeCredentialRequest::new(b"", rp, user, pub_key_cred_params); + let reply: BTreeMap = device.call(Operation::MakeCredential, &request.into()); + assert_eq!(reply.get(&1).unwrap(), &Value::from("packed")); + assert!(reply.contains_key(&2)); + assert!(reply.contains_key(&3)); + }); +} diff --git a/tests/virt/dispatch.rs b/tests/virt/dispatch.rs new file mode 100644 index 0000000..a2751a9 --- /dev/null +++ b/tests/virt/dispatch.rs @@ -0,0 +1,106 @@ +use trussed::{ + api::{reply, request}, + serde_extensions::{ExtensionDispatch, ExtensionId, ExtensionImpl}, + service::ServiceResources, + types::NoData, + Error, Platform, +}; +use trussed_hkdf::{HkdfBackend, HkdfExtension}; + +#[derive(Default)] +pub struct Context { + #[cfg(feature = "chunked")] + staging: trussed_staging::StagingContext, +} + +#[derive(Default)] +pub struct Dispatch { + #[cfg(feature = "chunked")] + staging: trussed_staging::StagingBackend, +} + +impl ExtensionDispatch for Dispatch { + type BackendId = Backend; + type Context = Context; + type ExtensionId = Extension; + + fn extension_request( + &mut self, + backend: &Self::BackendId, + extension: &Self::ExtensionId, + ctx: &mut trussed::types::Context, + request: &request::SerdeExtension, + resources: &mut ServiceResources

, + ) -> Result { + match backend { + #[cfg(feature = "chunked")] + Backend::Staging => match extension { + Extension::Chunked => self.staging.extension_request_serialized( + &mut ctx.core, + &mut ctx.backends.staging, + request, + resources, + ), + _ => Err(Error::RequestNotAvailable), + }, + Backend::Hkdf => match extension { + Extension::Hkdf => HkdfBackend.extension_request_serialized( + &mut ctx.core, + &mut NoData, + request, + resources, + ), + #[cfg(feature = "chunked")] + _ => Err(Error::RequestNotAvailable), + }, + } + } +} + +#[cfg(feature = "chunked")] +impl ExtensionId for Dispatch { + type Id = Extension; + + const ID: Extension = Extension::Chunked; +} + +impl ExtensionId for Dispatch { + type Id = Extension; + + const ID: Extension = Extension::Hkdf; +} + +pub enum Backend { + #[cfg(feature = "chunked")] + Staging, + Hkdf, +} + +pub enum Extension { + #[cfg(feature = "chunked")] + Chunked, + Hkdf, +} + +impl From for u8 { + fn from(extension: Extension) -> u8 { + match extension { + #[cfg(feature = "chunked")] + Extension::Chunked => 0, + Extension::Hkdf => 1, + } + } +} + +impl TryFrom for Extension { + type Error = Error; + + fn try_from(id: u8) -> Result { + match id { + #[cfg(feature = "chunked")] + 0 => Ok(Self::Chunked), + 1 => Ok(Self::Hkdf), + _ => Err(Error::InternalError), + } + } +} diff --git a/tests/virt/mod.rs b/tests/virt/mod.rs new file mode 100644 index 0000000..e98fa1e --- /dev/null +++ b/tests/virt/mod.rs @@ -0,0 +1,174 @@ +mod dispatch; +mod pipe; + +use std::{ + borrow::Cow, + cell::RefCell, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Once, OnceLock, + }, + thread, + time::{Duration, SystemTime}, +}; + +use ciborium::Value; +use ctap_types::ctap2::Operation; +use ctaphid::{ + error::{RequestError, ResponseError}, + HidDevice, HidDeviceInfo, +}; +use ctaphid_dispatch::{ + dispatch::Dispatch, + types::{Channel, Requester}, +}; +use fido_authenticator::{Authenticator, Config, Conforming}; +use serde::de::DeserializeOwned; +use trussed::{ + backend::BackendId, + virt::{self, Ram}, +}; + +use pipe::Pipe; + +static INIT_LOGGER: Once = Once::new(); +static CHANNEL: OnceLock = OnceLock::new(); + +pub fn run_ctaphid(f: F) -> T +where + F: FnOnce(ctaphid::Device) -> T, +{ + INIT_LOGGER.call_once(|| { + env_logger::init(); + }); + virt::with_platform(Ram::default(), |platform| { + platform.run_client_with_backends( + "fido", + dispatch::Dispatch::default(), + &[ + BackendId::Core, + BackendId::Custom(dispatch::Backend::Hkdf), + #[cfg(feature = "chunked")] + BackendId::Custom(dispatch::Backend::Staging), + ], + |client| { + // TODO: setup attestation cert + + let mut authenticator = Authenticator::new( + client, + Conforming {}, + Config { + max_msg_size: 0, + skip_up_timeout: None, + max_resident_credential_count: None, + large_blobs: None, + nfc_transport: false, + }, + ); + + let channel = CHANNEL.get_or_init(Channel::new); + let (rq, rp) = channel.split().unwrap(); + + let stop = Arc::new(AtomicBool::new(false)); + let poller_stop = stop.clone(); + let poller = thread::spawn(move || { + let mut dispatch = Dispatch::new(rp); + while !poller_stop.load(Ordering::Relaxed) { + dispatch.poll(&mut [&mut authenticator]); + thread::sleep(Duration::from_millis(10)); + } + }); + + let device = Device::new(rq); + let device = ctaphid::Device::new(device, DeviceInfo).unwrap(); + let result = f(device); + stop.store(true, Ordering::Relaxed); + poller.join().unwrap(); + result + }, + ) + }) +} + +pub fn run_ctap2(f: F) -> T +where + F: FnOnce(Ctap2) -> T, +{ + run_ctaphid(|device| f(Ctap2(device))) +} + +pub struct Ctap2(ctaphid::Device); + +impl Ctap2 { + pub fn call(&self, operation: Operation, data: &Value) -> T { + let mut serialized = Vec::new(); + ciborium::into_writer(data, &mut serialized).unwrap(); + let reply = self.0.ctap2(operation.into(), &serialized).unwrap(); + ciborium::from_reader(reply.as_slice()).unwrap() + } +} + +#[derive(Debug)] +pub struct DeviceInfo; + +impl HidDeviceInfo for DeviceInfo { + fn vendor_id(&self) -> u16 { + 0x20a0 + } + + fn product_id(&self) -> u16 { + 0x42b2 + } + + fn path(&self) -> Cow<'_, str> { + "test".into() + } +} + +pub struct Device(RefCell); + +impl Device { + fn new(requester: Requester<'static>) -> Self { + Self(RefCell::new(Pipe::new(requester))) + } +} + +impl HidDevice for Device { + type Info = DeviceInfo; + + fn send(&self, data: &[u8]) -> Result<(), RequestError> { + self.0.borrow_mut().push(data); + Ok(()) + } + + fn receive<'a>( + &self, + buffer: &'a mut [u8], + timeout: Option, + ) -> Result<&'a [u8], ResponseError> { + let start = SystemTime::now(); + + loop { + if let Some(timeout) = timeout { + let elapsed = start.elapsed().unwrap(); + if elapsed >= timeout { + return Err(ResponseError::Timeout); + } + } + + if let Some(response) = self.0.borrow_mut().pop() { + return if buffer.len() >= response.len() { + log::info!("received response: {} bytes", response.len()); + buffer[..response.len()].copy_from_slice(&response); + Ok(&buffer[..response.len()]) + } else { + Err(ResponseError::PacketReceivingFailed( + "invalid buffer size".into(), + )) + }; + } + + thread::sleep(Duration::from_millis(10)); + } + } +} diff --git a/tests/virt/pipe.rs b/tests/virt/pipe.rs new file mode 100644 index 0000000..0ecfefc --- /dev/null +++ b/tests/virt/pipe.rs @@ -0,0 +1,422 @@ +// Extracted from the usbd-ctaphid crate: +// https://github.com/trussed-dev/usbd-ctaphid/blob/1db2e014f28669bc484c81ab0406c54b16bba33c/src/pipe.rs +// +// License: Apache-2.0 or MIT +// +// Authors: +// - Conor Patrick +// - Nicolas Stalder +// - Robin Krahl +// - Sosthène Guédon + +use std::collections::VecDeque; + +use ctap_types::Error; +use ctaphid_dispatch::{command::Command, types::Requester}; +use heapless::Vec; + +const MESSAGE_SIZE: usize = 3072; +const PACKET_SIZE: usize = 64; + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +struct Version { + major: u8, + minor: u8, + build: u8, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +struct Request { + channel: u32, + command: Command, + length: u16, + timestamp: u32, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +struct Response { + channel: u32, + command: Command, + length: u16, +} + +impl Response { + fn from_request_and_size(request: Request, size: usize) -> Self { + Self { + channel: request.channel, + command: request.command, + length: size as u16, + } + } + + fn error_from_request(request: Request) -> Self { + Self::error_on_channel(request.channel) + } + + fn error_on_channel(channel: u32) -> Self { + Self { + channel, + command: Command::Error, + length: 1, + } + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub 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 += 1; + self.transmitted += PACKET_SIZE - 5; + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum State { + Idle, + Receiving((Request, MessageState)), + WaitingOnAuthenticator(Request), + WaitingToSend(Response), + Sending((Response, MessageState)), +} + +pub struct Pipe { + queue: VecDeque<[u8; PACKET_SIZE]>, + state: State, + interchange: Requester<'static>, + buffer: [u8; MESSAGE_SIZE], + last_channel: u32, + implements: u8, + last_milliseconds: u32, + started_processing: bool, + needs_keepalive: bool, + version: Version, +} + +impl Pipe { + pub fn new(interchange: Requester<'static>) -> Self { + Self { + queue: Default::default(), + state: State::Idle, + interchange, + buffer: [0; MESSAGE_SIZE], + last_channel: Default::default(), + implements: 0x84, + last_milliseconds: Default::default(), + started_processing: Default::default(), + needs_keepalive: Default::default(), + version: Default::default(), + } + } + + pub fn push(&mut self, packet: &[u8]) { + let (_, packet) = packet.split_first().unwrap(); + self.read_and_handle_packet(packet); + } + + pub fn pop(&mut self) -> Option<[u8; PACKET_SIZE]> { + self.handle_response(); + self.maybe_write_packet(); + self.queue.pop_front() + } + + fn read_and_handle_packet(&mut self, packet: &[u8]) { + if packet.len() != PACKET_SIZE { + panic!("unexpected packet size"); + } + let channel = u32::from_be_bytes(packet[..4].try_into().unwrap()); + let is_initialization = (packet[4] >> 7) != 0; + if is_initialization { + let command_number = packet[4] & !0x80; + let Ok(command) = Command::try_from(command_number) else { + self.start_sending_error_on_channel(channel, Error::InvalidCommand); + return; + }; + let length = u16::from_be_bytes(packet[5..][..2].try_into().unwrap()); + let timestamp = self.last_milliseconds; + let current_request = Request { + channel, + command, + length, + timestamp, + }; + + if !(self.state == State::Idle) { + let request = match self.state { + State::WaitingOnAuthenticator(request) => request, + State::Receiving((request, _message_state)) => request, + _ => { + return; + } + }; + if packet[4] == 0x86 { + // self.cancel_ongoing_activity(); + } else { + if channel == request.channel { + if command == Command::Cancel { + // self.cancel_ongoing_activity(); + } else { + self.start_sending_error(request, Error::InvalidSeq); + } + } else { + self.send_error_now(current_request, Error::ChannelBusy); + } + + return; + } + } + + if length > MESSAGE_SIZE as u16 { + self.send_error_now(current_request, Error::InvalidLength); + return; + } + + if length > PACKET_SIZE as u16 - 7 { + self.buffer[..PACKET_SIZE - 7].copy_from_slice(&packet[7..]); + self.state = State::Receiving((current_request, { MessageState::default() })); + } else { + self.buffer[..length as usize].copy_from_slice(&packet[7..][..length as usize]); + self.dispatch_request(current_request); + } + } else { + match self.state { + State::Receiving((request, mut message_state)) => { + let sequence = packet[4]; + if sequence != message_state.next_sequence { + self.start_sending_error(request, Error::InvalidSeq); + return; + } + if channel != request.channel { + return; + } + + let payload_length = request.length as usize; + if message_state.transmitted + (PACKET_SIZE - 5) < payload_length { + self.buffer[message_state.transmitted..][..PACKET_SIZE - 5] + .copy_from_slice(&packet[5..]); + message_state.absorb_packet(); + self.state = State::Receiving((request, message_state)); + } else { + let missing = request.length as usize - message_state.transmitted; + self.buffer[message_state.transmitted..payload_length] + .copy_from_slice(&packet[5..][..missing]); + self.dispatch_request(request); + } + } + _ => { + panic!("unexpected continuation packet"); + } + } + } + } + + fn start_sending(&mut self, response: Response) { + self.state = State::WaitingToSend(response); + self.maybe_write_packet(); + } + + fn start_sending_error(&mut self, request: Request, error: Error) { + self.start_sending_error_on_channel(request.channel, error); + } + + fn start_sending_error_on_channel(&mut self, channel: u32, error: Error) { + self.buffer[0] = error as u8; + let response = Response::error_on_channel(channel); + self.start_sending(response); + } + + fn send_error_now(&mut self, request: Request, error: Error) { + let last_state = core::mem::replace(&mut self.state, State::Idle); + let last_first_byte = self.buffer[0]; + + self.buffer[0] = error as u8; + let response = Response::error_from_request(request); + self.start_sending(response); + self.maybe_write_packet(); + + self.state = last_state; + self.buffer[0] = last_first_byte; + } + + fn maybe_write_packet(&mut self) { + match self.state { + 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_in_one_packet = 7 + response.length as usize <= PACKET_SIZE; + if fits_in_one_packet { + packet[7..][..response.length as usize] + .copy_from_slice(&self.buffer[..response.length as usize]); + self.state = State::Idle; + } else { + packet[7..].copy_from_slice(&self.buffer[..PACKET_SIZE - 7]); + } + + self.queue.push_back(packet); + + if fits_in_one_packet { + self.state = State::Idle; + } else { + self.state = State::Sending((response, MessageState::default())); + } + } + State::Sending((response, mut message_state)) => { + let mut packet = [0u8; PACKET_SIZE]; + packet[..4].copy_from_slice(&response.channel.to_be_bytes()); + packet[4] = message_state.next_sequence; + + let sent = message_state.transmitted; + let remaining = response.length as usize - sent; + let last_packet = 5 + remaining <= PACKET_SIZE; + if last_packet { + packet[5..][..remaining] + .copy_from_slice(&self.buffer[message_state.transmitted..][..remaining]); + } else { + packet[5..].copy_from_slice( + &self.buffer[message_state.transmitted..][..PACKET_SIZE - 5], + ); + } + + self.queue.push_back(packet); + + if last_packet { + self.state = State::Idle; + } else { + message_state.absorb_packet(); + self.state = State::Sending((response, message_state)); + } + } + _ => {} + } + } + + fn dispatch_request(&mut self, request: Request) { + match request.command { + Command::Init => {} + _ => { + if request.channel == 0xffffffff { + self.start_sending_error(request, Error::InvalidChannel); + return; + } + } + } + match request.command { + Command::Init => { + match request.channel { + 0 => { + self.start_sending_error(request, Error::InvalidChannel); + } + cid => { + if request.length == 8 { + self.last_channel += 1; + let _nonce = &self.buffer[..8]; + let response = Response { + channel: cid, + command: request.command, + length: 17, + }; + + self.buffer[8..12].copy_from_slice(&self.last_channel.to_be_bytes()); + // CTAPHID protocol version + self.buffer[12] = 2; + // major device version number + self.buffer[13] = self.version.major; + // minor device version number + self.buffer[14] = self.version.minor; + // build device version number + self.buffer[15] = self.version.build; + // capabilities flags + // 0x1: implements WINK + // 0x4: implements CBOR + // 0x8: does not implement MSG + // self.buffer[16] = 0x01 | 0x08; + self.buffer[16] = self.implements; + self.start_sending(response); + } + } + } + } + + Command::Ping => { + let response = Response::from_request_and_size(request, request.length as usize); + self.start_sending(response); + } + + Command::Cancel => { + // self.cancel_ongoing_activity(); + } + + _ => { + self.needs_keepalive = request.command == Command::Cbor; + if self.interchange.state() == interchange::State::Responded { + self.interchange.take_response(); + } + match self.interchange.request(( + request.command, + Vec::from_slice(&self.buffer[..request.length as usize]).unwrap(), + )) { + Ok(_) => { + self.state = State::WaitingOnAuthenticator(request); + self.started_processing = true; + } + Err(_) => { + self.send_error_now(request, Error::ChannelBusy); + } + } + } + } + } + + fn handle_response(&mut self) { + if let State::WaitingOnAuthenticator(request) = self.state { + if let Ok(response) = self.interchange.response() { + match &response.0 { + Err(ctaphid_dispatch::app::Error::InvalidCommand) => { + self.start_sending_error(request, Error::InvalidCommand); + } + Err(ctaphid_dispatch::app::Error::InvalidLength) => { + self.start_sending_error(request, Error::InvalidLength); + } + Err(ctaphid_dispatch::app::Error::NoResponse) => { + log::info!("Got waiting noresponse from authenticator??"); + } + + Ok(message) => { + if message.len() > self.buffer.len() { + log::error!( + "Message is longer than buffer ({} > {})", + message.len(), + self.buffer.len(), + ); + self.start_sending_error(request, Error::InvalidLength); + } else { + log::info!( + "Got {} bytes response from authenticator, starting send", + message.len() + ); + let response = Response::from_request_and_size(request, message.len()); + self.buffer[..message.len()].copy_from_slice(message); + self.start_sending(response); + } + } + } + } + } + } +} diff --git a/tests/webauthn/mod.rs b/tests/webauthn/mod.rs new file mode 100644 index 0000000..5236bb9 --- /dev/null +++ b/tests/webauthn/mod.rs @@ -0,0 +1,150 @@ +use ciborium::Value; + +#[derive(Default)] +pub struct Map(Vec<(Value, Value)>); + +impl Map { + pub fn push(&mut self, key: impl Into, value: impl Into) { + self.0.push((key.into(), value.into())); + } +} + +impl From for Value { + fn from(map: Map) -> Value { + Value::from(map.0) + } +} + +pub struct Rp { + id: String, + name: Option, +} + +impl Rp { + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + name: None, + } + } + + pub fn name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); + self + } +} + +impl From for Value { + fn from(rp: Rp) -> Value { + let mut map = Map::default(); + map.push("id", rp.id); + if let Some(name) = rp.name { + map.push("name", name); + } + map.into() + } +} + +pub struct User { + id: Vec, + name: Option, + display_name: Option, +} + +impl User { + pub fn new(id: impl Into>) -> Self { + Self { + id: id.into(), + name: None, + display_name: None, + } + } + + pub fn name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); + self + } + + pub fn display_name(mut self, display_name: impl Into) -> Self { + self.display_name = Some(display_name.into()); + self + } +} + +impl From for Value { + fn from(user: User) -> Value { + let mut map = Map::default(); + map.push("id", user.id); + if let Some(name) = user.name { + map.push("name", name); + } + if let Some(display_name) = user.display_name { + map.push("displayName", display_name); + } + map.into() + } +} + +pub struct PubKeyCredParam { + ty: String, + alg: i32, +} + +impl PubKeyCredParam { + pub fn new(ty: impl Into, alg: impl Into) -> Self { + Self { + ty: ty.into(), + alg: alg.into(), + } + } +} + +impl From for Value { + fn from(param: PubKeyCredParam) -> Value { + let mut map = Map::default(); + map.push("type", param.ty); + map.push("alg", param.alg); + map.into() + } +} + +pub struct MakeCredentialRequest { + client_data_hash: Vec, + rp: Rp, + user: User, + pub_key_cred_params: Vec, +} + +impl MakeCredentialRequest { + pub fn new( + client_data_hash: impl Into>, + rp: Rp, + user: User, + pub_key_cred_params: impl Into>, + ) -> Self { + Self { + client_data_hash: client_data_hash.into(), + rp, + user, + pub_key_cred_params: pub_key_cred_params.into(), + } + } +} + +impl From for Value { + fn from(request: MakeCredentialRequest) -> Value { + let mut map = Map::default(); + map.push(1, request.client_data_hash); + map.push(2, request.rp); + map.push(3, request.user); + map.push( + 4, + request + .pub_key_cred_params + .into_iter() + .map(Value::from) + .collect::>(), + ); + map.into() + } +}