integration test: basic idea how to mock devices
Signed-off-by: Arthur Gautier <baloo@gandi.net>
This commit is contained in:
Родитель
65a3850b37
Коммит
101739f236
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
34
src/ctap.rs
34
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<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;
|
||||
|
|
11
src/lib.rs
11
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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
extern crate simple_logger;
|
||||
|
||||
use simple_logger::init;
|
||||
|
||||
pub fn setup() {
|
||||
let _ = init();
|
||||
}
|
|
@ -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);
|
||||
}
|
Загрузка…
Ссылка в новой задаче