integration test: basic idea how to mock devices

Signed-off-by: Arthur Gautier <baloo@gandi.net>
This commit is contained in:
Arthur Gautier 2019-03-29 05:00:25 +00:00
Родитель 65a3850b37
Коммит 101739f236
17 изменённых файлов: 1226 добавлений и 52 удалений

Просмотреть файл

@ -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"

Просмотреть файл

@ -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<u8>) -> 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()
}
}

Просмотреть файл

@ -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<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, v: &[u8]) -> Result<Self::Value, E>
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<ClientDataHash> {
// TODO(baloo): this could use a bit more spec, there is no ordering specified. Are spaces

Просмотреть файл

@ -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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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::<BigEndian>(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<u8>);
@ -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<AttestationFormat> = 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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;

Просмотреть файл

@ -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<Command> {
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<u8> for StatusCode {
}
}
#[cfg(test)]
impl Into<u8> 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() {

Просмотреть файл

@ -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<Vec<u8>, 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<u8>,
@ -89,12 +111,12 @@ impl<'de> Deserialize<'de> for Alg {
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
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<D>(deserializer: D) -> Result<Self, D::Error>
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<M>(self, mut map: M) -> Result<Self::Value, M::Error>
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<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, v: &str) -> Result<Self::Value, E>
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<u8>,
@ -161,6 +278,77 @@ impl Serialize for PublicKeyCredentialDescriptor {
}
}
#[cfg(test)]
impl<'de> Deserialize<'de> for PublicKeyCredentialDescriptor {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<M>(self, mut map: M) -> Result<Self::Value, M::Error>
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;

Просмотреть файл

@ -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;

Просмотреть файл

@ -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<Self> {
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() {

7
src/tests/common.rs Normal file
Просмотреть файл

@ -0,0 +1,7 @@
extern crate simple_logger;
use simple_logger::init;
pub fn setup() {
let _ = init();
}

81
src/tests/mod.rs Normal file
Просмотреть файл

@ -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);
}

Просмотреть файл

@ -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<PublicKeyCredentialParameters>,
exclude_list: Vec<PublicKeyCredentialDescriptor>,
extensions: BTreeMap<String, Value>,
options: Option<MakeCredentialsOptions>,
pin_auth: Option<PinAuth>,
pin_protocol: Option<u8>,
}
impl MakeCredentials {
pub fn rp(&self) -> &RelyingParty {
&self.rp
}
}
impl<'de> Deserialize<'de> for MakeCredentials {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<M>(self, mut map: M) -> Result<Self::Value, M::Error>
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)
}
}

Просмотреть файл

@ -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<Self, Error> {
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()))
}
}

Просмотреть файл

@ -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<TestDevice>,
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<usize> {
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<usize> {
Ok(0)
impl io::Write for Device {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
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<Self, Error> {
debug!("test_case={:?}", test_case);
let inner: Box<TestDevice> = 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<usize> {
Err(io::Error::new(io::ErrorKind::Other, "oh no!"))
}
}
impl io::Write for WriteErrorDevice {
fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
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<u8> },
Ready,
Working,
}
#[derive(Debug)]
struct FidoState {
state: FidoStateInternal,
private_key: PrivateKey,
}
impl FidoState {
fn new() -> Result<Self, Error> {
let private_key = PrivateKey::generate()?;
Ok(FidoState {
state: FidoStateInternal::Ready,
private_key,
})
}
fn read(&mut self) -> io::Result<Vec<u8>> {
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<usize> {
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<Vec<u8>>,
seq: u8,
fido_state: Option<FidoState>,
},
PendingRequest {
cid: [u8; 4],
data_len: u16,
request: Vec<u8>,
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<usize> {
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<usize> {
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 {}
}

Просмотреть файл

@ -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<TestCase> = RefCell::new(TestCase::default());
}

Просмотреть файл

@ -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 {})
}

Просмотреть файл

@ -1,7 +1 @@
extern crate simple_logger;
use simple_logger::init;
pub fn setup() {
init().unwrap();
}

Просмотреть файл

@ -1,9 +0,0 @@
extern crate simple_logger;
mod common;
#[test]
fn it_adds_two() {
common::setup();
assert_eq!(4, 2 + 2);
}