diff --git a/Cargo.toml b/Cargo.toml index cfb5ceb..eea1760 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,4 +55,8 @@ sha2 = "^0.8" base64 = "^0.10" env_logger = "0.6" hex-literal = "0.1.4" +once_cell = "0.1.8" simple_logger = "1.0.1" +ring = "0.14" +untrusted = "0.6" + diff --git a/cose/src/lib.rs b/cose/src/lib.rs index f15ab36..ba9851f 100644 --- a/cose/src/lib.rs +++ b/cose/src/lib.rs @@ -55,7 +55,7 @@ impl StdErrorT for Error { // https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves #[derive(Debug, Copy, Clone, PartialEq, Eq)] #[repr(u8)] -enum EllipticCurve { +pub enum EllipticCurve { P256 = 1, P384 = 2, // TODO(baloo): looks unsupported by openssl, have to check @@ -140,6 +140,13 @@ impl PublicKey { Ok((x.to_vec().into(), y.to_vec().into())) } + + pub fn new(curve: EllipticCurve, bytes: Vec) -> Self { + PublicKey { + curve, + bytes, + } + } } impl<'de> Deserialize<'de> for PublicKey { @@ -251,3 +258,9 @@ impl Serialize for PublicKey { map.end() } } + +impl AsRef<[u8]> for PublicKey { + fn as_ref(&self) -> &[u8] { + self.bytes.as_ref() + } +} diff --git a/src/ctap.rs b/src/ctap.rs index 6a99bf0..62e7f3f 100644 --- a/src/ctap.rs +++ b/src/ctap.rs @@ -1,5 +1,7 @@ use std::fmt; +#[cfg(test)] +use serde::de::{self, Deserialize, Deserializer, Visitor}; use serde::ser::{Serialize, SerializeMap, Serializer}; use serde_json as json; use sha2::{Digest, Sha256}; @@ -146,6 +148,38 @@ impl Serialize for ClientDataHash { } } +#[cfg(test)] +impl<'de> Deserialize<'de> for ClientDataHash { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct ClientDataHashVisitor; + + impl<'de> Visitor<'de> for ClientDataHashVisitor { + type Value = ClientDataHash; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a byte string") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: de::Error, + { + let mut out = [0u8; 32]; + if out.len() != v.len() { + return Err(E::custom("unexpected byte len")); + } + out.copy_from_slice(v); + Ok(ClientDataHash(out)) + } + } + + deserializer.deserialize_bytes(ClientDataHashVisitor) + } +} + impl CollectedClientData { pub fn hash(&self) -> json::Result { // TODO(baloo): this could use a bit more spec, there is no ordering specified. Are spaces diff --git a/src/ctap2/attestation.rs b/src/ctap2/attestation.rs index de36ac9..9384300 100644 --- a/src/ctap2/attestation.rs +++ b/src/ctap2/attestation.rs @@ -1,7 +1,13 @@ use std::fmt; +#[cfg(test)] +use std::io::{self, Write}; +#[cfg(test)] +use byteorder::{BigEndian, WriteBytesExt}; use nom::{be_u16, be_u32, be_u8, Err as NomErr, IResult}; use serde::de::{self, Deserialize, Deserializer, Error as SerdeError, MapAccess, Visitor}; +#[cfg(test)] +use serde::ser::{Error as SerError, Serialize, SerializeMap, Serializer}; use serde_bytes::ByteBuf; use super::server::Alg; @@ -220,6 +226,29 @@ impl<'de> Deserialize<'de> for AuthenticatorData { } } +#[cfg(test)] +impl Serialize for AuthenticatorData { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut data = io::Cursor::new(Vec::new()); + data.write_all(&self.rp_id_hash.0) + .map_err(|e| S::Error::custom(format!("io error: {:?}", e)))?; + data.write_all(&[self.flags.bits()]) + .map_err(|e| S::Error::custom(format!("io error: {:?}", e)))?; + data.write_u32::(self.counter) + .map_err(|e| S::Error::custom(format!("io error: {:?}", e)))?; + + // TODO(baloo): need to yield credential_data and extensions, but that dependends on flags, + // should we consider another type system? + + data.set_position(0); + let data = data.into_inner(); + serde_bytes::serialize(&data[..], serializer) + } +} + #[derive(Debug, Serialize, PartialEq, Eq)] /// x509 encoded attestation certificate pub struct AttestationCertificate(Vec); @@ -260,10 +289,30 @@ impl fmt::Debug for Signature { } } +#[derive(Debug, PartialEq, Eq)] +pub enum AttestationStatement { + None, + Packed(AttestationStatementPacked), + // TODO(baloo): there is a couple other options than None and Packed: + // https://w3c.github.io/webauthn/#generating-an-attestation-object + // https://w3c.github.io/webauthn/#defined-attestation-formats + //TPM, + //AndroidKey, + //AndroidSafetyNet, + //FidoU2F, + // TODO(baloo): should we do an Unknow version? most likely +} + +impl AttestationStatement { + pub fn is_none(&self) -> bool { + *self == AttestationStatement::None + } +} + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] // TODO(baloo): there is a couple other options than x5c: // https://www.w3.org/TR/webauthn/#packed-attestation -pub struct AttestationStatement { +pub struct AttestationStatementPacked { alg: Alg, sig: Signature, @@ -273,15 +322,15 @@ pub struct AttestationStatement { } #[derive(Debug, PartialEq, Eq)] -pub enum AttestationFormat { +enum AttestationFormat { Packed, + None, // TOOD(baloo): only packed is implemented for now see spec: // https://www.w3.org/TR/webauthn/#defined-attestation-formats - TPM, - AndroidKey, - AndroidSafetyNet, - FidoU2F, - NONE, + //TPM, + //AndroidKey, + //AndroidSafetyNet, + //FidoU2F, } impl<'de> Deserialize<'de> for AttestationFormat { @@ -304,6 +353,7 @@ impl<'de> Deserialize<'de> for AttestationFormat { { match v { "packed" => Ok(AttestationFormat::Packed), + "none" => Ok(AttestationFormat::None), other => Err(de::Error::custom(format!("unexpected value {}", other))), } } @@ -315,7 +365,6 @@ impl<'de> Deserialize<'de> for AttestationFormat { #[derive(Debug, PartialEq, Eq)] pub struct AttestationObject { - pub format: AttestationFormat, pub auth_data: AuthenticatorData, pub att_statement: AttestationStatement, } @@ -338,7 +387,7 @@ impl<'de> Deserialize<'de> for AttestationObject { where M: MapAccess<'de>, { - let mut format = None; + let mut format: Option = None; let mut auth_data = None; let mut att_statement = None; @@ -360,24 +409,30 @@ impl<'de> Deserialize<'de> for AttestationObject { auth_data = Some(map.next_value()?); } 3 => { + let format = format.as_ref().ok_or(de::Error::missing_field("fmt"))?; if att_statement.is_some() { return Err(de::Error::duplicate_field("att_statement")); } - att_statement = Some(map.next_value()?); + match format { + // This should not actually happen, but ... + AttestationFormat::None => { + att_statement = Some(AttestationStatement::None); + } + AttestationFormat::Packed => { + att_statement = + Some(AttestationStatement::Packed(map.next_value()?)); + } + } } k => return Err(M::Error::custom(format!("unexpected key: {:?}", k))), } } - // TODO(baloo): convert those custom error to missing_field - let format = format.ok_or_else(|| M::Error::custom("found no fmt".to_string()))?; let auth_data = auth_data.ok_or_else(|| M::Error::custom("found no auth_data".to_string()))?; - let att_statement = att_statement - .ok_or_else(|| M::Error::custom("found no att_statement".to_string()))?; + let att_statement = att_statement.unwrap_or(AttestationStatement::None); Ok(AttestationObject { - format, auth_data, att_statement, }) @@ -388,6 +443,35 @@ impl<'de> Deserialize<'de> for AttestationObject { } } +#[cfg(test)] +impl Serialize for AttestationObject { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map_len = 2; + if !self.att_statement.is_none() { + map_len += 1; + } + + let mut map = serializer.serialize_map(Some(map_len))?; + + map.serialize_entry(&2, &self.auth_data)?; + + match self.att_statement { + AttestationStatement::None => { + map.serialize_entry(&1, &"none")?; + } + AttestationStatement::Packed(ref v) => { + map.serialize_entry(&1, &"packed")?; + map.serialize_entry(&3, v)?; + } + } + + map.end() + } +} + #[cfg(test)] mod test { use super::super::utils::from_slice_stream; diff --git a/src/ctap2/commands.rs b/src/ctap2/commands.rs index 78229a7..9f61f16 100644 --- a/src/ctap2/commands.rs +++ b/src/ctap2/commands.rs @@ -26,10 +26,9 @@ use crate::transport::{self, FidoDevice}; use crate::ctap2::attestation::AAGuid; use crate::ctap2::attestation::AttestationObject; -use crate::ctap2::server::PublicKeyCredentialDescriptor; -use crate::ctap2::server::PublicKeyCredentialParameters; -use crate::ctap2::server::RelyingParty; -use crate::ctap2::server::User; +use crate::ctap2::server::{ + PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, User, +}; use crate::ctap::{ClientDataHash, CollectedClientData, Version}; @@ -63,6 +62,7 @@ trait RequestWithPin: Request { // Spec: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticator-api #[repr(u8)] +#[derive(Debug)] pub enum Command { MakeCredentials = 0x01, GetAssertion = 0x02, @@ -72,6 +72,21 @@ pub enum Command { GetNextAssertion = 0x08, } +impl Command { + #[cfg(test)] + pub fn from_u8(v: u8) -> Option { + match v { + 0x01 => Some(Command::MakeCredentials), + 0x02 => Some(Command::GetAssertion), + 0x04 => Some(Command::GetInfo), + 0x06 => Some(Command::ClientPin), + 0x07 => Some(Command::Reset), + 0x08 => Some(Command::GetNextAssertion), + _ => None, + } + } +} + #[derive(Debug)] pub enum StatusCode { /// Indicates successful response. @@ -228,6 +243,57 @@ impl From for StatusCode { } } +#[cfg(test)] +impl Into for StatusCode { + fn into(self) -> u8 { + match self { + StatusCode::OK => 0x00, + StatusCode::InvalidCommand => 0x01, + StatusCode::InvalidParameter => 0x02, + StatusCode::InvalidLength => 0x03, + StatusCode::InvalidSeq => 0x04, + StatusCode::Timeout => 0x05, + StatusCode::ChannelBusy => 0x06, + StatusCode::LockRequired => 0x0A, + StatusCode::InvalidChannel => 0x0B, + StatusCode::CBORUnexpectedType => 0x11, + StatusCode::InvalidCBOR => 0x12, + StatusCode::MissingParameter => 0x14, + StatusCode::LimitExceeded => 0x15, + StatusCode::UnsupportedExtension => 0x16, + StatusCode::CredentialExcluded => 0x19, + StatusCode::Processing => 0x21, + StatusCode::InvalidCredential => 0x22, + StatusCode::UserActionPending => 0x23, + StatusCode::OperationPending => 0x24, + StatusCode::NoOperations => 0x25, + StatusCode::UnsupportedAlgorithm => 0x26, + StatusCode::OperationDenied => 0x27, + StatusCode::KeyStoreFull => 0x28, + StatusCode::NoOperationPending => 0x2A, + StatusCode::UnsupportedOption => 0x2B, + StatusCode::InvalidOption => 0x2C, + StatusCode::KeepaliveCancel => 0x2D, + StatusCode::NoCredentials => 0x2E, + StatusCode::UserActionTimeout => 0x2f, + StatusCode::NotAllowed => 0x30, + StatusCode::PinInvalid => 0x31, + StatusCode::PinBlocked => 0x32, + StatusCode::PinAuthInvalid => 0x33, + StatusCode::PinAuthBlocked => 0x34, + StatusCode::PinNotSet => 0x35, + StatusCode::PinRequired => 0x36, + StatusCode::PinPolicyViolation => 0x37, + StatusCode::PinTokenExpired => 0x38, + StatusCode::RequestTooLarge => 0x39, + StatusCode::ActionTimeout => 0x3A, + StatusCode::UpRequired => 0x3B, + + StatusCode::Unknown(othr) => othr, + } + } +} + #[derive(Debug, Copy, Clone)] pub enum PinError { PinIsTooShort, @@ -256,6 +322,7 @@ impl StdErrorT for PinError { } #[derive(Debug)] +#[cfg_attr(test, derive(Deserialize))] pub struct PinAuth([u8; 16]); impl AsRef<[u8]> for PinAuth { @@ -358,6 +425,7 @@ where } #[derive(Debug, Serialize)] +#[cfg_attr(test, derive(Deserialize))] pub struct MakeCredentialsOptions { #[serde(rename = "rk")] resident_key: bool, @@ -1208,13 +1276,13 @@ impl AsRef<[u8]> for PinToken { } #[cfg(test)] -mod test { +pub mod test { use super::{AuthenticatorInfo, MakeCredentials, Request}; use crate::ctap::{CollectedClientData, WebauthnType}; use crate::ctap2::server::{Alg, PublicKeyCredentialParameters, RelyingParty, User}; use serde_cbor::de::from_slice; - const MAKE_CREDENTIALS_SAMPLE_RESPONSE: [u8; 666] = + pub const MAKE_CREDENTIALS_SAMPLE_RESPONSE: [u8; 666] = include!("tests/MAKE_CREDENTIALS_SAMPLE_RESPONSE,in"); #[test] @@ -1256,7 +1324,8 @@ mod test { ); } - const AUTHENTICATOR_INFO_PAYLOAD: [u8; 85] = include!("tests/AUTHENTICATOR_INFO_PAYLOAD.in"); + pub const AUTHENTICATOR_INFO_PAYLOAD: [u8; 85] = + include!("tests/AUTHENTICATOR_INFO_PAYLOAD.in"); #[test] fn parse_authenticator_info() { diff --git a/src/ctap2/server.rs b/src/ctap2/server.rs index 56ac4d3..a82899d 100644 --- a/src/ctap2/server.rs +++ b/src/ctap2/server.rs @@ -4,10 +4,19 @@ use serde_bytes::ByteBuf; use serde_cbor::error; use serde_cbor::ser; -use serde::de::{Deserialize, Deserializer, Error, Unexpected, Visitor}; +#[cfg(test)] +use serde::de::MapAccess; +use serde::de::{self, Deserialize, Deserializer, Unexpected, Visitor}; use serde::ser::{Serialize, SerializeMap, Serializer}; +#[cfg(test)] +use sha2::{Digest, Sha256}; + +#[cfg(test)] +use crate::ctap2::attestation::RpIdHash; + #[derive(Debug, Serialize)] +#[cfg_attr(test, derive(Deserialize))] pub struct RelyingParty { // TODO(baloo): spec is wrong !!!!111 // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#commands @@ -19,9 +28,22 @@ impl RelyingParty { pub fn to_ctap2(&self) -> Result, error::Error> { ser::to_vec(self) } + + #[cfg(test)] + pub fn hash(&self) -> RpIdHash { + let mut hasher = Sha256::new(); + hasher.input(&self.id.as_bytes()[..]); + + let mut output = [0u8; 32]; + let len = output.len(); + output.copy_from_slice(&hasher.result().as_slice()[..len]); + + RpIdHash(output) + } } #[derive(Debug, Serialize, Clone, Eq, PartialEq)] +#[cfg_attr(test, derive(Deserialize))] pub struct User { #[serde(with = "serde_bytes")] pub id: Vec, @@ -89,12 +111,12 @@ impl<'de> Deserialize<'de> for Alg { fn visit_i64(self, v: i64) -> Result where - E: Error, + E: de::Error, { match v { -7 => Ok(Alg::ES256), -257 => Ok(Alg::RS256), - v => Err(Error::invalid_value(Unexpected::Signed(v), &self)), + v => Err(de::Error::invalid_value(Unexpected::Signed(v), &self)), } } } @@ -120,6 +142,68 @@ impl Serialize for PublicKeyCredentialParameters { } } +#[cfg(test)] +impl<'de> Deserialize<'de> for PublicKeyCredentialParameters { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct PublicKeyCredentialParametersVisitor; + + impl<'de> Visitor<'de> for PublicKeyCredentialParametersVisitor { + type Value = PublicKeyCredentialParameters; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'de>, + { + let mut found_type = false; + let mut alg = None; + + while let Some(key) = map.next_key()? { + match key { + "alg" => { + if alg.is_some() { + return Err(de::Error::duplicate_field("alg")); + } + + alg = Some(map.next_value()?); + } + "type" => { + if found_type { + return Err(de::Error::duplicate_field("type")); + } + + let v: &str = map.next_value()?; + if v != "public-key" { + return Err(de::Error::custom(format!("invalid value: {}", v))); + } + found_type = true; + } + v => { + return Err(de::Error::unknown_field(v, &[])); + } + } + } + + if !found_type { + return Err(de::Error::missing_field("type")); + } + + let alg = alg.ok_or(de::Error::missing_field("alg"))?; + + Ok(PublicKeyCredentialParameters { alg }) + } + } + + deserializer.deserialize_bytes(PublicKeyCredentialParametersVisitor) + } +} + #[derive(Debug, PartialEq, Eq)] pub enum Transport { USB, @@ -142,6 +226,39 @@ impl Serialize for Transport { } } +#[cfg(test)] +impl<'de> Deserialize<'de> for Transport { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct TransportVisitor; + + impl<'de> Visitor<'de> for TransportVisitor { + type Value = Transport; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + match v { + "usb" => Ok(Transport::USB), + "nfc" => Ok(Transport::NFC), + "ble" => Ok(Transport::BLE), + "internal" => Ok(Transport::Internal), + v => Err(E::custom(format!("unknown value: {}", v))), + } + } + } + + deserializer.deserialize_bytes(TransportVisitor) + } +} + #[derive(Debug)] pub struct PublicKeyCredentialDescriptor { id: Vec, @@ -161,6 +278,77 @@ impl Serialize for PublicKeyCredentialDescriptor { } } +#[cfg(test)] +impl<'de> Deserialize<'de> for PublicKeyCredentialDescriptor { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct PublicKeyCredentialDescriptorVisitor; + + impl<'de> Visitor<'de> for PublicKeyCredentialDescriptorVisitor { + type Value = PublicKeyCredentialDescriptor; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'de>, + { + let mut found_type = false; + let mut id = None; + let mut transports = None; + + while let Some(key) = map.next_key()? { + match key { + "id" => { + if id.is_some() { + return Err(de::Error::duplicate_field("id")); + } + + id = Some(map.next_value()?); + } + "transports" => { + if transports.is_some() { + return Err(de::Error::duplicate_field("transports")); + } + + transports = Some(map.next_value()?); + } + "type" => { + if found_type { + return Err(de::Error::duplicate_field("type")); + } + + let v: &str = map.next_value()?; + if v != "public-key" { + return Err(de::Error::custom(format!("invalid value: {}", v))); + } + found_type = true; + } + v => { + return Err(de::Error::unknown_field(v, &[])); + } + } + } + + if !found_type { + return Err(de::Error::missing_field("type")); + } + + let id = id.ok_or(de::Error::missing_field("id"))?; + let transports = transports.ok_or(de::Error::missing_field("transports"))?; + + Ok(PublicKeyCredentialDescriptor { id, transports }) + } + } + + deserializer.deserialize_bytes(PublicKeyCredentialDescriptorVisitor) + } +} + #[cfg(test)] mod test { use super::Alg; diff --git a/src/lib.rs b/src/lib.rs index cafae59..87e6509 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,6 +42,14 @@ extern crate openssl; #[cfg(test)] #[macro_use] extern crate hex_literal; +#[cfg(test)] +extern crate once_cell; +#[cfg(test)] +extern crate ring; +#[cfg(test)] +extern crate simple_logger; +#[cfg(test)] +extern crate untrusted; mod consts; mod statemachine; @@ -106,3 +114,6 @@ pub use consts::*; pub use u2fprotocol::*; #[cfg(fuzzing)] pub use u2ftypes::*; + +#[cfg(test)] +mod tests; diff --git a/src/manager.rs b/src/manager.rs index 4f1dfae..a2badce 100644 --- a/src/manager.rs +++ b/src/manager.rs @@ -14,6 +14,9 @@ use crate::ctap2::server::{PublicKeyCredentialParameters, RelyingParty, User}; use runloop::RunLoop; use statemachine::StateMachine; use util::OnceCallback; +#[cfg(test)] +use crate::transport::platform::TestCase; + enum QueueAction { Register { @@ -46,8 +49,17 @@ impl FidoManager { pub fn new() -> io::Result { let (tx, rx) = channel(); + // Tests case injection works with thread local storage values, + // this looks up the value, and reinject it inside the new thread. + // This is only enabled for tests + #[cfg(test)] + let value = TestCase::active(); + // Start a new work queue thread. let queue = RunLoop::new(move |alive| { + #[cfg(test)] + TestCase::activate(value); + let mut sm = StateMachine::new(); while alive() { diff --git a/src/tests/common.rs b/src/tests/common.rs new file mode 100644 index 0000000..d5650d8 --- /dev/null +++ b/src/tests/common.rs @@ -0,0 +1,7 @@ +extern crate simple_logger; + +use simple_logger::init; + +pub fn setup() { + let _ = init(); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 0000000..e345f42 --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1,81 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::sync::mpsc; + +use crate::ctap2::commands::Pin; +use crate::ctap2::server::{Alg, PublicKeyCredentialParameters, User}; +use crate::transport::platform::TestCase; +use crate::FidoManager; + +mod common; + +#[test] +fn test_write_error() { + common::setup(); + debug!("activating writeerror"); + TestCase::activate(TestCase::WriteError); + + let manager = FidoManager::new().unwrap(); + let (tx, rx) = mpsc::channel(); + + manager + .register( + String::from("example.com"), + String::from("https://www.example.com"), + 15_000, + vec![0, 1, 2, 3], + User { + id: vec![0], + name: String::from("j.doe"), + display_name: None, + icon: None, + }, + vec![PublicKeyCredentialParameters { alg: Alg::ES256 }], + Some(Pin::new("1234")), + move |rv| { + tx.send(rv.unwrap()).unwrap(); + }, + ) + .unwrap(); + + let res = rx.recv(); + debug!("res = {:?}", res); + assert_eq!(res, Err(mpsc::RecvError)); +} + +#[test] +fn test_simple_fido2() { + common::setup(); + TestCase::activate(TestCase::Fido2Simple); + + let manager = FidoManager::new().unwrap(); + let (tx, rx) = mpsc::channel(); + + manager + .register( + String::from("example.com"), + String::from("https://www.example.com"), + 15_000, + vec![0, 1, 2, 3], + User { + id: vec![0], + name: String::from("j.doe"), + display_name: None, + icon: None, + }, + vec![PublicKeyCredentialParameters { alg: Alg::ES256 }], + Some(Pin::new("1234")), + move |rv| { + tx.send(rv.unwrap()).unwrap(); + }, + ) + .unwrap(); + + let register_data = try_or!(rx.recv(), |res| { + debug!("result {:?}", res); + panic!("Problem receiving, unable to continue"); + }); + println!("Register result: {:?}", register_data); +} diff --git a/src/transport/test/commands.rs b/src/transport/test/commands.rs new file mode 100644 index 0000000..6bdcf73 --- /dev/null +++ b/src/transport/test/commands.rs @@ -0,0 +1,142 @@ +use std::collections::BTreeMap; +use std::fmt; + +use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor}; +use serde_cbor::Value; + +use crate::ctap::ClientDataHash; +use crate::ctap2::commands::{MakeCredentialsOptions, PinAuth}; +use crate::ctap2::server::{ + PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, User, +}; + +#[derive(Debug)] +pub struct MakeCredentials { + client_data: ClientDataHash, + rp: RelyingParty, + user: User, + pub_cred_params: Vec, + exclude_list: Vec, + extensions: BTreeMap, + options: Option, + pin_auth: Option, + pin_protocol: Option, +} + +impl MakeCredentials { + pub fn rp(&self) -> &RelyingParty { + &self.rp + } +} + +impl<'de> Deserialize<'de> for MakeCredentials { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct MakeCredentialsVisitor; + + impl<'de> Visitor<'de> for MakeCredentialsVisitor { + type Value = MakeCredentials; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'de>, + { + let mut client_data = None; + let mut rp = None; + let mut user = None; + let mut pub_cred_params = Vec::new(); + let mut exclude_list = Vec::new(); + let mut extensions = BTreeMap::new(); + let mut options = None; + let mut pin_auth = None; + let mut pin_protocol = None; + + while let Some(key) = map.next_key()? { + match key { + 1 => { + if client_data.is_some() { + return Err(de::Error::duplicate_field("client_data")); + } + client_data = Some(map.next_value()?); + } + 2 => { + if rp.is_some() { + return Err(de::Error::duplicate_field("rp")); + } + rp = Some(map.next_value()?); + } + 3 => { + if user.is_some() { + return Err(de::Error::duplicate_field("user")); + } + user = Some(map.next_value()?); + } + 4 => { + if !pub_cred_params.is_empty() { + return Err(de::Error::duplicate_field("pub_cred_params")); + } + pub_cred_params = map.next_value()?; + } + 5 => { + if !exclude_list.is_empty() { + return Err(de::Error::duplicate_field("exclude_list")); + } + exclude_list = map.next_value()?; + } + 6 => { + if !extensions.is_empty() { + return Err(de::Error::duplicate_field("extensions")); + } + extensions = map.next_value()?; + } + 7 => { + if options.is_some() { + return Err(de::Error::duplicate_field("options")); + } + options = Some(map.next_value()?); + } + 8 => { + if pin_auth.is_some() { + return Err(de::Error::duplicate_field("pin_auth")); + } + pin_auth = Some(map.next_value()?); + } + 9 => { + if pin_protocol.is_some() { + return Err(de::Error::duplicate_field("pin_protocol")); + } + pin_protocol = Some(map.next_value()?); + } + v => { + return Err(de::Error::unknown_field(&format!("{}", v), &[])); + } + } + } + + let client_data = client_data.ok_or(de::Error::missing_field("client_data"))?; + let rp = rp.ok_or(de::Error::missing_field("rp"))?; + let user = user.ok_or(de::Error::missing_field("user"))?; + + Ok(MakeCredentials { + client_data, + rp, + user, + pub_cred_params, + exclude_list, + extensions, + options, + pin_auth, + pin_protocol, + }) + } + } + + deserializer.deserialize_bytes(MakeCredentialsVisitor) + } +} diff --git a/src/transport/test/crypto.rs b/src/transport/test/crypto.rs new file mode 100644 index 0000000..3b54e2e --- /dev/null +++ b/src/transport/test/crypto.rs @@ -0,0 +1,37 @@ +use ring::{ + error::{KeyRejected, Unspecified}, + rand, + signature::{self, EcdsaKeyPair, KeyPair, ECDSA_P256_SHA256_FIXED_SIGNING}, +}; + +use cose::EllipticCurve; +pub use cose::PublicKey; + +#[derive(Debug)] +pub enum Error { + Generation(Unspecified), + Invalid(KeyRejected), +} + +#[derive(Debug)] +pub struct PrivateKey(EcdsaKeyPair); + +impl PrivateKey { + pub fn generate() -> Result { + let rng = rand::SystemRandom::new(); + let pkcs8_bytes = + signature::EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng) + .map_err(Error::Generation)?; + let key_pair = signature::EcdsaKeyPair::from_pkcs8( + &ECDSA_P256_SHA256_FIXED_SIGNING, + untrusted::Input::from(pkcs8_bytes.as_ref()), + ) + .map_err(Error::Invalid)?; + Ok(PrivateKey(key_pair)) + } + + pub fn public_key(&self) -> PublicKey { + let pub_key = self.0.public_key(); + PublicKey::new(EllipticCurve::P256, Vec::from(pub_key.as_ref())) + } +} diff --git a/src/transport/test/device.rs b/src/transport/test/device.rs index 1e1d2e8..389814d 100644 --- a/src/transport/test/device.rs +++ b/src/transport/test/device.rs @@ -4,18 +4,19 @@ extern crate libc; +use std::fmt; use std::io; -use std::io::{Read, Write}; use super::TestCase; use consts::CID_BROADCAST; use ctap2::commands::{AuthenticatorInfo, ECDHSecret}; -use transport::hid::{Cid, DeviceVersion, HIDDevice}; -use transport::{Capability, Error}; +use transport::hid::{Capability, Cid, DeviceVersion, HIDDevice}; +use transport::Error; #[derive(Debug)] pub struct Device { test_case: TestCase, + inner: Box, initialized: bool, cid: Cid, u2fhid_version: u8, @@ -31,20 +32,19 @@ impl PartialEq for Device { } } -impl Read for Device { +impl io::Read for Device { fn read(&mut self, buf: &mut [u8]) -> io::Result { - debug!("writing to device {:?}: {:?}", self.test_case, buf); - Ok(buf.len()) + self.inner.read(buf) } } -impl Write for Device { - fn write(&mut self, _buf: &[u8]) -> io::Result { - Ok(0) +impl io::Write for Device { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.inner.write(buf) } fn flush(&mut self) -> io::Result<()> { - Ok(()) + self.inner.flush() } } @@ -53,8 +53,15 @@ impl HIDDevice for Device { type Id = TestCase; fn new(test_case: TestCase) -> Result { + debug!("test_case={:?}", test_case); + let inner: Box = match test_case { + TestCase::WriteError => Box::new(write_error::WriteErrorDevice::default()), + TestCase::Fido2Simple => Box::new(fido2simple::Fido2SimpleDevice::default()), + }; + Ok(Self { test_case, + inner, initialized: false, cid: CID_BROADCAST, u2fhid_version: 0, @@ -123,3 +130,472 @@ impl HIDDevice for Device { self.authenticator_info = Some(authenticator_info) } } + +trait TestDevice: io::Read + io::Write + fmt::Debug {} + +mod write_error { + use super::TestDevice; + use std::io; + + #[derive(Debug)] + pub struct WriteErrorDevice; + + impl Default for WriteErrorDevice { + fn default() -> Self { + WriteErrorDevice + } + } + + impl io::Read for WriteErrorDevice { + fn read(&mut self, _buf: &mut [u8]) -> io::Result { + Err(io::Error::new(io::ErrorKind::Other, "oh no!")) + } + } + + impl io::Write for WriteErrorDevice { + fn write(&mut self, _buf: &[u8]) -> io::Result { + Err(io::Error::new(io::ErrorKind::Other, "oh no!")) + } + + fn flush(&mut self) -> io::Result<()> { + Err(io::Error::new(io::ErrorKind::Other, "oh no!")) + } + } + + impl TestDevice for WriteErrorDevice {} +} + +mod fido2simple { + use std::cmp::min; + use std::io::{self, Read, Write}; + use std::mem; + + use byteorder::{BigEndian, ByteOrder}; + use pretty_hex::pretty_hex; + use rand::{thread_rng, RngCore}; + use serde_cbor::from_slice; + + use crate::consts::{CTAPHID_INIT, HID_RPT_SIZE}; + use crate::ctap2::attestation::{ + AAGuid, AttestationObject, AttestationStatement, AttestedCredentialData, AuthenticatorData, + AuthenticatorDataFlags, + }; + use crate::ctap2::commands::test::AUTHENTICATOR_INFO_PAYLOAD; + use crate::ctap2::commands::{Command, StatusCode}; + use crate::transport::hid::{Capability, HIDCmd}; + + use super::TestDevice; + use crate::transport::platform::commands::MakeCredentials; + use crate::transport::platform::crypto::{Error, PrivateKey}; + + #[derive(Debug)] + enum FidoStateInternal { + PendingReply { reply: Vec }, + Ready, + Working, + } + + #[derive(Debug)] + struct FidoState { + state: FidoStateInternal, + private_key: PrivateKey, + } + + impl FidoState { + fn new() -> Result { + let private_key = PrivateKey::generate()?; + Ok(FidoState { + state: FidoStateInternal::Ready, + private_key, + }) + } + + fn read(&mut self) -> io::Result> { + match mem::replace(&mut self.state, FidoStateInternal::Working) { + FidoStateInternal::PendingReply{reply} => { + mem::replace(&mut self.state, FidoStateInternal::Ready); + Ok(reply) + }, + FidoStateInternal::Ready{..} => Err(io::Error::new(io::ErrorKind::Other, "you forgot to send a command, but let me tell you a little story: Once upon a time, a fido2 device ...")), + FidoStateInternal::Working => Err(io::Error::new(io::ErrorKind::Other, "oh no! this should not happen, you have a bug in your logic :( (read(FidoStateInternal::Working))")) + } + } + + fn write_cbor(&mut self, data: &[u8]) -> io::Result { + trace!("reading cbor input: {}", pretty_hex(&data)); + let mut buff = io::Cursor::new(data); + + match mem::replace(&mut self.state, FidoStateInternal::Working) { + FidoStateInternal::Ready => { + let mut cbor_cmd = [0u8; 1]; + if buff.read(&mut cbor_cmd)? != 1 { + return Err(io::Error::new(io::ErrorKind::Other, "expected cbor cmd")); + } + let cbor_cmd = cbor_cmd[0]; + let len = buff.get_ref().len() - 1; + let mut args = Vec::with_capacity(len); + args.resize(len, 0); + if buff.read(args.as_mut_slice())? != len { + return Err(io::Error::new(io::ErrorKind::Other, "unable to read args")); + } + + let cbor_cmd = if let Some(cmd) = Command::from_u8(cbor_cmd) { + cmd + } else { + return Err(io::Error::new(io::ErrorKind::Other, format!("unknown cbor command: {:?}", cbor_cmd))); + }; + + trace!("got CBOR({:?}): {}", cbor_cmd, pretty_hex(&&args)); + match cbor_cmd { + Command::GetInfo => { + // Prepare reply + let mut reply = io::Cursor::new(Vec::new()); + reply.write_all(&[StatusCode::OK.into()])?; + reply.write_all(&AUTHENTICATOR_INFO_PAYLOAD[..])?; + + reply.set_position(0); + let reply = reply.into_inner(); + trace!("replying: {}", pretty_hex(&&reply[..])); + mem::replace(&mut self.state, FidoStateInternal::PendingReply{ + reply, + }); + }, + Command::MakeCredentials => { + let params: MakeCredentials = from_slice(&args[..]) + .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("parse error: {:?}", e)))?; + debug!("MakeCredentials: {:?}", params); + + let credential_data = AttestedCredentialData { + aaguid: AAGuid([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f]), + credential_id: vec![], + credential_public_key: self.private_key.public_key(), + }; + let auth_data = AuthenticatorData { + rp_id_hash: params.rp().hash(), + counter: 0, + flags: AuthenticatorDataFlags::empty(), + extensions: vec![], + credential_data: Some(credential_data), + }; + let att_statement = AttestationStatement::None; + let attestation_object = AttestationObject { + auth_data, + att_statement, + }; + + let mut reply = io::Cursor::new(Vec::new()); + reply.write_all(&[StatusCode::OK.into()])?; + let data = serde_cbor::to_vec(&attestation_object) + .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("error serializing: {:?}", e)))?; + reply.write_all(&data[..])?; + + reply.set_position(0); + let reply = reply.into_inner(); + trace!("replying: {}", pretty_hex(&&reply[..])); + mem::replace(&mut self.state, FidoStateInternal::PendingReply{ + reply, + }); + } + _ => { + } + } + + let buf = buff.into_inner(); + Ok(buf.len()) + }, + + FidoStateInternal::PendingReply{..} => Err(io::Error::new(io::ErrorKind::Other, "oh no! this should not happen, you have a bug in your logic :(, write(FidoStateInternal::PendingReply)")), + FidoStateInternal::Working => Err(io::Error::new(io::ErrorKind::Other, "oh no! this should not happen, you have a bug in your logic :( (Write(FidoStateInternal::Working)")), + } + } + } + + #[derive(Debug)] + enum HIDState { + Uninitialized, + PendingReply { + cid: [u8; 4], + next_cid: Option<[u8; 4]>, + hid_cmd: HIDCmd, + reply: io::Cursor>, + seq: u8, + fido_state: Option, + }, + PendingRequest { + cid: [u8; 4], + data_len: u16, + request: Vec, + seq: u8, + fido_state: FidoState, + }, + Ready { + cid: [u8; 4], + fido_state: FidoState, + }, + Working, + } + + #[derive(Debug)] + pub struct Fido2SimpleDevice { + state: HIDState, + } + + impl Default for Fido2SimpleDevice { + fn default() -> Self { + Fido2SimpleDevice { + state: HIDState::Uninitialized, + } + } + } + + const HID_HEADER_SIZE: usize = 4; + + impl io::Read for Fido2SimpleDevice { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let mut buff = io::Cursor::new(buf); + match mem::replace(&mut self.state, HIDState::Working) { + HIDState::PendingReply{cid, next_cid, hid_cmd, mut reply, mut seq, fido_state} => { + if buff.get_ref().len() < HID_HEADER_SIZE { + return Err(io::Error::new(io::ErrorKind::Other, "take a blue pill and enlarge your buffer")); + } + + let my_cid = next_cid.as_ref().unwrap_or(&cid); + buff.write_all(my_cid)?; + + if reply.position() == 0 { + // Init packet + buff.write_all(&[hid_cmd.into()])?; + + let len = reply.get_ref().len(); + let mut len_buf = [0u8; 2]; + BigEndian::write_u16(&mut len_buf, len as u16); + buff.write_all(&len_buf)?; + + let mut buffer = [0u8; HID_RPT_SIZE - HID_HEADER_SIZE - 1 - 2]; + reply.read(&mut buffer)?; + buff.write_all(&buffer[..])?; + } else { + // Cont packet + buff.write_all(&[seq])?; + seq = seq +1; + + let mut buffer = [0u8; HID_RPT_SIZE - HID_HEADER_SIZE - 1]; + reply.read(&mut buffer)?; + buff.write_all(&buffer[..])?; + } + + if (reply.position() as usize) < reply.get_ref().len() { + // we still have data in buffer, need a continuation packet + mem::replace(&mut self.state, HIDState::PendingReply { + cid, + next_cid, + hid_cmd, + reply, + seq, + fido_state, + }); + } else { + // Note(baloo): unwrap, in tests? not sure we care + let fido_state = fido_state.unwrap_or(FidoState::new().unwrap()); + mem::replace(&mut self.state, HIDState::Ready{cid, fido_state}); + } + + // Hackish but meh + Ok(HID_RPT_SIZE) + }, + HIDState::Ready{..} => Err(io::Error::new(io::ErrorKind::Other, "you forgot to send a command, but let me tell you a little story: Once upon a time, a fido2 device ...")), + HIDState::PendingRequest{..} => Err(io::Error::new(io::ErrorKind::Other, "you haven't finished your sentense")), + HIDState::Uninitialized => Err(io::Error::new(io::ErrorKind::Other, "do you really think I want to talk to you?")), + HIDState::Working => Err(io::Error::new(io::ErrorKind::Other, "oh no! this should not happen, you have a bug in your logic :( (read(HIDState::Working))")) + } + } + } + + impl io::Write for Fido2SimpleDevice { + fn write(&mut self, buf: &[u8]) -> io::Result { + trace!("reading input: {}", pretty_hex(&buf)); + let mut buff = io::Cursor::new(buf); + + // Unknown HID prefix + let mut unknown = [0u8; 1]; + if let Ok(l) = buff.read(&mut unknown) { + if l != 1 { + return Err(io::Error::new(io::ErrorKind::Other, "Expected HID prefix")); + } + } + + match mem::replace(&mut self.state, HIDState::Working) { + HIDState::Uninitialized => { + let mut broadcast = [0u8; 4]; + let _ = buff.read(&mut broadcast); + if broadcast != [255u8; 4] { + return Err(io::Error::new(io::ErrorKind::Other, "Expected hid broadcast")); + } + + let mut command = [0u8; 1]; + if buff.read(&mut command)? != 1 && command[0] != CTAPHID_INIT { + return Err(io::Error::new(io::ErrorKind::Other, "Expected hid init cmd")); + } + + let mut data_len = [0u8; 2]; + if buff.read(&mut data_len)? != 2 { + return Err(io::Error::new(io::ErrorKind::Other, "Expected hid data len")); + } + + let mut nonce = [0u8; 8]; + if buff.read(&mut nonce)? != 8 { + return Err(io::Error::new(io::ErrorKind::Other, "Expected hid nonce")); + } + + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + let read_buf = buff.into_inner(); + + // Prepare reply + let mut reply = io::Cursor::new(Vec::new()); + + reply.write_all(&nonce)?; + reply.write_all(&cid)?; + // u2fhid version + reply.write_all(&[42])?; // I (baloo) have no idea what this is used for + // device version + reply.write_all(b"\x00\x00\x2a")?; + // capabilities + reply.write_all(&[Capability::CBOR.bits()])?; + reply.set_position(0); + + let next_cid = Some([255u8; 4]); + let hid_cmd = HIDCmd::Init; + let seq = 0; + let fido_state = None; + mem::replace(&mut self.state, HIDState::PendingReply{ + cid, + next_cid, + hid_cmd, + reply, + seq, + fido_state, + }); + + Ok(read_buf.len()) + }, + + HIDState::Ready{cid, mut fido_state} => { + let mut command_cid = [0u8; 4]; + if 4 != buff.read(&mut command_cid)? || command_cid != cid { + return Err(io::Error::new(io::ErrorKind::Other, "unexpected cid")); + } + + let mut command = [0u8; 1]; + buff.read_exact(&mut command).map_err(|_| + io::Error::new(io::ErrorKind::Other, "expected hid cmd") + )?; + + match HIDCmd::from(command[0]) { + HIDCmd::Cbor => { + let mut data_len = [0u8; 2]; + buff.read_exact(&mut data_len).map_err(|_| + io::Error::new(io::ErrorKind::Other, "expected data_len") + )?; + + let data_len = BigEndian::read_u16(&data_len); + + let available = min(buff.get_ref().len() - (buff.position() as usize), data_len as usize); + let mut request = Vec::with_capacity(data_len as usize); + request.resize(available as usize, 0); + buff.read_exact(&mut request[..available])?; + + if (data_len as usize) > available { + let new_state = HIDState::PendingRequest{ + cid, + fido_state, + seq: 0, + request, + data_len, + }; + + mem::replace(&mut self.state, new_state); + } else { + fido_state.write_cbor(&request[..])?; + let reply = fido_state.read()?; + + let new_state = HIDState::PendingReply{ + cid, + next_cid: None, + hid_cmd: HIDCmd::Cbor, + seq: 0, + fido_state: Some(fido_state), + reply: io::Cursor::new(reply), + }; + + mem::replace(&mut self.state, new_state); + } + }, + _ => unimplemented!("command {:?} is not implemented", command), + } + + let buf = buff.into_inner(); + Ok(buf.len()) + }, + HIDState::PendingRequest {cid, data_len, mut request, mut seq, mut fido_state} => { + let mut command_cid = [0u8; 4]; + if 4 != buff.read(&mut command_cid)? || command_cid != cid { + return Err(io::Error::new(io::ErrorKind::Other, "unexpected cid")); + } + let mut seq_ = [0u8; 1]; + buff.read_exact(&mut seq_)?; + if seq_[0] != seq { + return Err(io::Error::new(io::ErrorKind::Other, "unexpected sequence")); + } + seq = seq + 1; + + let available = min(buff.get_ref().len() - (buff.position() as usize), (data_len as usize) - request.len()); + + let mut cont_buf = Vec::with_capacity(available); + cont_buf.resize(available, 0); + buff.read_exact(&mut cont_buf[..])?; + debug!("PendingRequest:\nbuf = {}\ncont = {}", pretty_hex(&&request[..]), pretty_hex(&&cont_buf[..])); + request.append(&mut cont_buf); + + if (data_len as usize) == request.len() { + fido_state.write_cbor(&request[..])?; + let reply = fido_state.read()?; + + let new_state = HIDState::PendingReply{ + cid, + next_cid: None, + hid_cmd: HIDCmd::Cbor, + seq: 0, + fido_state: Some(fido_state), + reply: io::Cursor::new(reply), + }; + + mem::replace(&mut self.state, new_state); + } else { + let new_state = HIDState::PendingRequest{ + cid, + fido_state, + seq, + request, + data_len, + }; + + mem::replace(&mut self.state, new_state); + } + let buf = buff.into_inner(); + Ok(buf.len()) + }, + HIDState::PendingReply{..} => Err(io::Error::new(io::ErrorKind::Other, "oh no! this should not happen, you have a bug in your logic :(, write(HIDState::PendingReply)")), + HIDState::Working => Err(io::Error::new(io::ErrorKind::Other, "oh no! this should not happen, you have a bug in your logic :( (Write(HIDState::Working)")), + } + } + + fn flush(&mut self) -> io::Result<()> { + // nothing? + Ok(()) + } + } + + impl TestDevice for Fido2SimpleDevice {} +} diff --git a/src/transport/test/mod.rs b/src/transport/test/mod.rs index 9b00e3f..b03de03 100644 --- a/src/transport/test/mod.rs +++ b/src/transport/test/mod.rs @@ -2,10 +2,40 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +pub mod commands; +pub mod crypto; pub mod device; pub mod transaction; +use std::cell::RefCell; + #[derive(Debug, Eq, PartialEq, Copy, Clone)] pub enum TestCase { + WriteError, Fido2Simple, } + +impl TestCase { + pub fn activate(value: TestCase) { + // ENABLED_TEST will return older value in error side of a result, just + // ignore it. + debug!("enabling test_case={:?} in {:?}", value, std::thread::current().id()); + ENABLED_TEST.with(|v| v.replace(value)); + } + + pub fn active() -> TestCase { + let value = ENABLED_TEST.with(|v| v.clone().into_inner()); + debug!("enabling test_case={:?} in {:?}", value, std::thread::current().id()); + value + } +} + +impl Default for TestCase { + fn default() -> Self { + TestCase::Fido2Simple + } +} + +thread_local! { + static ENABLED_TEST: RefCell = RefCell::new(TestCase::default()); +} diff --git a/src/transport/test/transaction.rs b/src/transport/test/transaction.rs index 1a158f0..e8cd6a3 100644 --- a/src/transport/test/transaction.rs +++ b/src/transport/test/transaction.rs @@ -24,7 +24,8 @@ impl Transaction { F: Fn(TestCase, &Fn() -> bool) + Sync + Send + 'static, T: 'static, { - new_device_cb(TestCase::Fido2Simple, &always_alive); + let test_case = TestCase::active(); + new_device_cb(test_case, &always_alive); Ok(Self {}) } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 04697fe..8b13789 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,7 +1 @@ -extern crate simple_logger; -use simple_logger::init; - -pub fn setup() { - init().unwrap(); -} diff --git a/tests/fido2.rs b/tests/fido2.rs deleted file mode 100644 index 3aa8456..0000000 --- a/tests/fido2.rs +++ /dev/null @@ -1,9 +0,0 @@ -extern crate simple_logger; - -mod common; - -#[test] -fn it_adds_two() { - common::setup(); - assert_eq!(4, 2 + 2); -}