mirror of
https://github.com/trussed-dev/fido-authenticator.git
synced 2026-06-20 04:16:16 -07:00
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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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| {});
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user