Setup basic integration tests

This patch adds basic integration tests that use the ctaphid library to
send CTAPHID commands to the authenticator over ctaphid-dispatch.
Unfortunately, usbd-ctaphid does not provide a public interface to its
packet handling code, so we have to copy that code for the time being.
This commit is contained in:
Robin Krahl
2024-03-21 21:16:42 +01:00
parent b8e9a12e3d
commit 7db98dd731
8 changed files with 1024 additions and 11 deletions
+1 -1
View File
@@ -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
+9
View File
@@ -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" }
+118 -10
View File
@@ -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<trussed_usbip::Service<Ram, Dispatcher>, 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<P: Platform>(
&mut self,
backend: &Self::BackendId,
extension: &Self::ExtensionId,
ctx: &mut trussed::types::Context<Self::Context>,
request: &request::SerdeExtension,
resources: &mut ServiceResources<P>,
) -> Result<reply::SerdeExtension, Error> {
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<trussed_chunked::ChunkedExtension> for Dispatch {
type Id = Extension;
const ID: Extension = Extension::Chunked;
}
impl ExtensionId<HkdfExtension> 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<Extension> for u8 {
fn from(extension: Extension) -> u8 {
match extension {
#[cfg(feature = "chunked")]
Extension::Chunked => 0,
Extension::Hkdf => 1,
}
}
}
impl TryFrom<u8> for Extension {
type Error = Error;
fn try_from(id: u8) -> Result<Self, Error> {
match id {
#[cfg(feature = "chunked")]
0 => Ok(Self::Chunked),
1 => Ok(Self::Hkdf),
_ => Err(Error::InternalError),
}
}
}
type VirtClient = ClientImplementation<trussed_usbip::Service<Ram, Dispatch>, Dispatch>;
struct FidoApp {
fido: fido_authenticator::Authenticator<fido_authenticator::Conforming, VirtClient>,
}
impl trussed_usbip::Apps<'static, VirtClient, Dispatcher> for FidoApp {
impl trussed_usbip::Apps<'static, VirtClient, Dispatch> for FidoApp {
type Data = ();
fn new<B: ClientBuilder<VirtClient, Dispatcher>>(builder: &B, _data: ()) -> Self {
fn new<B: ClientBuilder<VirtClient, Dispatch>>(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::<FidoApp>()
.exec(|_platform| {});
}
+44
View File
@@ -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<u8, Value> = device.call(Operation::GetInfo, &Value::Null);
let versions: Vec<String> = 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<u8, Value> = 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));
});
}
+106
View File
@@ -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<P: Platform>(
&mut self,
backend: &Self::BackendId,
extension: &Self::ExtensionId,
ctx: &mut trussed::types::Context<Self::Context>,
request: &request::SerdeExtension,
resources: &mut ServiceResources<P>,
) -> Result<reply::SerdeExtension, Error> {
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<trussed_chunked::ChunkedExtension> for Dispatch {
type Id = Extension;
const ID: Extension = Extension::Chunked;
}
impl ExtensionId<HkdfExtension> 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<Extension> for u8 {
fn from(extension: Extension) -> u8 {
match extension {
#[cfg(feature = "chunked")]
Extension::Chunked => 0,
Extension::Hkdf => 1,
}
}
}
impl TryFrom<u8> for Extension {
type Error = Error;
fn try_from(id: u8) -> Result<Self, Error> {
match id {
#[cfg(feature = "chunked")]
0 => Ok(Self::Chunked),
1 => Ok(Self::Hkdf),
_ => Err(Error::InternalError),
}
}
}
+174
View File
@@ -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<Channel> = OnceLock::new();
pub fn run_ctaphid<F, T>(f: F) -> T
where
F: FnOnce(ctaphid::Device<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, T>(f: F) -> T
where
F: FnOnce(Ctap2) -> T,
{
run_ctaphid(|device| f(Ctap2(device)))
}
pub struct Ctap2(ctaphid::Device<Device>);
impl Ctap2 {
pub fn call<T: DeserializeOwned>(&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<Pipe>);
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<Duration>,
) -> 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));
}
}
}
+422
View File
@@ -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 <conorpp94@gmail.com>
// - Nicolas Stalder <n@stalder.io>
// - Robin Krahl <robin@nitrokey.com>
// - Sosthène Guédon <sosthene@nitrokey.com>
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);
}
}
}
}
}
}
}
+150
View File
@@ -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>, value: impl Into<Value>) {
self.0.push((key.into(), value.into()));
}
}
impl From<Map> for Value {
fn from(map: Map) -> Value {
Value::from(map.0)
}
}
pub struct Rp {
id: String,
name: Option<String>,
}
impl Rp {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
name: None,
}
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
}
impl From<Rp> 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<u8>,
name: Option<String>,
display_name: Option<String>,
}
impl User {
pub fn new(id: impl Into<Vec<u8>>) -> Self {
Self {
id: id.into(),
name: None,
display_name: None,
}
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn display_name(mut self, display_name: impl Into<String>) -> Self {
self.display_name = Some(display_name.into());
self
}
}
impl From<User> 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<String>, alg: impl Into<i32>) -> Self {
Self {
ty: ty.into(),
alg: alg.into(),
}
}
}
impl From<PubKeyCredParam> 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<u8>,
rp: Rp,
user: User,
pub_key_cred_params: Vec<PubKeyCredParam>,
}
impl MakeCredentialRequest {
pub fn new(
client_data_hash: impl Into<Vec<u8>>,
rp: Rp,
user: User,
pub_key_cred_params: impl Into<Vec<PubKeyCredParam>>,
) -> Self {
Self {
client_data_hash: client_data_hash.into(),
rp,
user,
pub_key_cred_params: pub_key_cred_params.into(),
}
}
}
impl From<MakeCredentialRequest> 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::<Vec<_>>(),
);
map.into()
}
}