From bfe6fb4e2d454175e27512e82d6cae9560535c65 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus <> Date: Tue, 11 May 2021 16:54:40 +0200 Subject: [PATCH 01/15] WIP: Add part MakeCredentials --- Cargo.toml | 11 +- src/ctap2/attestation.rs | 597 +++++++++++++++++++++++++ src/ctap2/client_data.rs | 259 +++++++++++ src/ctap2/commands/make_credentials.rs | 584 ++++++++++++++++++++++++ src/ctap2/commands/mod.rs | 15 +- src/ctap2/mod.rs | 5 + src/ctap2/server.rs | 549 +++++++++++++++++++++++ src/ctap2/utils.rs | 14 + 8 files changed, 2028 insertions(+), 6 deletions(-) create mode 100644 src/ctap2/attestation.rs create mode 100644 src/ctap2/client_data.rs create mode 100644 src/ctap2/commands/make_credentials.rs create mode 100644 src/ctap2/server.rs create mode 100644 src/ctap2/utils.rs diff --git a/Cargo.toml b/Cargo.toml index bd17571a..019acb22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ maintenance = { status = "actively-developed" } [features] binding-recompile = ["bindgen"] -webdriver = ["base64", "bytes", "warp", "tokio", "serde_json"] +webdriver = ["bytes", "warp", "tokio"] [target.'cfg(target_os = "linux")'.dependencies] libudev = "^0.2" @@ -48,14 +48,17 @@ bitflags = "1.0" tokio = { version = "0.2", optional = true, features = ["macros"] } warp = { version = "0.2.4", optional = true } serde = { version = "1.0", features = ["derive"] } +serde_bytes = "0.11" serde_cbor = "0.11" -serde_json = { version = "1.0", optional = true } +serde_json = "1.0" bytes = { version = "0.5", optional = true, features = ["serde"] } -base64 = { version = "^0.10", optional = true } +base64 = "^0.10" +nom = "^5.1" +sha2 = "^0.8.0" [dev-dependencies] -sha2 = "^0.8.2" base64 = "^0.10" env_logger = "^0.6" getopts = "^0.2" assert_matches = "1.2" +byteorder = "^1.3.2" diff --git a/src/ctap2/attestation.rs b/src/ctap2/attestation.rs new file mode 100644 index 00000000..814935c1 --- /dev/null +++ b/src/ctap2/attestation.rs @@ -0,0 +1,597 @@ +use crate::ctap2::server::RpIdHash; +use std::fmt; +#[cfg(test)] +use std::io::{self, Write}; + +#[cfg(test)] +use byteorder::{BigEndian, WriteBytesExt}; +use nom::{ + cond, do_parse, map, map_res, named, + number::complete::{be_u16, be_u32, be_u8}, + take, Err as NomErr, IResult, +}; +use serde::{ + de::{Error as SerdeError, MapAccess, Visitor}, + Deserialize, Deserializer, Serialize, +}; +#[cfg(test)] +use serde::{ + ser::{Error as SerError, SerializeMap}, + Serializer, +}; + +use serde_bytes::ByteBuf; + +use super::server::Alg; +use super::utils::from_slice_stream; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct Extension {} + +named!( + parse_extensions>, + do_parse!(extensions: serde_to_nom >> (extensions)) +); + +#[derive(Serialize, PartialEq, Default, Eq, Clone)] +pub struct AAGuid(pub [u8; 16]); + +impl AAGuid { + pub fn from(src: &[u8]) -> Result { + let mut payload = [0u8; 16]; + if src.len() != payload.len() { + Err(()) + } else { + payload.copy_from_slice(src); + Ok(AAGuid(payload)) + } + } +} + +impl fmt::Debug for AAGuid { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "AAGuid({:x}{:x}{:x}{:x}-{:x}{:x}-{:x}{:x}-{:x}{:x}-{:x}{:x}{:x}{:x}{:x}{:x})", + self.0[0], + self.0[1], + self.0[2], + self.0[3], + self.0[4], + self.0[5], + self.0[6], + self.0[7], + self.0[8], + self.0[9], + self.0[10], + self.0[11], + self.0[12], + self.0[13], + self.0[14], + self.0[15] + ) + } +} + +impl<'de> Deserialize<'de> for AAGuid { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct AAGuidVisitor; + + impl<'de> Visitor<'de> for AAGuidVisitor { + type Value = AAGuid; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a byte array") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: SerdeError, + { + if v.len() != 16 { + return Err(E::custom("expecting 16 bytes data")); + } + + let mut buf = [0u8; 16]; + + buf.copy_from_slice(v); + + Ok(AAGuid(buf)) + } + } + + deserializer.deserialize_bytes(AAGuidVisitor) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct AttestedCredentialData { + pub aaguid: AAGuid, + pub credential_id: Vec, + // TODO(MS): unimplemented! + // pub credential_public_key: crate::ctap2::commands::client_pin::PublicKey, +} + +fn serde_to_nom<'a, Output>(input: &'a [u8]) -> IResult<&'a [u8], Output> +where + Output: Deserialize<'a>, +{ + from_slice_stream(input) + .map_err(|_e| nom::Err::Error(nom::error::make_error(input, nom::error::ErrorKind::NoneOf))) + // can't use custom errorkind because of error type mismatch in parse_attested_cred_data + //.map_err(|e| NomErr::Error(Context::Code(input, ErrorKind::Custom(e)))) + // .map_err(|_| NomErr::Error(Context::Code(input, ErrorKind::Custom(42)))) +} + +named!( + parse_attested_cred_data, + do_parse!( + aaguid: map_res!(take!(16), AAGuid::from) + >> cred_len: be_u16 + >> credential_id: map!(take!(cred_len), Vec::from) + // >> credential_public_key: serde_to_nom + >> (AttestedCredentialData { + aaguid, + credential_id, + // credential_public_key, + }) + ) +); + +bitflags! { + pub struct AuthenticatorDataFlags: u8 { + const USER_PRESENT = 0x01; + const USER_VERIFIED = 0x04; + const ATTESTED = 0x40; + const EXTENSION_DATA = 0x80; + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct AuthenticatorData { + pub rp_id_hash: RpIdHash, + pub flags: AuthenticatorDataFlags, + pub counter: u32, + pub credential_data: Option, + pub extensions: Vec, +} + +named!( + parse_ad, + do_parse!( + rp_id_hash: map_res!(take!(32), RpIdHash::from) >> + // be conservative, there is a couple of reserved values in + // AuthenticatorDataFlags, just truncate the one we don't know + flags: map!(be_u8, AuthenticatorDataFlags::from_bits_truncate) >> + counter: be_u32 >> + credential_data: cond!(flags.contains(AuthenticatorDataFlags::ATTESTED), parse_attested_cred_data) >> + extensions: cond!(flags.contains(AuthenticatorDataFlags::EXTENSION_DATA), parse_extensions) >> + // TODO(baloo): we should check for end of buffer and raise a parse + // parse error if data is still in the buffer + //eof!() >> + (AuthenticatorData{ + rp_id_hash, + flags, + counter, + credential_data, + extensions: extensions.unwrap_or_default(), + }) + ) +); + +impl<'de> Deserialize<'de> for AuthenticatorData { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct AuthenticatorDataVisitor; + + impl<'de> Visitor<'de> for AuthenticatorDataVisitor { + type Value = AuthenticatorData; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a byte array") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: SerdeError, + { + parse_ad(v) + .map(|(_input, value)| value) + .map_err(|e| match e { + NomErr::Incomplete(len) => E::custom(format!("need {:?} more bytes", len)), + // TODO(baloo): is that enough? should we be more + // specific on the error type? + e => E::custom(e.to_string()), + }) + } + } + + deserializer.deserialize_bytes(AuthenticatorDataVisitor) + } +} + +#[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, Deserialize, PartialEq, Eq)] +/// x509 encoded attestation certificate +pub struct AttestationCertificate(#[serde(with = "serde_bytes")] pub(crate) Vec); + +impl AsRef<[u8]> for AttestationCertificate { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +#[derive(Serialize, Deserialize, PartialEq, Eq)] +pub struct Signature(pub(crate) ByteBuf); + +impl fmt::Debug for Signature { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let value = base64::encode_config(&self.0, base64::URL_SAFE_NO_PAD); + write!(f, "Signature({})", value) + } +} + +impl AsRef<[u8]> for Signature { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +#[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(AttestationStatementFidoU2F), + // TODO(baloo): should we do an Unknow version? most likely +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +// See https://www.w3.org/TR/webauthn/#fido-u2f-attestation +pub struct AttestationStatementFidoU2F { + pub sig: Signature, + + #[serde(rename = "x5c")] + /// Certificate chain in x509 format + pub attestation_cert: Vec, +} + +impl AttestationStatementFidoU2F { + pub fn new(cert: &[u8], signature: &[u8]) -> Self { + AttestationStatementFidoU2F { + sig: Signature(ByteBuf::from(signature)), + attestation_cert: vec![AttestationCertificate(Vec::from(cert))], + } + } +} + +#[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 AttestationStatementPacked { + pub alg: Alg, + pub sig: Signature, + + #[serde(rename = "x5c")] + /// Certificate chain in x509 format + pub attestation_cert: Vec, +} + +#[derive(Debug, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +enum AttestationFormat { + #[serde(rename = "fido-u2f")] + FidoU2F, + Packed, + None, + // TOOD(baloo): only packed is implemented for now, see spec: + // https://www.w3.org/TR/webauthn/#defined-attestation-formats + //TPM, + //AndroidKey, + //AndroidSafetyNet, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct AttestationObject { + pub auth_data: AuthenticatorData, + pub att_statement: AttestationStatement, +} + +impl<'de> Deserialize<'de> for AttestationObject { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct AttestationObjectVisitor; + + impl<'de> Visitor<'de> for AttestationObjectVisitor { + type Value = AttestationObject; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a cbor map") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'de>, + { + let mut format: Option = None; + let mut auth_data = None; + let mut att_statement = None; + + while let Some(key) = map.next_key()? { + match key { + // Spec for CTAP 2.0 is wrong and fmt should be numbered 1, and auth_data 2: + // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorMakeCredential + // Corrected in CTAP 2.1 and Webauthn spec + 1 => { + if format.is_some() { + return Err(SerdeError::duplicate_field("fmt")); + } + format = Some(map.next_value()?); + } + 2 => { + if auth_data.is_some() { + return Err(SerdeError::duplicate_field("auth_data")); + } + auth_data = Some(map.next_value()?); + } + 3 => { + let format = format + .as_ref() + .ok_or_else(|| SerdeError::missing_field("fmt"))?; + if att_statement.is_some() { + return Err(SerdeError::duplicate_field("att_statement")); + } + 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()?)); + } + AttestationFormat::FidoU2F => { + att_statement = + Some(AttestationStatement::FidoU2F(map.next_value()?)); + } + } + } + k => return Err(M::Error::custom(format!("unexpected key: {:?}", k))), + } + } + + let auth_data = + auth_data.ok_or_else(|| M::Error::custom("found no auth_data".to_string()))?; + let att_statement = att_statement.unwrap_or(AttestationStatement::None); + + Ok(AttestationObject { + auth_data, + att_statement, + }) + } + } + + deserializer.deserialize_bytes(AttestationObjectVisitor) + } +} + +#[cfg(test)] +impl Serialize for AttestationObject { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map_len = 2; + if self.att_statement != AttestationStatement::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)?; + } + AttestationStatement::FidoU2F(_) => { + unimplemented!(); + } + } + + map.end() + } +} + +#[cfg(test)] +mod test { + use super::super::utils::from_slice_stream; + use super::AttestationCertificate; + use super::AttestationObject; + use serde_cbor::from_slice; + + const SAMPLE_ATTESTATION: [u8; 1006] = [ + 0xa3, 0x1, 0x66, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x64, 0x2, 0x58, 0xc4, 0x49, 0x96, 0xd, + 0xe5, 0x88, 0xe, 0x8c, 0x68, 0x74, 0x34, 0x17, 0xf, 0x64, 0x76, 0x60, 0x5b, 0x8f, 0xe4, + 0xae, 0xb9, 0xa2, 0x86, 0x32, 0xc7, 0x99, 0x5c, 0xf3, 0xba, 0x83, 0x1d, 0x97, 0x63, 0x41, + 0x0, 0x0, 0x0, 0x7, 0xcb, 0x69, 0x48, 0x1e, 0x8f, 0xf7, 0x40, 0x39, 0x93, 0xec, 0xa, 0x27, + 0x29, 0xa1, 0x54, 0xa8, 0x0, 0x40, 0xc3, 0xcf, 0x1, 0x3b, 0xc6, 0x26, 0x93, 0x28, 0xfb, + 0x7f, 0xa9, 0x76, 0xef, 0xa8, 0x4b, 0x66, 0x71, 0xad, 0xa9, 0x64, 0xea, 0xcb, 0x58, 0x76, + 0x54, 0x51, 0xa, 0xc8, 0x86, 0x4f, 0xbb, 0x53, 0x2d, 0xfb, 0x2, 0xfc, 0xdc, 0xa9, 0x84, + 0xc2, 0x5c, 0x67, 0x8a, 0x3a, 0xab, 0x57, 0xf3, 0x71, 0x77, 0xd3, 0xd4, 0x41, 0x64, 0x1, + 0x50, 0xca, 0x6c, 0x42, 0x73, 0x1c, 0x42, 0xcb, 0x81, 0xba, 0xa5, 0x1, 0x2, 0x3, 0x26, + 0x20, 0x1, 0x21, 0x58, 0x20, 0x9, 0x2e, 0x34, 0xfe, 0xa7, 0xd7, 0x32, 0xc8, 0xae, 0x4c, + 0xf6, 0x96, 0xbe, 0x7a, 0x12, 0xdc, 0x29, 0xd5, 0xf1, 0xd3, 0xf1, 0x55, 0x4d, 0xdc, 0x87, + 0xc4, 0xc, 0x9b, 0xd0, 0x17, 0xba, 0xf, 0x22, 0x58, 0x20, 0xc9, 0xf0, 0x97, 0x33, 0x55, + 0x36, 0x58, 0xd9, 0xdb, 0x76, 0xf5, 0xef, 0x95, 0xcf, 0x8a, 0xc7, 0xfc, 0xc1, 0xb6, 0x81, + 0x25, 0x5f, 0x94, 0x6b, 0x62, 0x13, 0x7d, 0xd0, 0xc4, 0x86, 0x53, 0xdb, 0x3, 0xa3, 0x63, + 0x61, 0x6c, 0x67, 0x26, 0x63, 0x73, 0x69, 0x67, 0x58, 0x48, 0x30, 0x46, 0x2, 0x21, 0x0, + 0xac, 0x2a, 0x78, 0xa8, 0xaf, 0x18, 0x80, 0x39, 0x73, 0x8d, 0x3, 0x5e, 0x4, 0x4d, 0x94, + 0x4f, 0x3f, 0x57, 0xce, 0x88, 0x41, 0xfa, 0x81, 0x50, 0x40, 0xb6, 0xd1, 0x95, 0xb5, 0xeb, + 0xe4, 0x6f, 0x2, 0x21, 0x0, 0x8f, 0xf4, 0x15, 0xc9, 0xb3, 0x6d, 0x1c, 0xd, 0x4c, 0xa3, + 0xcf, 0x99, 0x8a, 0x46, 0xd4, 0x4c, 0x8b, 0x5c, 0x26, 0x3f, 0xdf, 0x22, 0x6c, 0x9b, 0x23, + 0x83, 0x8b, 0x69, 0x47, 0x67, 0x48, 0x45, 0x63, 0x78, 0x35, 0x63, 0x81, 0x59, 0x2, 0xc1, + 0x30, 0x82, 0x2, 0xbd, 0x30, 0x82, 0x1, 0xa5, 0xa0, 0x3, 0x2, 0x1, 0x2, 0x2, 0x4, 0x18, + 0xac, 0x46, 0xc0, 0x30, 0xd, 0x6, 0x9, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0xd, 0x1, 0x1, 0xb, + 0x5, 0x0, 0x30, 0x2e, 0x31, 0x2c, 0x30, 0x2a, 0x6, 0x3, 0x55, 0x4, 0x3, 0x13, 0x23, 0x59, + 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x55, 0x32, 0x46, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, + 0x43, 0x41, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x20, 0x34, 0x35, 0x37, 0x32, 0x30, + 0x30, 0x36, 0x33, 0x31, 0x30, 0x20, 0x17, 0xd, 0x31, 0x34, 0x30, 0x38, 0x30, 0x31, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x18, 0xf, 0x32, 0x30, 0x35, 0x30, 0x30, 0x39, 0x30, + 0x34, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x30, 0x6e, 0x31, 0xb, 0x30, 0x9, 0x6, 0x3, + 0x55, 0x4, 0x6, 0x13, 0x2, 0x53, 0x45, 0x31, 0x12, 0x30, 0x10, 0x6, 0x3, 0x55, 0x4, 0xa, + 0xc, 0x9, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x41, 0x42, 0x31, 0x22, 0x30, 0x20, + 0x6, 0x3, 0x55, 0x4, 0xb, 0xc, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, + 0x61, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x31, 0x27, 0x30, 0x25, 0x6, 0x3, 0x55, 0x4, 0x3, 0xc, 0x1e, 0x59, 0x75, 0x62, 0x69, + 0x63, 0x6f, 0x20, 0x55, 0x32, 0x46, 0x20, 0x45, 0x45, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, + 0x6c, 0x20, 0x34, 0x31, 0x33, 0x39, 0x34, 0x33, 0x34, 0x38, 0x38, 0x30, 0x59, 0x30, 0x13, + 0x6, 0x7, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x2, 0x1, 0x6, 0x8, 0x2a, 0x86, 0x48, 0xce, 0x3d, + 0x3, 0x1, 0x7, 0x3, 0x42, 0x0, 0x4, 0x79, 0xea, 0x3b, 0x2c, 0x7c, 0x49, 0x70, 0x10, 0x62, + 0x23, 0xc, 0xd2, 0x3f, 0xeb, 0x60, 0xe5, 0x29, 0x31, 0x71, 0xd4, 0x83, 0xf1, 0x0, 0xbe, + 0x85, 0x9d, 0x6b, 0xf, 0x83, 0x97, 0x3, 0x1, 0xb5, 0x46, 0xcd, 0xd4, 0x6e, 0xcf, 0xca, + 0xe3, 0xe3, 0xf3, 0xf, 0x81, 0xe9, 0xed, 0x62, 0xbd, 0x26, 0x8d, 0x4c, 0x1e, 0xbd, 0x37, + 0xb3, 0xbc, 0xbe, 0x92, 0xa8, 0xc2, 0xae, 0xeb, 0x4e, 0x3a, 0xa3, 0x6c, 0x30, 0x6a, 0x30, + 0x22, 0x6, 0x9, 0x2b, 0x6, 0x1, 0x4, 0x1, 0x82, 0xc4, 0xa, 0x2, 0x4, 0x15, 0x31, 0x2e, + 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x31, 0x34, 0x38, 0x32, + 0x2e, 0x31, 0x2e, 0x37, 0x30, 0x13, 0x6, 0xb, 0x2b, 0x6, 0x1, 0x4, 0x1, 0x82, 0xe5, 0x1c, + 0x2, 0x1, 0x1, 0x4, 0x4, 0x3, 0x2, 0x5, 0x20, 0x30, 0x21, 0x6, 0xb, 0x2b, 0x6, 0x1, 0x4, + 0x1, 0x82, 0xe5, 0x1c, 0x1, 0x1, 0x4, 0x4, 0x12, 0x4, 0x10, 0xcb, 0x69, 0x48, 0x1e, 0x8f, + 0xf7, 0x40, 0x39, 0x93, 0xec, 0xa, 0x27, 0x29, 0xa1, 0x54, 0xa8, 0x30, 0xc, 0x6, 0x3, 0x55, + 0x1d, 0x13, 0x1, 0x1, 0xff, 0x4, 0x2, 0x30, 0x0, 0x30, 0xd, 0x6, 0x9, 0x2a, 0x86, 0x48, + 0x86, 0xf7, 0xd, 0x1, 0x1, 0xb, 0x5, 0x0, 0x3, 0x82, 0x1, 0x1, 0x0, 0x97, 0x9d, 0x3, 0x97, + 0xd8, 0x60, 0xf8, 0x2e, 0xe1, 0x5d, 0x31, 0x1c, 0x79, 0x6e, 0xba, 0xfb, 0x22, 0xfa, 0xa7, + 0xe0, 0x84, 0xd9, 0xba, 0xb4, 0xc6, 0x1b, 0xbb, 0x57, 0xf3, 0xe6, 0xb4, 0xc1, 0x8a, 0x48, + 0x37, 0xb8, 0x5c, 0x3c, 0x4e, 0xdb, 0xe4, 0x83, 0x43, 0xf4, 0xd6, 0xa5, 0xd9, 0xb1, 0xce, + 0xda, 0x8a, 0xe1, 0xfe, 0xd4, 0x91, 0x29, 0x21, 0x73, 0x5, 0x8e, 0x5e, 0xe1, 0xcb, 0xdd, + 0x6b, 0xda, 0xc0, 0x75, 0x57, 0xc6, 0xa0, 0xe8, 0xd3, 0x68, 0x25, 0xba, 0x15, 0x9e, 0x7f, + 0xb5, 0xad, 0x8c, 0xda, 0xf8, 0x4, 0x86, 0x8c, 0xf9, 0xe, 0x8f, 0x1f, 0x8a, 0xea, 0x17, + 0xc0, 0x16, 0xb5, 0x5c, 0x2a, 0x7a, 0xd4, 0x97, 0xc8, 0x94, 0xfb, 0x71, 0xd7, 0x53, 0xd7, + 0x9b, 0x9a, 0x48, 0x4b, 0x6c, 0x37, 0x6d, 0x72, 0x3b, 0x99, 0x8d, 0x2e, 0x1d, 0x43, 0x6, + 0xbf, 0x10, 0x33, 0xb5, 0xae, 0xf8, 0xcc, 0xa5, 0xcb, 0xb2, 0x56, 0x8b, 0x69, 0x24, 0x22, + 0x6d, 0x22, 0xa3, 0x58, 0xab, 0x7d, 0x87, 0xe4, 0xac, 0x5f, 0x2e, 0x9, 0x1a, 0xa7, 0x15, + 0x79, 0xf3, 0xa5, 0x69, 0x9, 0x49, 0x7d, 0x72, 0xf5, 0x4e, 0x6, 0xba, 0xc1, 0xc3, 0xb4, + 0x41, 0x3b, 0xba, 0x5e, 0xaf, 0x94, 0xc3, 0xb6, 0x4f, 0x34, 0xf9, 0xeb, 0xa4, 0x1a, 0xcb, + 0x6a, 0xe2, 0x83, 0x77, 0x6d, 0x36, 0x46, 0x53, 0x78, 0x48, 0xfe, 0xe8, 0x84, 0xbd, 0xdd, + 0xf5, 0xb1, 0xba, 0x57, 0x98, 0x54, 0xcf, 0xfd, 0xce, 0xba, 0xc3, 0x44, 0x5, 0x95, 0x27, + 0xe5, 0x6d, 0xd5, 0x98, 0xf8, 0xf5, 0x66, 0x71, 0x5a, 0xbe, 0x43, 0x1, 0xdd, 0x19, 0x11, + 0x30, 0xe6, 0xb9, 0xf0, 0xc6, 0x40, 0x39, 0x12, 0x53, 0xe2, 0x29, 0x80, 0x3f, 0x3a, 0xef, + 0x27, 0x4b, 0xed, 0xbf, 0xde, 0x3f, 0xcb, 0xbd, 0x42, 0xea, 0xd6, 0x79, + ]; + + const SAMPLE_CERT_CHAIN: [u8; 709] = [ + 0x81, 0x59, 0x2, 0xc1, 0x30, 0x82, 0x2, 0xbd, 0x30, 0x82, 0x1, 0xa5, 0xa0, 0x3, 0x2, 0x1, + 0x2, 0x2, 0x4, 0x18, 0xac, 0x46, 0xc0, 0x30, 0xd, 0x6, 0x9, 0x2a, 0x86, 0x48, 0x86, 0xf7, + 0xd, 0x1, 0x1, 0xb, 0x5, 0x0, 0x30, 0x2e, 0x31, 0x2c, 0x30, 0x2a, 0x6, 0x3, 0x55, 0x4, 0x3, + 0x13, 0x23, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x55, 0x32, 0x46, 0x20, 0x52, 0x6f, + 0x6f, 0x74, 0x20, 0x43, 0x41, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x20, 0x34, 0x35, + 0x37, 0x32, 0x30, 0x30, 0x36, 0x33, 0x31, 0x30, 0x20, 0x17, 0xd, 0x31, 0x34, 0x30, 0x38, + 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x18, 0xf, 0x32, 0x30, 0x35, 0x30, + 0x30, 0x39, 0x30, 0x34, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x30, 0x6e, 0x31, 0xb, + 0x30, 0x9, 0x6, 0x3, 0x55, 0x4, 0x6, 0x13, 0x2, 0x53, 0x45, 0x31, 0x12, 0x30, 0x10, 0x6, + 0x3, 0x55, 0x4, 0xa, 0xc, 0x9, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x41, 0x42, 0x31, + 0x22, 0x30, 0x20, 0x6, 0x3, 0x55, 0x4, 0xb, 0xc, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, + 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x31, 0x27, 0x30, 0x25, 0x6, 0x3, 0x55, 0x4, 0x3, 0xc, 0x1e, 0x59, + 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x55, 0x32, 0x46, 0x20, 0x45, 0x45, 0x20, 0x53, 0x65, + 0x72, 0x69, 0x61, 0x6c, 0x20, 0x34, 0x31, 0x33, 0x39, 0x34, 0x33, 0x34, 0x38, 0x38, 0x30, + 0x59, 0x30, 0x13, 0x6, 0x7, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x2, 0x1, 0x6, 0x8, 0x2a, 0x86, + 0x48, 0xce, 0x3d, 0x3, 0x1, 0x7, 0x3, 0x42, 0x0, 0x4, 0x79, 0xea, 0x3b, 0x2c, 0x7c, 0x49, + 0x70, 0x10, 0x62, 0x23, 0xc, 0xd2, 0x3f, 0xeb, 0x60, 0xe5, 0x29, 0x31, 0x71, 0xd4, 0x83, + 0xf1, 0x0, 0xbe, 0x85, 0x9d, 0x6b, 0xf, 0x83, 0x97, 0x3, 0x1, 0xb5, 0x46, 0xcd, 0xd4, 0x6e, + 0xcf, 0xca, 0xe3, 0xe3, 0xf3, 0xf, 0x81, 0xe9, 0xed, 0x62, 0xbd, 0x26, 0x8d, 0x4c, 0x1e, + 0xbd, 0x37, 0xb3, 0xbc, 0xbe, 0x92, 0xa8, 0xc2, 0xae, 0xeb, 0x4e, 0x3a, 0xa3, 0x6c, 0x30, + 0x6a, 0x30, 0x22, 0x6, 0x9, 0x2b, 0x6, 0x1, 0x4, 0x1, 0x82, 0xc4, 0xa, 0x2, 0x4, 0x15, + 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x31, 0x34, + 0x38, 0x32, 0x2e, 0x31, 0x2e, 0x37, 0x30, 0x13, 0x6, 0xb, 0x2b, 0x6, 0x1, 0x4, 0x1, 0x82, + 0xe5, 0x1c, 0x2, 0x1, 0x1, 0x4, 0x4, 0x3, 0x2, 0x5, 0x20, 0x30, 0x21, 0x6, 0xb, 0x2b, 0x6, + 0x1, 0x4, 0x1, 0x82, 0xe5, 0x1c, 0x1, 0x1, 0x4, 0x4, 0x12, 0x4, 0x10, 0xcb, 0x69, 0x48, + 0x1e, 0x8f, 0xf7, 0x40, 0x39, 0x93, 0xec, 0xa, 0x27, 0x29, 0xa1, 0x54, 0xa8, 0x30, 0xc, + 0x6, 0x3, 0x55, 0x1d, 0x13, 0x1, 0x1, 0xff, 0x4, 0x2, 0x30, 0x0, 0x30, 0xd, 0x6, 0x9, 0x2a, + 0x86, 0x48, 0x86, 0xf7, 0xd, 0x1, 0x1, 0xb, 0x5, 0x0, 0x3, 0x82, 0x1, 0x1, 0x0, 0x97, 0x9d, + 0x3, 0x97, 0xd8, 0x60, 0xf8, 0x2e, 0xe1, 0x5d, 0x31, 0x1c, 0x79, 0x6e, 0xba, 0xfb, 0x22, + 0xfa, 0xa7, 0xe0, 0x84, 0xd9, 0xba, 0xb4, 0xc6, 0x1b, 0xbb, 0x57, 0xf3, 0xe6, 0xb4, 0xc1, + 0x8a, 0x48, 0x37, 0xb8, 0x5c, 0x3c, 0x4e, 0xdb, 0xe4, 0x83, 0x43, 0xf4, 0xd6, 0xa5, 0xd9, + 0xb1, 0xce, 0xda, 0x8a, 0xe1, 0xfe, 0xd4, 0x91, 0x29, 0x21, 0x73, 0x5, 0x8e, 0x5e, 0xe1, + 0xcb, 0xdd, 0x6b, 0xda, 0xc0, 0x75, 0x57, 0xc6, 0xa0, 0xe8, 0xd3, 0x68, 0x25, 0xba, 0x15, + 0x9e, 0x7f, 0xb5, 0xad, 0x8c, 0xda, 0xf8, 0x4, 0x86, 0x8c, 0xf9, 0xe, 0x8f, 0x1f, 0x8a, + 0xea, 0x17, 0xc0, 0x16, 0xb5, 0x5c, 0x2a, 0x7a, 0xd4, 0x97, 0xc8, 0x94, 0xfb, 0x71, 0xd7, + 0x53, 0xd7, 0x9b, 0x9a, 0x48, 0x4b, 0x6c, 0x37, 0x6d, 0x72, 0x3b, 0x99, 0x8d, 0x2e, 0x1d, + 0x43, 0x6, 0xbf, 0x10, 0x33, 0xb5, 0xae, 0xf8, 0xcc, 0xa5, 0xcb, 0xb2, 0x56, 0x8b, 0x69, + 0x24, 0x22, 0x6d, 0x22, 0xa3, 0x58, 0xab, 0x7d, 0x87, 0xe4, 0xac, 0x5f, 0x2e, 0x9, 0x1a, + 0xa7, 0x15, 0x79, 0xf3, 0xa5, 0x69, 0x9, 0x49, 0x7d, 0x72, 0xf5, 0x4e, 0x6, 0xba, 0xc1, + 0xc3, 0xb4, 0x41, 0x3b, 0xba, 0x5e, 0xaf, 0x94, 0xc3, 0xb6, 0x4f, 0x34, 0xf9, 0xeb, 0xa4, + 0x1a, 0xcb, 0x6a, 0xe2, 0x83, 0x77, 0x6d, 0x36, 0x46, 0x53, 0x78, 0x48, 0xfe, 0xe8, 0x84, + 0xbd, 0xdd, 0xf5, 0xb1, 0xba, 0x57, 0x98, 0x54, 0xcf, 0xfd, 0xce, 0xba, 0xc3, 0x44, 0x5, + 0x95, 0x27, 0xe5, 0x6d, 0xd5, 0x98, 0xf8, 0xf5, 0x66, 0x71, 0x5a, 0xbe, 0x43, 0x1, 0xdd, + 0x19, 0x11, 0x30, 0xe6, 0xb9, 0xf0, 0xc6, 0x40, 0x39, 0x12, 0x53, 0xe2, 0x29, 0x80, 0x3f, + 0x3a, 0xef, 0x27, 0x4b, 0xed, 0xbf, 0xde, 0x3f, 0xcb, 0xbd, 0x42, 0xea, 0xd6, 0x79, + ]; + + #[test] + fn parse_cert_chain() { + let cert: AttestationCertificate = from_slice(&SAMPLE_CERT_CHAIN[1..]).unwrap(); + assert_eq!(&cert.0, &SAMPLE_CERT_CHAIN[4..]); + + let _cert: Vec = from_slice(&SAMPLE_CERT_CHAIN).unwrap(); + } + + #[test] + fn parse_attestation_object() { + let value: AttestationObject = from_slice(&SAMPLE_ATTESTATION).unwrap(); + println!("{:?}", value); + + //assert_eq!(true, false); + } + + #[test] + fn parse_reader() { + let v: Vec = vec![ + 0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72, 0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72, + ]; + let (rest, value): (&[u8], String) = from_slice_stream(&v).unwrap(); + assert_eq!(value, "foobar"); + assert_eq!(rest, &[0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72]); + let (rest, value): (&[u8], String) = from_slice_stream(rest).unwrap(); + assert_eq!(value, "foobar"); + assert!(rest.is_empty()); + } +} diff --git a/src/ctap2/client_data.rs b/src/ctap2/client_data.rs new file mode 100644 index 00000000..0d98c726 --- /dev/null +++ b/src/ctap2/client_data.rs @@ -0,0 +1,259 @@ +#[cfg(test)] +use serde::de::{self, Deserialize, Deserializer, Visitor}; +use serde::ser::SerializeMap; +use serde::{Serialize, Serializer}; +use serde_json as json; +use sha2::{Digest, Sha256}; +use std::fmt; + +/// https://w3c.github.io/webauthn/#dom-collectedclientdata-tokenbinding +// tokenBinding, of type TokenBinding +// +// This OPTIONAL member contains information about the state of the Token +// Binding protocol [TokenBinding] used when communicating with the Relying +// Party. Its absence indicates that the client doesn’t support token +// binding. +// +// status, of type TokenBindingStatus +// +// This member is one of the following: +// +// supported +// +// Indicates the client supports token binding, but it was not +// negotiated when communicating with the Relying Party. +// +// present +// +// Indicates token binding was used when communicating with the +// Relying Party. In this case, the id member MUST be present. +// +// id, of type DOMString +// +// This member MUST be present if status is present, and MUST be a +// base64url encoding of the Token Binding ID that was used when +// communicating with the Relying Party. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TokenBinding { + Present(Vec), + Supported, +} + +impl Serialize for TokenBinding { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(2))?; + match *self { + TokenBinding::Supported => { + map.serialize_entry(&"status", &"supported")?; + } + TokenBinding::Present(ref v) => { + // The term Base64url Encoding refers to the base64 encoding + // using the URL- and filename-safe character set defined in + // Section 5 of [RFC4648], with all trailing '=' characters + // omitted (as permitted by Section 3.2) and without the + // inclusion of any line breaks, whitespace, or other additional + // characters. + let b64 = base64::encode_config(&v, base64::URL_SAFE_NO_PAD); + + map.serialize_entry(&"status", "present")?; + map.serialize_entry(&"id", &b64)?; + } + } + map.end() + } +} + +/// https://w3c.github.io/webauthn/#dom-collectedclientdata-type +// type, of type DOMString +// +// This member contains the string "webauthn.create" when creating new +// credentials, and "webauthn.get" when getting an assertion from an +// existing credential. The purpose of this member is to prevent certain +// types of signature confusion attacks (where an attacker substitutes one +// legitimate signature for another). +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum WebauthnType { + Create, + Get, +} + +impl Serialize for WebauthnType { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + WebauthnType::Create => serializer.serialize_str(&"webauthn.create"), + WebauthnType::Get => serializer.serialize_str(&"webauthn.get"), + } + } +} + +#[derive(Serialize, Clone, PartialEq, Eq)] +pub struct Challenge(Vec); + +impl fmt::Debug for Challenge { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let value = base64::encode_config(&self.0, base64::URL_SAFE_NO_PAD); + write!(f, "Challenge({})", value) + } +} + +impl From> for Challenge { + fn from(v: Vec) -> Challenge { + Challenge(v) + } +} + +impl AsRef<[u8]> for Challenge { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +pub type Origin = String; + +#[derive(Debug, Serialize, Clone, PartialEq, Eq)] +pub struct CollectedClientData { + #[serde(rename = "type")] + pub webauthn_type: WebauthnType, + pub challenge: Challenge, + pub origin: Origin, + // Should not be skipped but default to False according to https://www.w3.org/TR/webauthn/#collectedclientdata-hash-of-the-serialized-client-data + #[serde(rename = "crossOrigin", skip_serializing_if = "Option::is_none")] + pub cross_origin: Option, + #[serde(rename = "tokenBinding", skip_serializing_if = "Option::is_none")] + pub token_binding: Option, +} + +#[derive(Debug, Eq, PartialEq)] +pub struct ClientDataHash([u8; 32]); + +impl PartialEq<[u8]> for ClientDataHash { + fn eq(&self, other: &[u8]) -> bool { + self.0.eq(other) + } +} + +impl AsRef<[u8]> for ClientDataHash { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl Serialize for ClientDataHash { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_bytes(&self.0) + } +} + +#[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 { + // WebIDL's dictionary definition specifies that the order of the struct + // is exactly as the WebIDL specification declares it, with an algorithm + // for partial dictionaries, so that's how interop works for these + // things. + // See: https://heycam.github.io/webidl/#dfn-dictionary + + let data = json::to_vec(&self)?; + + let mut hasher = Sha256::new(); + hasher.input(&data); + + let mut output = [0u8; 32]; + output.copy_from_slice(hasher.result().as_slice()); + + Ok(ClientDataHash(output)) + } +} + +#[cfg(test)] +mod test { + use super::{Challenge, ClientDataHash, CollectedClientData, TokenBinding, WebauthnType}; + use serde_json as json; + + #[test] + fn test_token_binding_status() { + let tok = TokenBinding::Present(vec![0x00, 0x01, 0x02, 0x03]); + + let json_value = json::to_string(&tok).unwrap(); + assert_eq!(json_value, "{\"status\":\"present\",\"id\":\"AAECAw\"}"); + + let tok = TokenBinding::Supported; + + let json_value = json::to_string(&tok).unwrap(); + assert_eq!(json_value, "{\"status\":\"supported\"}"); + } + + #[test] + fn test_webauthn_type() { + let t = WebauthnType::Create; + + let json_value = json::to_string(&t).unwrap(); + assert_eq!(json_value, "\"webauthn.create\""); + + let t = WebauthnType::Get; + let json_value = json::to_string(&t).unwrap(); + assert_eq!(json_value, "\"webauthn.get\""); + } + + #[test] + fn test_collected_client_data() { + let client_data = CollectedClientData { + webauthn_type: WebauthnType::Create, + challenge: Challenge(vec![0x00, 0x01, 0x02, 0x03]), + origin: String::from("example.com"), + cross_origin: None, + token_binding: Some(TokenBinding::Present(vec![0x00, 0x01, 0x02, 0x03])), + }; + + assert_eq!( + client_data.hash().unwrap(), + ClientDataHash { + 0: [ + 0xc1, 0xdd, 0x35, 0x5f, 0x3c, 0x81, 0x69, 0x23, 0xe0, 0x57, 0xca, 0x03, 0x8d, + 0xba, 0xad, 0xb8, 0x5f, 0x95, 0x55, 0xcf, 0xc7, 0x62, 0x9b, 0x9d, 0x53, 0x66, + 0x97, 0x53, 0x80, 0xd7, 0x69, 0x4f + ] + } + ); + } +} diff --git a/src/ctap2/commands/make_credentials.rs b/src/ctap2/commands/make_credentials.rs new file mode 100644 index 00000000..078ac8ef --- /dev/null +++ b/src/ctap2/commands/make_credentials.rs @@ -0,0 +1,584 @@ +use super::{Command, CommandError, Request, RequestCtap1, RequestCtap2, Retryable, StatusCode}; +use crate::consts::{PARAMETER_SIZE, U2F_REGISTER, U2F_REQUEST_USER_PRESENCE}; +use crate::ctap2::attestation::{ + AAGuid, AttestationObject, AttestationStatement, AttestationStatementFidoU2F, + AttestedCredentialData, AuthenticatorData, AuthenticatorDataFlags, +}; +use crate::ctap2::client_data::CollectedClientData; +use crate::ctap2::server::{ + PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, User, +}; +use crate::transport::errors::{ApduErrorStatus, HIDError as TransportError}; +use crate::u2ftypes::{U2FAPDUHeader, U2FDevice}; +use nom::{do_parse, named, number::complete::be_u8, tag, take}; +#[cfg(test)] +use serde::Deserialize; +use serde::{ + ser::{Error as SerError, SerializeMap}, + Serialize, Serializer, +}; +use serde_cbor::{self, de::from_slice, ser, Value}; +use serde_json::{value as json_value, Map}; +use std::fmt; +use std::io; + +#[derive(Copy, Clone, Debug, Serialize)] +#[cfg_attr(test, derive(Deserialize))] +pub struct MakeCredentialsOptions { + #[serde(rename = "rk")] + pub resident_key: bool, + #[serde(rename = "uv")] + pub user_validation: bool, +} + +impl Default for MakeCredentialsOptions { + fn default() -> Self { + Self { + resident_key: false, + user_validation: true, + } + } +} + +pub(crate) trait UserValidation { + fn ask_user_validation(&self) -> bool; +} + +impl UserValidation for Option { + fn ask_user_validation(&self) -> bool { + match *self { + Some(ref e) if e.user_validation => true, + _ => false, + } + } +} + +#[derive(Debug)] +pub struct Pin(String); // TODO(MS): unimplemented! Requires more crypto + +#[derive(Debug)] +pub struct MakeCredentials { + client_data: CollectedClientData, + rp: RelyingParty, + // Note(baloo): If none -> ctap1 + user: Option, + pub_cred_params: Vec, + exclude_list: Vec, + + // https://www.w3.org/TR/webauthn/#client-extension-input + // The client extension input, which is a value that can be encoded in JSON, + // is passed from the WebAuthn Relying Party to the client in the get() or + // create() call, while the CBOR authenticator extension input is passed + // from the client to the authenticator for authenticator extensions during + // the processing of these calls. + extensions: Map, + options: Option, + pin: Option, + // TODO(MS): pin_protocol +} + +impl MakeCredentials { + pub fn new( + client_data: CollectedClientData, + rp: RelyingParty, + user: Option, + pub_cred_params: Vec, + exclude_list: Vec, + options: Option, + pin: Option, + ) -> Self { + Self { + client_data, + rp, + user, + pub_cred_params, + exclude_list, + // TODO(baloo): need to sort those out once final api is in + extensions: Map::new(), + options, + pin, + } + } +} + +impl Serialize for MakeCredentials { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Need to define how many elements are going to be in the map + // beforehand + let mut map_len = 4; + if !self.exclude_list.is_empty() { + map_len += 1; + } + if !self.extensions.is_empty() { + map_len += 1; + } + if self.options.is_some() { + map_len += 1; + } + + let mut map = serializer.serialize_map(Some(map_len))?; + let client_data_hash = self + .client_data + .hash() + .map_err(|e| S::Error::custom(format!("error while hashing client data: {}", e)))?; + map.serialize_entry(&1, &client_data_hash)?; + map.serialize_entry(&2, &self.rp)?; + map.serialize_entry(&3, &self.user)?; + map.serialize_entry(&4, &self.pub_cred_params)?; + if !self.exclude_list.is_empty() { + map.serialize_entry(&5, &self.exclude_list)?; + } + if !self.extensions.is_empty() { + map.serialize_entry(&6, &self.extensions)?; + } + if self.options.is_some() { + map.serialize_entry(&7, &self.options)?; + } + map.end() + } +} + +impl Request<(AttestationObject, CollectedClientData)> for MakeCredentials {} + +impl RequestCtap1 for MakeCredentials { + type Output = (AttestationObject, CollectedClientData); + + fn apdu_format(&self, _dev: &mut Dev) -> Result, TransportError> + where + Dev: U2FDevice, + { + // TODO(MS): Mandatory sanity checks are missing: + // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#u2f-authenticatorMakeCredential-interoperability + // If any of the below conditions is not true, platform errors out with CTAP2_ERR_UNSUPPORTED_OPTION. + // * pubKeyCredParams must use the ES256 algorithm (-7). + // * Options must not include "rk" set to true. + // * Options must not include "uv" set to true. + + let flags = if self.options.ask_user_validation() { + U2F_REQUEST_USER_PRESENCE + } else { + 0 + }; + + let mut register_data = Vec::with_capacity(2 * PARAMETER_SIZE); + register_data.extend_from_slice(self.client_data.challenge.as_ref()); + register_data.extend_from_slice(self.rp.hash().as_ref()); + + let cmd = U2F_REGISTER; + let apdu = U2FAPDUHeader::serialize(cmd, flags, ®ister_data)?; + + Ok(apdu) + } + + fn handle_response_ctap1( + &self, + status: Result<(), ApduErrorStatus>, + input: &[u8], + ) -> Result> { + if Err(ApduErrorStatus::ConditionsNotSatisfied) == status { + return Err(Retryable::Retry); + } + + named!( + parse_register<(&[u8], &[u8])>, + do_parse!( + reserved: tag!(&[0x05]) + >> public_key: take!(65) + >> key_handle_len: be_u8 + >> key_handle: take!(key_handle_len) + >> (public_key, key_handle) + ) + ); + + let (_rest, (_public_key, key_handle)) = parse_register(input) + .map_err(|e| { + error!("error while parsing registration = {:?}", e); + io::Error::new(io::ErrorKind::Other, "unable to parse registration") + }) + .map_err(|e| TransportError::IO(None, e)) + .map_err(Retryable::Error)?; + + // TODO(MS): This is currently not parsed within the crate, but outside by a user-provided function + // See examples/main.rs: u2f_get_key_handle_from_register_response() + // We would need to add a DER parser as a dependency, which I don't want to do right now. + // let (signature, cert) = der_parser::parse_der(rest) + // .map_err(|e| { + // error!("error while parsing cert = {:?}", e); + // let err = io::Error::new(io::ErrorKind::Other, "Failed to parse x509 certificate"); + // let err = error::Error::from(err); + // let err = CommandError::Parsing(err); + // let err = TransportError::Command(err); + // Retryable::Error(err) + // }) + // .map(|(sig, cert)| (sig, &rest[..rest.len() - sig.len()]))?; + + let auth_data = AuthenticatorData { + rp_id_hash: self.rp.hash(), + flags: AuthenticatorDataFlags::empty(), + counter: 0, + credential_data: Some(AttestedCredentialData { + aaguid: AAGuid::default(), + credential_id: Vec::from(key_handle), + // TODO(baloo): this is wrong, this is not the format expected by cose::PublicKey + // (or is it?) + // see This is the (uncompressed) x,y-representation of a curve point on the P-256 NIST elliptic curve. + // https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html + //credential_public_key: PublicKey::new(EllipticCurve::P256, Vec::from(public_key)), + }), + extensions: Vec::new(), + }; + + // TODO(MS) + // let att_statement_u2f = AttestationStatementFidoU2F::new(cert, signature); + let att_statement_u2f = AttestationStatementFidoU2F::new(&[], &[]); + let att_statement = AttestationStatement::FidoU2F(att_statement_u2f); + let attestation_object = AttestationObject { + auth_data, + att_statement, + }; + let client_data = self.client_data.clone(); + + Ok((attestation_object, client_data)) + } +} + +impl RequestCtap2 for MakeCredentials { + type Output = (AttestationObject, CollectedClientData); + + fn command() -> Command { + Command::MakeCredentials + } + + fn wire_format(&self, _dev: &mut Dev) -> Result, TransportError> + where + Dev: U2FDevice + io::Read + io::Write + fmt::Debug, + { + Ok(ser::to_vec(&self).map_err(CommandError::Serialization)?) + } + + fn handle_response_ctap2( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result + where + Dev: U2FDevice + io::Read + io::Write + fmt::Debug, + { + if input.is_empty() { + return Err(TransportError::Command(CommandError::InputTooSmall)); + } + + let status: StatusCode = input[0].into(); + debug!("response status code: {:?}", status); + if input.len() > 1 { + if status.is_ok() { + let attestation = from_slice(&input[1..]).map_err(CommandError::Parsing)?; + let client_data = self.client_data.clone(); + Ok((attestation, client_data)) + } else { + let data: Value = from_slice(&input[1..]).map_err(CommandError::Parsing)?; + Err(TransportError::Command(CommandError::StatusCode( + status, + Some(data), + ))) + } + } else if status.is_ok() { + Err(TransportError::Command(CommandError::InputTooSmall)) + } else { + Err(TransportError::Command(CommandError::StatusCode( + status, None, + ))) + } + } +} + +#[cfg(test)] +pub mod test { + use super::MakeCredentials; + use crate::ctap2::attestation::{ + AAGuid, AttestationCertificate, AttestationObject, AttestationStatement, + AttestationStatementPacked, AttestedCredentialData, AuthenticatorData, + AuthenticatorDataFlags, Signature, + }; + use crate::ctap2::client_data::{Challenge, CollectedClientData, TokenBinding, WebauthnType}; + use crate::ctap2::commands::RequestCtap2; + use crate::ctap2::server::RpIdHash; + use crate::ctap2::server::{Alg, PublicKeyCredentialParameters, RelyingParty, User}; + use crate::u2fprotocol::tests::platform::TestDevice; + use serde_bytes::ByteBuf; + pub const MAKE_CREDENTIALS_SAMPLE_RESPONSE: [u8; 666] = [ + 0x00, // status = success + 0xa3, // map(3) + 0x01, // unsigned(1) + 0x66, // text(6) + 0x70, 0x61, 0x63, 0x6b, 0x65, 0x64, // "packed" + 0x02, // unsigned(2) + 0x58, 0x9a, // bytes(154) + // authData + 0xc2, 0x89, 0xc5, 0xca, 0x9b, 0x04, 0x60, 0xf9, 0x34, 0x6a, 0xb4, 0xe4, 0x2d, 0x84, + 0x27, // rp_id_hash + 0x43, 0x40, 0x4d, 0x31, 0xf4, 0x84, 0x68, 0x25, 0xa6, 0xd0, 0x65, 0xbe, 0x59, 0x7a, + 0x87, // rp_id_hash + 0x05, 0x1d, 0x41, // authData Flags + 0x00, 0x00, 0x00, 0x0b, // authData counter + 0xf8, 0xa0, 0x11, 0xf3, 0x8c, 0x0a, 0x4d, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1f, 0x9e, 0xdc, + 0x7d, // AAGUID + 0x00, 0x10, // credential id length + 0x89, 0x59, 0xce, 0xad, 0x5b, 0x5c, 0x48, 0x16, 0x4e, 0x8a, 0xbc, 0xd6, 0xd9, 0x43, 0x5c, + 0x6f, // credential id + 0xa3, 0x63, 0x61, 0x6c, 0x67, 0x65, 0x45, 0x53, 0x32, 0x35, 0x36, 0x61, 0x78, 0x58, 0x20, + 0xf7, 0xc4, 0xf4, 0xa6, 0xf1, 0xd7, 0x95, 0x38, 0xdf, 0xa4, 0xc9, 0xac, 0x50, 0x84, 0x8d, + 0xf7, 0x08, 0xbc, 0x1c, 0x99, 0xf5, 0xe6, 0x0e, 0x51, 0xb4, 0x2a, 0x52, 0x1b, 0x35, 0xd3, + 0xb6, 0x9a, 0x61, 0x79, 0x58, 0x20, 0xde, 0x7b, 0x7d, 0x6c, 0xa5, 0x64, 0xe7, 0x0e, 0xa3, + 0x21, 0xa4, 0xd5, 0xd9, 0x6e, 0xa0, 0x0e, 0xf0, 0xe2, 0xdb, 0x89, 0xdd, 0x61, 0xd4, 0x89, + 0x4c, 0x15, 0xac, 0x58, 0x5b, 0xd2, 0x36, 0x84, 0x03, // unsigned(3) + 0xa3, // map(3) + 0x63, // text(3) + 0x61, 0x6c, 0x67, // "alg" + 0x26, // -7 (ES256) + 0x63, // text(3) + 0x73, 0x69, 0x67, // "sig" + 0x58, 0x47, // bytes(71) + 0x30, 0x45, 0x02, 0x20, 0x13, 0xf7, 0x3c, 0x5d, // signature... + 0x9d, 0x53, 0x0e, 0x8c, 0xc1, 0x5c, 0xc9, 0xbd, 0x96, 0xad, 0x58, 0x6d, 0x39, 0x36, 0x64, + 0xe4, 0x62, 0xd5, 0xf0, 0x56, 0x12, 0x35, 0xe6, 0x35, 0x0f, 0x2b, 0x72, 0x89, 0x02, 0x21, + 0x00, 0x90, 0x35, 0x7f, 0xf9, 0x10, 0xcc, 0xb5, 0x6a, 0xc5, 0xb5, 0x96, 0x51, 0x19, 0x48, + 0x58, 0x1c, 0x8f, 0xdd, 0xb4, 0xa2, 0xb7, 0x99, 0x59, 0x94, 0x80, 0x78, 0xb0, 0x9f, 0x4b, + 0xdc, 0x62, 0x29, 0x63, // text(3) + 0x78, 0x35, 0x63, // "x5c" + 0x81, // array(1) + 0x59, 0x01, 0x97, // bytes(407) + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, //certificate... + 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x09, 0x00, 0x85, 0x9b, 0x72, 0x6c, 0xb2, 0x4b, + 0x4c, 0x29, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30, + 0x47, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, + 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75, 0x62, 0x69, 0x63, + 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, 0x06, 0x03, 0x55, 0x04, 0x0b, + 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, + 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x1e, 0x17, + 0x0d, 0x31, 0x36, 0x31, 0x32, 0x30, 0x34, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x17, + 0x0d, 0x32, 0x36, 0x31, 0x32, 0x30, 0x32, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x30, + 0x47, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, + 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75, 0x62, 0x69, 0x63, + 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, 0x06, 0x03, 0x55, 0x04, 0x0b, + 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, + 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x59, 0x30, + 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, + 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xad, 0x11, 0xeb, 0x0e, 0x88, 0x52, + 0xe5, 0x3a, 0xd5, 0xdf, 0xed, 0x86, 0xb4, 0x1e, 0x61, 0x34, 0xa1, 0x8e, 0xc4, 0xe1, 0xaf, + 0x8f, 0x22, 0x1a, 0x3c, 0x7d, 0x6e, 0x63, 0x6c, 0x80, 0xea, 0x13, 0xc3, 0xd5, 0x04, 0xff, + 0x2e, 0x76, 0x21, 0x1b, 0xb4, 0x45, 0x25, 0xb1, 0x96, 0xc4, 0x4c, 0xb4, 0x84, 0x99, 0x79, + 0xcf, 0x6f, 0x89, 0x6e, 0xcd, 0x2b, 0xb8, 0x60, 0xde, 0x1b, 0xf4, 0x37, 0x6b, 0xa3, 0x0d, + 0x30, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x04, 0x02, 0x30, 0x00, 0x30, 0x0a, + 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x49, 0x00, 0x30, 0x46, + 0x02, 0x21, 0x00, 0xe9, 0xa3, 0x9f, 0x1b, 0x03, 0x19, 0x75, 0x25, 0xf7, 0x37, 0x3e, 0x10, + 0xce, 0x77, 0xe7, 0x80, 0x21, 0x73, 0x1b, 0x94, 0xd0, 0xc0, 0x3f, 0x3f, 0xda, 0x1f, 0xd2, + 0x2d, 0xb3, 0xd0, 0x30, 0xe7, 0x02, 0x21, 0x00, 0xc4, 0xfa, 0xec, 0x34, 0x45, 0xa8, 0x20, + 0xcf, 0x43, 0x12, 0x9c, 0xdb, 0x00, 0xaa, 0xbe, 0xfd, 0x9a, 0xe2, 0xd8, 0x74, 0xf9, 0xc5, + 0xd3, 0x43, 0xcb, 0x2f, 0x11, 0x3d, 0xa2, 0x37, 0x23, 0xf3, + ]; + + pub const MAKE_CREDENTIALS_SAMPLE_REQUEST: [u8; 244] = [ + // 0xa5, // map(5) Replace line below with this one, once MakeCredentialOptions work + 0xa4, // map(4) + 0x01, // unsigned(1) - clientDataHash + 0x58, 0x20, // bytes(32) + 0xc1, 0xdd, 0x35, 0x5f, 0x3c, 0x81, 0x69, 0x23, 0xe0, 0x57, 0xca, 0x03, 0x8d, // hash + 0xba, 0xad, 0xb8, 0x5f, 0x95, 0x55, 0xcf, 0xc7, 0x62, 0x9b, 0x9d, 0x53, 0x66, // hash + 0x97, 0x53, 0x80, 0xd7, 0x69, 0x4f, // hash + 0x02, // unsigned(2) - rp + // 0xa2, // map(2) Replace line below with this one, once RelyingParty supports "name" + 0xa1, // map(1) + 0x62, // text(2) + 0x69, 0x64, // "id" + 0x6b, // text(11) + 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, // "example.com" + // TODO(MS): RelyingParty does not yet support optional fields "name" and "icon" + // 0x64, // text(4) + // 0x6e, 0x61, 0x6d, 0x65, // "name" + // 0x64, // text(4) + // 0x41, 0x63, 0x6d, 0x65, // "Acme" + 0x03, // unsigned(3) - user + 0xa4, // map(4) + 0x62, // text(2) + 0x69, 0x64, // "id" + 0x58, 0x20, // bytes(32) + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, + 0x02, // userid + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, // ... + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, // ... + 0x64, // text(4) + 0x69, 0x63, 0x6f, 0x6e, // "icon" + 0x78, 0x2b, // text(43) + 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x70, 0x69, 0x63, 0x73, 0x2e, 0x65, + 0x78, // "https://pics.example.com/00/p/aBjjjpqPb.png" + 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x30, 0x30, 0x2f, 0x70, + 0x2f, // + 0x61, 0x42, 0x6a, 0x6a, 0x6a, 0x70, 0x71, 0x50, 0x62, 0x2e, 0x70, 0x6e, 0x67, // + 0x64, // text(4) + 0x6e, 0x61, 0x6d, 0x65, // "name" + 0x76, // text(22) + 0x6a, 0x6f, 0x68, 0x6e, 0x70, 0x73, 0x6d, 0x69, 0x74, 0x68, 0x40, 0x65, 0x78, 0x61, + 0x6d, // "johnpsmith@example.com" + 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, // ... + 0x6b, // text(11) + 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, // "displayName" + 0x6d, // text(13) + 0x4a, 0x6f, 0x68, 0x6e, 0x20, 0x50, 0x2e, 0x20, 0x53, 0x6d, 0x69, 0x74, + 0x68, // "John P. Smith" + 0x04, // unsigned(4) - pubKeyCredParams + 0x82, // array(2) + 0xa2, // map(2) + 0x63, // text(3) + 0x61, 0x6c, 0x67, // "alg" + 0x26, // -7 (ES256) + 0x64, // text(4) + 0x74, 0x79, 0x70, 0x65, // "type" + 0x6a, // text(10) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // "public-key" + 0xa2, // map(2) + 0x63, // text(3) + 0x61, 0x6c, 0x67, // "alg" + 0x39, 0x01, 0x00, // -257 (RS256) + 0x64, // text(4) + 0x74, 0x79, 0x70, 0x65, // "type" + 0x6a, // text(10) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, + 0x79, // "public-key" + // TODO(MS): Options seem to be parsed differently than in the example here. + // 0x07, // unsigned(7) - options + // 0xa1, // map(1) + // 0x62, // text(2) + // 0x72, 0x6b, // "rk" + // 0xf5, // primitive(21) + ]; + #[test] + fn parse_response() { + let req = MakeCredentials::new( + CollectedClientData { + webauthn_type: WebauthnType::Create, + challenge: Challenge::from(vec![0x00, 0x01, 0x02, 0x03]), + origin: String::from("example.com"), + cross_origin: None, + token_binding: Some(TokenBinding::Present(vec![0x00, 0x01, 0x02, 0x03])), + }, + RelyingParty { + id: String::from("example.com"), + }, + Some(User { + id: base64::decode_config( + "MIIBkzCCATigAwIBAjCCAZMwggE4oAMCAQIwggGTMII=", + base64::URL_SAFE_NO_PAD, + ) + .unwrap(), + icon: Some("https://pics.example.com/00/p/aBjjjpqPb.png".to_string()), + name: String::from("johnpsmith@example.com"), + display_name: Some(String::from("John P. Smith")), + }), + vec![ + PublicKeyCredentialParameters { alg: Alg::ES256 }, + PublicKeyCredentialParameters { alg: Alg::RS256 }, + ], + Vec::new(), + None, + // Some(MakeCredentialsOptions { + // resident_key: true, + // user_validation: false, + // }), + None, + ); + + let mut device = TestDevice::new(); // not really used (all functions ignore it) + let req_serialized = req + .wire_format(&mut device) + .expect("Failed to serialize MakeCredentials request"); + assert_eq!(req_serialized, MAKE_CREDENTIALS_SAMPLE_REQUEST); + let (attestation_object, _collected_client_data) = req + .handle_response_ctap2(&mut device, &MAKE_CREDENTIALS_SAMPLE_RESPONSE) + .expect("Failed to handle CTAP2 response"); + + let expected = AttestationObject { + auth_data: AuthenticatorData { + rp_id_hash: RpIdHash::from(&[ + 0xc2, 0x89, 0xc5, 0xca, 0x9b, 0x04, 0x60, 0xf9, 0x34, 0x6a, 0xb4, 0xe4, 0x2d, + 0x84, 0x27, 0x43, 0x40, 0x4d, 0x31, 0xf4, 0x84, 0x68, 0x25, 0xa6, 0xd0, 0x65, + 0xbe, 0x59, 0x7a, 0x87, 0x5, 0x1d, + ]) + .unwrap(), + flags: AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::ATTESTED, + counter: 11, + credential_data: Some(AttestedCredentialData { + aaguid: AAGuid::from(&[ + 0xf8, 0xa0, 0x11, 0xf3, 0x8c, 0x0a, 0x4d, 0x15, 0x80, 0x06, 0x17, 0x11, + 0x1f, 0x9e, 0xdc, 0x7d, + ]) + .unwrap(), + credential_id: vec![ + 0x89, 0x59, 0xce, 0xad, 0x5b, 0x5c, 0x48, 0x16, 0x4e, 0x8a, 0xbc, 0xd6, + 0xd9, 0x43, 0x5c, 0x6f, + ], + }), + extensions: Vec::new(), + }, + att_statement: AttestationStatement::Packed(AttestationStatementPacked { + alg: Alg::ES256, + sig: Signature(ByteBuf::from([ + 0x30, 0x45, 0x02, 0x20, 0x13, 0xf7, 0x3c, 0x5d, 0x9d, 0x53, 0x0e, 0x8c, 0xc1, + 0x5c, 0xc9, 0xbd, 0x96, 0xad, 0x58, 0x6d, 0x39, 0x36, 0x64, 0xe4, 0x62, 0xd5, + 0xf0, 0x56, 0x12, 0x35, 0xe6, 0x35, 0x0f, 0x2b, 0x72, 0x89, 0x02, 0x21, 0x00, + 0x90, 0x35, 0x7f, 0xf9, 0x10, 0xcc, 0xb5, 0x6a, 0xc5, 0xb5, 0x96, 0x51, 0x19, + 0x48, 0x58, 0x1c, 0x8f, 0xdd, 0xb4, 0xa2, 0xb7, 0x99, 0x59, 0x94, 0x80, 0x78, + 0xb0, 0x9f, 0x4b, 0xdc, 0x62, 0x29, + ])), + attestation_cert: vec![AttestationCertificate(vec![ + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, + 0x02, 0x09, 0x00, 0x85, 0x9b, 0x72, 0x6c, 0xb2, 0x4b, 0x4c, 0x29, 0x30, 0x0a, + 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30, 0x47, 0x31, + 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, + 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75, 0x62, + 0x69, 0x63, 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, 0x06, + 0x03, 0x55, 0x04, 0x0b, 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, + 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x1e, 0x17, 0x0d, 0x31, 0x36, 0x31, 0x32, + 0x30, 0x34, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x17, 0x0d, 0x32, 0x36, + 0x31, 0x32, 0x30, 0x32, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x30, 0x47, + 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, + 0x31, 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75, + 0x62, 0x69, 0x63, 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, + 0x06, 0x03, 0x55, 0x04, 0x0b, 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, + 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, + 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, + 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, + 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xad, 0x11, 0xeb, 0x0e, 0x88, 0x52, + 0xe5, 0x3a, 0xd5, 0xdf, 0xed, 0x86, 0xb4, 0x1e, 0x61, 0x34, 0xa1, 0x8e, 0xc4, + 0xe1, 0xaf, 0x8f, 0x22, 0x1a, 0x3c, 0x7d, 0x6e, 0x63, 0x6c, 0x80, 0xea, 0x13, + 0xc3, 0xd5, 0x04, 0xff, 0x2e, 0x76, 0x21, 0x1b, 0xb4, 0x45, 0x25, 0xb1, 0x96, + 0xc4, 0x4c, 0xb4, 0x84, 0x99, 0x79, 0xcf, 0x6f, 0x89, 0x6e, 0xcd, 0x2b, 0xb8, + 0x60, 0xde, 0x1b, 0xf4, 0x37, 0x6b, 0xa3, 0x0d, 0x30, 0x0b, 0x30, 0x09, 0x06, + 0x03, 0x55, 0x1d, 0x13, 0x04, 0x02, 0x30, 0x00, 0x30, 0x0a, 0x06, 0x08, 0x2a, + 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x49, 0x00, 0x30, 0x46, 0x02, + 0x21, 0x00, 0xe9, 0xa3, 0x9f, 0x1b, 0x03, 0x19, 0x75, 0x25, 0xf7, 0x37, 0x3e, + 0x10, 0xce, 0x77, 0xe7, 0x80, 0x21, 0x73, 0x1b, 0x94, 0xd0, 0xc0, 0x3f, 0x3f, + 0xda, 0x1f, 0xd2, 0x2d, 0xb3, 0xd0, 0x30, 0xe7, 0x02, 0x21, 0x00, 0xc4, 0xfa, + 0xec, 0x34, 0x45, 0xa8, 0x20, 0xcf, 0x43, 0x12, 0x9c, 0xdb, 0x00, 0xaa, 0xbe, + 0xfd, 0x9a, 0xe2, 0xd8, 0x74, 0xf9, 0xc5, 0xd3, 0x43, 0xcb, 0x2f, 0x11, 0x3d, + 0xa2, 0x37, 0x23, 0xf3, + ])], + }), + }; + + assert_eq!( + &attestation_object.auth_data.rp_id_hash.0, + &[ + 0xc2, 0x89, 0xc5, 0xca, 0x9b, 0x04, 0x60, 0xf9, 0x34, 0x6a, 0xb4, 0xe4, 0x2d, 0x84, + 0x27, 0x43, 0x40, 0x4d, 0x31, 0xf4, 0x84, 0x68, 0x25, 0xa6, 0xd0, 0x65, 0xbe, 0x59, + 0x7a, 0x87, 0x5, 0x1d + ] + ); + + assert_eq!(attestation_object, expected); + } +} diff --git a/src/ctap2/commands/mod.rs b/src/ctap2/commands/mod.rs index 3a4711f9..704c7aaa 100644 --- a/src/ctap2/commands/mod.rs +++ b/src/ctap2/commands/mod.rs @@ -1,6 +1,6 @@ use crate::transport::errors::{ApduErrorStatus, HIDError}; use crate::transport::FidoDevice; -use serde_cbor::error::Error as SerdeError; +use serde_cbor::{error::Error as CborError, Value}; use std::error::Error as StdErrorT; use std::fmt; use std::io::{Read, Write}; @@ -8,6 +8,7 @@ use std::io::{Read, Write}; #[allow(dead_code)] // TODO(MS): Remove me asap pub(crate) mod get_info; pub(crate) mod get_version; +pub(crate) mod make_credentials; pub(crate) trait Request where @@ -314,7 +315,10 @@ impl Into for StatusCode { pub enum CommandError { InputTooSmall, MissingRequiredField(&'static str), - Deserializing(SerdeError), + Deserializing(CborError), + Serialization(CborError), + Parsing(CborError), + StatusCode(StatusCode, Option), } impl fmt::Display for CommandError { @@ -327,6 +331,13 @@ impl fmt::Display for CommandError { CommandError::Deserializing(ref e) => { write!(f, "CommandError: Error while parsing: {}", e) } + CommandError::Serialization(ref e) => { + write!(f, "CommandError: Error while serializing: {}", e) + } + CommandError::Parsing(ref e) => write!(f, "CommandError: Error while parsing: {}", e), + CommandError::StatusCode(ref code, ref value) => { + write!(f, "CommandError: Unexpected code: {:?} ({:?})", code, value) + } } } } diff --git a/src/ctap2/mod.rs b/src/ctap2/mod.rs index 398fb7db..b0687bb0 100644 --- a/src/ctap2/mod.rs +++ b/src/ctap2/mod.rs @@ -1,4 +1,9 @@ #[allow(dead_code)] // TODO(MS): Remove me asap pub mod commands; +pub(crate) mod attestation; + +pub mod client_data; +pub(crate) mod server; +pub(crate) mod utils; // TODO: More here soon diff --git a/src/ctap2/server.rs b/src/ctap2/server.rs new file mode 100644 index 00000000..4749453a --- /dev/null +++ b/src/ctap2/server.rs @@ -0,0 +1,549 @@ +use serde_bytes::ByteBuf; +#[cfg(test)] +use serde_cbor::{error, ser}; +use sha2::{Digest, Sha256}; +use std::fmt; + +#[cfg(test)] +use serde::de::MapAccess; +use serde::{ + de::{Error as SerdeError, Unexpected, Visitor}, + ser::SerializeMap, + Deserialize, Deserializer, Serialize, Serializer, +}; + +use crate::AuthenticatorTransports; +use crate::KeyHandle; + +#[derive(Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct RpIdHash(pub [u8; 32]); + +impl fmt::Debug for RpIdHash { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let value = base64::encode_config(&self.0[..], base64::URL_SAFE_NO_PAD); + write!(f, "RpIdHash({})", value) + } +} + +impl AsRef<[u8]> for RpIdHash { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl RpIdHash { + pub fn from(src: &[u8]) -> Result { + let mut payload = [0u8; 32]; + if src.len() != payload.len() { + Err(()) + } else { + payload.copy_from_slice(src); + Ok(RpIdHash(payload)) + } + } +} + +#[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 + // in the example "A PublicKeyCredentialRpEntity DOM object defined as follows:" + // inconsistent with https://w3c.github.io/webauthn/#sctn-rp-credential-params + pub id: String, + // TODO(baloo): need to add name and icon fields (optionals) +} + +impl RelyingParty { + #[cfg(test)] + pub fn to_ctap2(&self) -> Result, error::Error> { + ser::to_vec(self) + } + + 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) + } +} + +// TODO(baloo): should we rename this PublicKeyCredentialUserEntity ? +#[derive(Debug, Serialize, Clone, Eq, PartialEq, Deserialize)] +pub struct User { + #[serde(with = "serde_bytes")] + pub id: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, + pub name: String, + #[serde(skip_serializing_if = "Option::is_none", rename = "displayName")] + pub display_name: Option, +} + +#[cfg(test)] +impl User { + pub fn to_ctap2(&self) -> Result, error::Error> { + ser::to_vec(self) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +/// see: https://www.iana.org/assignments/cose/cose.xhtml#table-algorithms +// TODO(baloo): could probably use a more generic approach, need to see this +// whenever we introduce the firefox-side api +pub enum Alg { + ES256, + RS256, +} + +impl Serialize for Alg { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + Alg::ES256 => serializer.serialize_i8(-7), + Alg::RS256 => serializer.serialize_i16(-257), + } + } +} + +impl<'de> Deserialize<'de> for Alg { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct AlgVisitor; + + impl<'de> Visitor<'de> for AlgVisitor { + type Value = Alg; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a signed integer") + } + + fn visit_i64(self, v: i64) -> Result + where + E: SerdeError, + { + match v { + -7 => Ok(Alg::ES256), + -257 => Ok(Alg::RS256), + v => Err(SerdeError::invalid_value(Unexpected::Signed(v), &self)), + } + } + } + + deserializer.deserialize_any(AlgVisitor) + } +} + +#[derive(Debug, Clone)] +pub struct PublicKeyCredentialParameters { + pub alg: Alg, +} + +impl Serialize for PublicKeyCredentialParameters { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(2))?; + map.serialize_entry("alg", &self.alg)?; + map.serialize_entry("type", "public-key")?; + map.end() + } +} + +#[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(SerdeError::duplicate_field("alg")); + } + + alg = Some(map.next_value()?); + } + "type" => { + if found_type { + return Err(SerdeError::duplicate_field("type")); + } + + let v: &str = map.next_value()?; + if v != "public-key" { + return Err(SerdeError::custom(format!("invalid value: {}", v))); + } + found_type = true; + } + v => { + return Err(SerdeError::unknown_field(v, &[])); + } + } + } + + if !found_type { + return Err(SerdeError::missing_field("type")); + } + + let alg = alg.ok_or(SerdeError::missing_field("alg"))?; + + Ok(PublicKeyCredentialParameters { alg }) + } + } + + deserializer.deserialize_bytes(PublicKeyCredentialParametersVisitor) + } +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Eq)] +#[serde(rename_all = "lowercase")] +pub enum Transport { + USB, + NFC, + BLE, + Internal, +} + +impl From for Vec { + fn from(t: AuthenticatorTransports) -> Self { + let mut transports = Vec::new(); + if t.contains(AuthenticatorTransports::USB) { + transports.push(Transport::USB); + } + if t.contains(AuthenticatorTransports::NFC) { + transports.push(Transport::NFC); + } + if t.contains(AuthenticatorTransports::BLE) { + transports.push(Transport::BLE); + } + + transports + } +} + +#[derive(Debug)] +pub struct PublicKeyCredentialDescriptor { + pub id: Vec, + pub transports: Vec, +} + +impl Serialize for PublicKeyCredentialDescriptor { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(3))?; + map.serialize_entry("type", "public-key")?; + map.serialize_entry("id", &ByteBuf::from(&self.id[..]))?; + map.serialize_entry("transports", &self.transports)?; + map.end() + } +} + +#[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(SerdeError::duplicate_field("id")); + } + + id = Some(map.next_value()?); + } + "transports" => { + if transports.is_some() { + return Err(SerdeError::duplicate_field("transports")); + } + + transports = Some(map.next_value()?); + } + "type" => { + if found_type { + return Err(SerdeError::duplicate_field("type")); + } + + let v: &str = map.next_value()?; + if v != "public-key" { + return Err(SerdeError::custom(format!("invalid value: {}", v))); + } + found_type = true; + } + v => { + return Err(SerdeError::unknown_field(v, &[])); + } + } + } + + if !found_type { + return Err(SerdeError::missing_field("type")); + } + + let id = id.ok_or(SerdeError::missing_field("id"))?; + let transports = transports.ok_or(SerdeError::missing_field("transports"))?; + + Ok(PublicKeyCredentialDescriptor { id, transports }) + } + } + + deserializer.deserialize_bytes(PublicKeyCredentialDescriptorVisitor) + } +} + +impl From<&KeyHandle> for PublicKeyCredentialDescriptor { + fn from(kh: &KeyHandle) -> Self { + Self { + id: kh.credential.clone(), + transports: kh.transports.into(), + } + } +} + +#[cfg(test)] +mod test { + use super::{ + Alg, PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, Transport, + User, + }; + + #[test] + fn serialize_rp() { + let rp = RelyingParty { + id: String::from("Acme"), + }; + + let payload = rp.to_ctap2(); + let payload = payload.unwrap(); + assert_eq!( + &payload, + &[ + 0xa1, // map(1) + 0x62, // text(2) + 0x69, 0x64, // "id" + 0x64, // text(4) + 0x41, 0x63, 0x6d, 0x65 + ] + ); + } + + #[test] + fn serialize_user() { + let user = User { + id: vec![ + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30, + 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82, + 0x01, 0x93, 0x30, 0x82, + ], + icon: Some(String::from("https://pics.example.com/00/p/aBjjjpqPb.png")), + name: String::from("johnpsmith@example.com"), + display_name: Some(String::from("John P. Smith")), + }; + + let payload = user.to_ctap2(); + println!("payload = {:?}", payload); + let payload = payload.unwrap(); + assert_eq!( + payload, + vec![ + 0xa4, // map(4) + 0x62, // text(2) + 0x69, 0x64, // "id" + 0x58, 0x20, // bytes(32) + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, // userid + 0x02, 0x01, 0x02, 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, // ... + 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82, 0x01, 0x93, // ... + 0x30, 0x82, // ... + 0x64, // text(4) + 0x69, 0x63, 0x6f, 0x6e, // "icon" + 0x78, 0x2b, // text(43) + 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x70, + 0x69, // "https://pics.example.com/00/p/aBjjjpqPb.png" + 0x63, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, // ... + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x30, 0x30, 0x2f, 0x70, 0x2f, // ... + 0x61, 0x42, 0x6a, 0x6a, 0x6a, 0x70, 0x71, 0x50, 0x62, 0x2e, // ... + 0x70, 0x6e, 0x67, // ... + 0x64, // text(4) + 0x6e, 0x61, 0x6d, 0x65, // "name" + 0x76, // text(22) + 0x6a, 0x6f, 0x68, 0x6e, 0x70, 0x73, 0x6d, 0x69, 0x74, + 0x68, // "johnpsmith@example.com" + 0x40, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, // ... + 0x6f, 0x6d, // ... + 0x6b, // text(11) + 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, // "displayName" + 0x65, // ... + 0x6d, // text(13) + 0x4a, 0x6f, 0x68, 0x6e, 0x20, 0x50, 0x2e, 0x20, 0x53, 0x6d, // "John P. Smith" + 0x69, 0x74, 0x68, // ... + ] + ); + } + + #[test] + fn serialize_user_noicon_nodisplayname() { + let user = User { + id: vec![ + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30, + 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82, + 0x01, 0x93, 0x30, 0x82, + ], + icon: None, + name: String::from("johnpsmith@example.com"), + display_name: None, + }; + + let payload = user.to_ctap2(); + println!("payload = {:?}", payload); + let payload = payload.unwrap(); + assert_eq!( + payload, + vec![ + 0xa2, // map(2) + 0x62, // text(2) + 0x69, 0x64, // "id" + 0x58, 0x20, // bytes(32) + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, // userid + 0x02, 0x01, 0x02, 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, // ... + 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82, 0x01, 0x93, // ... + 0x30, 0x82, // ... + 0x64, // text(4) + 0x6e, 0x61, 0x6d, 0x65, // "name" + 0x76, // text(22) + 0x6a, 0x6f, 0x68, 0x6e, 0x70, 0x73, 0x6d, 0x69, 0x74, + 0x68, // "johnpsmith@example.com" + 0x40, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, // ... + 0x6f, 0x6d, // ... + ] + ); + } + + use serde_cbor::ser; + + #[test] + fn public_key() { + let keys = vec![ + PublicKeyCredentialParameters { alg: Alg::ES256 }, + PublicKeyCredentialParameters { alg: Alg::RS256 }, + ]; + + let payload = ser::to_vec(&keys); + println!("payload = {:?}", payload); + let payload = payload.unwrap(); + assert_eq!( + payload, + vec![ + 0x82, // array(2) + 0xa2, // map(2) + 0x63, // text(3) + 0x61, 0x6c, 0x67, // "alg" + 0x26, // -7 (ES256) + 0x64, // text(4) + 0x74, 0x79, 0x70, 0x65, // "type" + 0x6a, // text(10) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, // "public-key" + 0x2D, 0x6B, 0x65, 0x79, // ... + 0xa2, // map(2) + 0x63, // text(3) + 0x61, 0x6c, 0x67, // "alg" + 0x39, 0x01, 0x00, // -257 (RS256) + 0x64, // text(4) + 0x74, 0x79, 0x70, 0x65, // "type" + 0x6a, // text(10) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, // "public-key" + 0x2D, 0x6B, 0x65, 0x79 // ... + ] + ); + } + + #[test] + fn public_key_desc() { + let key = PublicKeyCredentialDescriptor { + id: vec![ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, + 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, + 0x1c, 0x1d, 0x1e, 0x1f, + ], + transports: vec![Transport::BLE, Transport::USB], + }; + + let payload = ser::to_vec(&key); + println!("payload = {:?}", payload); + let payload = payload.unwrap(); + + assert_eq!( + payload, + vec![ + 0xa3, // map(3) + 0x64, // text(4) + 0x74, 0x79, 0x70, 0x65, // "type" + 0x6a, // text(10) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, // "public-key" + 0x2D, 0x6B, 0x65, 0x79, // ... + 0x62, // text(2) + 0x69, 0x64, // "id" + 0x58, 0x20, // bytes(32) + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, // key id + 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, // ... + 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, // ... + 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, // ... + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, // ... + 0x1e, 0x1f, // ... + 0x6a, // text(10) + 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, // "transports" + 0x6f, 0x72, 0x74, 0x73, // ... + 0x82, // array(2) + 0x63, // text(3) + 0x62, 0x6c, 0x65, // "ble" + 0x63, // text(3) + 0x75, 0x73, 0x62 // "usb" + ] + ); + } +} diff --git a/src/ctap2/utils.rs b/src/ctap2/utils.rs new file mode 100644 index 00000000..ba9c7db3 --- /dev/null +++ b/src/ctap2/utils.rs @@ -0,0 +1,14 @@ +use serde::de; +use serde_cbor::error::Result; +use serde_cbor::Deserializer; + +pub fn from_slice_stream<'a, T>(slice: &'a [u8]) -> Result<(&'a [u8], T)> +where + T: de::Deserialize<'a>, +{ + let mut deserializer = Deserializer::from_slice(slice); + let value = de::Deserialize::deserialize(&mut deserializer)?; + let rest = &slice[deserializer.byte_offset()..]; + + Ok((rest, value)) +} From 0e746fbe294e89cfe45a88a601bf25e105b46861 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus <> Date: Wed, 12 May 2021 13:35:50 +0200 Subject: [PATCH 02/15] Add test for (and do some fixes) for MakeCredential CTAP1 mapping --- src/ctap2/commands/make_credentials.rs | 525 +++++++++++++++++-------- src/ctap2/commands/mod.rs | 4 + src/ctap2/server.rs | 16 +- 3 files changed, 380 insertions(+), 165 deletions(-) diff --git a/src/ctap2/commands/make_credentials.rs b/src/ctap2/commands/make_credentials.rs index 078ac8ef..8273b8cf 100644 --- a/src/ctap2/commands/make_credentials.rs +++ b/src/ctap2/commands/make_credentials.rs @@ -164,9 +164,13 @@ impl RequestCtap1 for MakeCredentials { }; let mut register_data = Vec::with_capacity(2 * PARAMETER_SIZE); - register_data.extend_from_slice(self.client_data.challenge.as_ref()); + register_data.extend_from_slice( + self.client_data + .hash() + .map_err(|e| TransportError::Command(CommandError::Json(e)))? + .as_ref(), + ); register_data.extend_from_slice(self.rp.hash().as_ref()); - let cmd = U2F_REGISTER; let apdu = U2FAPDUHeader::serialize(cmd, flags, ®ister_data)?; @@ -300,163 +304,18 @@ pub mod test { use super::MakeCredentials; use crate::ctap2::attestation::{ AAGuid, AttestationCertificate, AttestationObject, AttestationStatement, - AttestationStatementPacked, AttestedCredentialData, AuthenticatorData, - AuthenticatorDataFlags, Signature, + AttestationStatementFidoU2F, AttestationStatementPacked, AttestedCredentialData, + AuthenticatorData, AuthenticatorDataFlags, Signature, }; use crate::ctap2::client_data::{Challenge, CollectedClientData, TokenBinding, WebauthnType}; - use crate::ctap2::commands::RequestCtap2; + use crate::ctap2::commands::{RequestCtap1, RequestCtap2}; use crate::ctap2::server::RpIdHash; use crate::ctap2::server::{Alg, PublicKeyCredentialParameters, RelyingParty, User}; use crate::u2fprotocol::tests::platform::TestDevice; use serde_bytes::ByteBuf; - pub const MAKE_CREDENTIALS_SAMPLE_RESPONSE: [u8; 666] = [ - 0x00, // status = success - 0xa3, // map(3) - 0x01, // unsigned(1) - 0x66, // text(6) - 0x70, 0x61, 0x63, 0x6b, 0x65, 0x64, // "packed" - 0x02, // unsigned(2) - 0x58, 0x9a, // bytes(154) - // authData - 0xc2, 0x89, 0xc5, 0xca, 0x9b, 0x04, 0x60, 0xf9, 0x34, 0x6a, 0xb4, 0xe4, 0x2d, 0x84, - 0x27, // rp_id_hash - 0x43, 0x40, 0x4d, 0x31, 0xf4, 0x84, 0x68, 0x25, 0xa6, 0xd0, 0x65, 0xbe, 0x59, 0x7a, - 0x87, // rp_id_hash - 0x05, 0x1d, 0x41, // authData Flags - 0x00, 0x00, 0x00, 0x0b, // authData counter - 0xf8, 0xa0, 0x11, 0xf3, 0x8c, 0x0a, 0x4d, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1f, 0x9e, 0xdc, - 0x7d, // AAGUID - 0x00, 0x10, // credential id length - 0x89, 0x59, 0xce, 0xad, 0x5b, 0x5c, 0x48, 0x16, 0x4e, 0x8a, 0xbc, 0xd6, 0xd9, 0x43, 0x5c, - 0x6f, // credential id - 0xa3, 0x63, 0x61, 0x6c, 0x67, 0x65, 0x45, 0x53, 0x32, 0x35, 0x36, 0x61, 0x78, 0x58, 0x20, - 0xf7, 0xc4, 0xf4, 0xa6, 0xf1, 0xd7, 0x95, 0x38, 0xdf, 0xa4, 0xc9, 0xac, 0x50, 0x84, 0x8d, - 0xf7, 0x08, 0xbc, 0x1c, 0x99, 0xf5, 0xe6, 0x0e, 0x51, 0xb4, 0x2a, 0x52, 0x1b, 0x35, 0xd3, - 0xb6, 0x9a, 0x61, 0x79, 0x58, 0x20, 0xde, 0x7b, 0x7d, 0x6c, 0xa5, 0x64, 0xe7, 0x0e, 0xa3, - 0x21, 0xa4, 0xd5, 0xd9, 0x6e, 0xa0, 0x0e, 0xf0, 0xe2, 0xdb, 0x89, 0xdd, 0x61, 0xd4, 0x89, - 0x4c, 0x15, 0xac, 0x58, 0x5b, 0xd2, 0x36, 0x84, 0x03, // unsigned(3) - 0xa3, // map(3) - 0x63, // text(3) - 0x61, 0x6c, 0x67, // "alg" - 0x26, // -7 (ES256) - 0x63, // text(3) - 0x73, 0x69, 0x67, // "sig" - 0x58, 0x47, // bytes(71) - 0x30, 0x45, 0x02, 0x20, 0x13, 0xf7, 0x3c, 0x5d, // signature... - 0x9d, 0x53, 0x0e, 0x8c, 0xc1, 0x5c, 0xc9, 0xbd, 0x96, 0xad, 0x58, 0x6d, 0x39, 0x36, 0x64, - 0xe4, 0x62, 0xd5, 0xf0, 0x56, 0x12, 0x35, 0xe6, 0x35, 0x0f, 0x2b, 0x72, 0x89, 0x02, 0x21, - 0x00, 0x90, 0x35, 0x7f, 0xf9, 0x10, 0xcc, 0xb5, 0x6a, 0xc5, 0xb5, 0x96, 0x51, 0x19, 0x48, - 0x58, 0x1c, 0x8f, 0xdd, 0xb4, 0xa2, 0xb7, 0x99, 0x59, 0x94, 0x80, 0x78, 0xb0, 0x9f, 0x4b, - 0xdc, 0x62, 0x29, 0x63, // text(3) - 0x78, 0x35, 0x63, // "x5c" - 0x81, // array(1) - 0x59, 0x01, 0x97, // bytes(407) - 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, //certificate... - 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x09, 0x00, 0x85, 0x9b, 0x72, 0x6c, 0xb2, 0x4b, - 0x4c, 0x29, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30, - 0x47, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, - 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75, 0x62, 0x69, 0x63, - 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, 0x06, 0x03, 0x55, 0x04, 0x0b, - 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, - 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x1e, 0x17, - 0x0d, 0x31, 0x36, 0x31, 0x32, 0x30, 0x34, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x17, - 0x0d, 0x32, 0x36, 0x31, 0x32, 0x30, 0x32, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x30, - 0x47, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, - 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75, 0x62, 0x69, 0x63, - 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, 0x06, 0x03, 0x55, 0x04, 0x0b, - 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, - 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x59, 0x30, - 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, - 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xad, 0x11, 0xeb, 0x0e, 0x88, 0x52, - 0xe5, 0x3a, 0xd5, 0xdf, 0xed, 0x86, 0xb4, 0x1e, 0x61, 0x34, 0xa1, 0x8e, 0xc4, 0xe1, 0xaf, - 0x8f, 0x22, 0x1a, 0x3c, 0x7d, 0x6e, 0x63, 0x6c, 0x80, 0xea, 0x13, 0xc3, 0xd5, 0x04, 0xff, - 0x2e, 0x76, 0x21, 0x1b, 0xb4, 0x45, 0x25, 0xb1, 0x96, 0xc4, 0x4c, 0xb4, 0x84, 0x99, 0x79, - 0xcf, 0x6f, 0x89, 0x6e, 0xcd, 0x2b, 0xb8, 0x60, 0xde, 0x1b, 0xf4, 0x37, 0x6b, 0xa3, 0x0d, - 0x30, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x04, 0x02, 0x30, 0x00, 0x30, 0x0a, - 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x49, 0x00, 0x30, 0x46, - 0x02, 0x21, 0x00, 0xe9, 0xa3, 0x9f, 0x1b, 0x03, 0x19, 0x75, 0x25, 0xf7, 0x37, 0x3e, 0x10, - 0xce, 0x77, 0xe7, 0x80, 0x21, 0x73, 0x1b, 0x94, 0xd0, 0xc0, 0x3f, 0x3f, 0xda, 0x1f, 0xd2, - 0x2d, 0xb3, 0xd0, 0x30, 0xe7, 0x02, 0x21, 0x00, 0xc4, 0xfa, 0xec, 0x34, 0x45, 0xa8, 0x20, - 0xcf, 0x43, 0x12, 0x9c, 0xdb, 0x00, 0xaa, 0xbe, 0xfd, 0x9a, 0xe2, 0xd8, 0x74, 0xf9, 0xc5, - 0xd3, 0x43, 0xcb, 0x2f, 0x11, 0x3d, 0xa2, 0x37, 0x23, 0xf3, - ]; - pub const MAKE_CREDENTIALS_SAMPLE_REQUEST: [u8; 244] = [ - // 0xa5, // map(5) Replace line below with this one, once MakeCredentialOptions work - 0xa4, // map(4) - 0x01, // unsigned(1) - clientDataHash - 0x58, 0x20, // bytes(32) - 0xc1, 0xdd, 0x35, 0x5f, 0x3c, 0x81, 0x69, 0x23, 0xe0, 0x57, 0xca, 0x03, 0x8d, // hash - 0xba, 0xad, 0xb8, 0x5f, 0x95, 0x55, 0xcf, 0xc7, 0x62, 0x9b, 0x9d, 0x53, 0x66, // hash - 0x97, 0x53, 0x80, 0xd7, 0x69, 0x4f, // hash - 0x02, // unsigned(2) - rp - // 0xa2, // map(2) Replace line below with this one, once RelyingParty supports "name" - 0xa1, // map(1) - 0x62, // text(2) - 0x69, 0x64, // "id" - 0x6b, // text(11) - 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, // "example.com" - // TODO(MS): RelyingParty does not yet support optional fields "name" and "icon" - // 0x64, // text(4) - // 0x6e, 0x61, 0x6d, 0x65, // "name" - // 0x64, // text(4) - // 0x41, 0x63, 0x6d, 0x65, // "Acme" - 0x03, // unsigned(3) - user - 0xa4, // map(4) - 0x62, // text(2) - 0x69, 0x64, // "id" - 0x58, 0x20, // bytes(32) - 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, - 0x02, // userid - 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, // ... - 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, // ... - 0x64, // text(4) - 0x69, 0x63, 0x6f, 0x6e, // "icon" - 0x78, 0x2b, // text(43) - 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x70, 0x69, 0x63, 0x73, 0x2e, 0x65, - 0x78, // "https://pics.example.com/00/p/aBjjjpqPb.png" - 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x30, 0x30, 0x2f, 0x70, - 0x2f, // - 0x61, 0x42, 0x6a, 0x6a, 0x6a, 0x70, 0x71, 0x50, 0x62, 0x2e, 0x70, 0x6e, 0x67, // - 0x64, // text(4) - 0x6e, 0x61, 0x6d, 0x65, // "name" - 0x76, // text(22) - 0x6a, 0x6f, 0x68, 0x6e, 0x70, 0x73, 0x6d, 0x69, 0x74, 0x68, 0x40, 0x65, 0x78, 0x61, - 0x6d, // "johnpsmith@example.com" - 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, // ... - 0x6b, // text(11) - 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, // "displayName" - 0x6d, // text(13) - 0x4a, 0x6f, 0x68, 0x6e, 0x20, 0x50, 0x2e, 0x20, 0x53, 0x6d, 0x69, 0x74, - 0x68, // "John P. Smith" - 0x04, // unsigned(4) - pubKeyCredParams - 0x82, // array(2) - 0xa2, // map(2) - 0x63, // text(3) - 0x61, 0x6c, 0x67, // "alg" - 0x26, // -7 (ES256) - 0x64, // text(4) - 0x74, 0x79, 0x70, 0x65, // "type" - 0x6a, // text(10) - 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // "public-key" - 0xa2, // map(2) - 0x63, // text(3) - 0x61, 0x6c, 0x67, // "alg" - 0x39, 0x01, 0x00, // -257 (RS256) - 0x64, // text(4) - 0x74, 0x79, 0x70, 0x65, // "type" - 0x6a, // text(10) - 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, - 0x79, // "public-key" - // TODO(MS): Options seem to be parsed differently than in the example here. - // 0x07, // unsigned(7) - options - // 0xa1, // map(1) - // 0x62, // text(2) - // 0x72, 0x6b, // "rk" - // 0xf5, // primitive(21) - ]; #[test] - fn parse_response() { + fn test_make_credentials_ctap2() { let req = MakeCredentials::new( CollectedClientData { webauthn_type: WebauthnType::Create, @@ -467,6 +326,8 @@ pub mod test { }, RelyingParty { id: String::from("example.com"), + name: None, + icon: None, }, Some(User { id: base64::decode_config( @@ -495,9 +356,9 @@ pub mod test { let req_serialized = req .wire_format(&mut device) .expect("Failed to serialize MakeCredentials request"); - assert_eq!(req_serialized, MAKE_CREDENTIALS_SAMPLE_REQUEST); + assert_eq!(req_serialized, MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP2); let (attestation_object, _collected_client_data) = req - .handle_response_ctap2(&mut device, &MAKE_CREDENTIALS_SAMPLE_RESPONSE) + .handle_response_ctap2(&mut device, &MAKE_CREDENTIALS_SAMPLE_RESPONSE_CTAP2) .expect("Failed to handle CTAP2 response"); let expected = AttestationObject { @@ -570,15 +431,361 @@ pub mod test { }), }; + assert_eq!(attestation_object, expected); + } + + #[test] + fn test_make_credentials_ctap1() { + let req = MakeCredentials::new( + CollectedClientData { + webauthn_type: WebauthnType::Create, + challenge: Challenge::from(vec![0x00, 0x01, 0x02, 0x03]), + origin: String::from("example.com"), + cross_origin: None, + token_binding: Some(TokenBinding::Present(vec![0x00, 0x01, 0x02, 0x03])), + }, + RelyingParty { + id: String::from("example.com"), + name: Some(String::from("Acme")), + icon: None, + }, + Some(User { + id: base64::decode_config( + "MIIBkzCCATigAwIBAjCCAZMwggE4oAMCAQIwggGTMII=", + base64::URL_SAFE_NO_PAD, + ) + .unwrap(), + icon: Some("https://pics.example.com/00/p/aBjjjpqPb.png".to_string()), + name: String::from("johnpsmith@example.com"), + display_name: Some(String::from("John P. Smith")), + }), + vec![ + PublicKeyCredentialParameters { alg: Alg::ES256 }, + PublicKeyCredentialParameters { alg: Alg::RS256 }, + ], + Vec::new(), + None, + // Some(MakeCredentialsOptions { + // resident_key: true, + // user_validation: false, + // }), + None, + ); + + let mut device = TestDevice::new(); // not really used (all functions ignore it) + let req_serialized = req + .apdu_format(&mut device) + .expect("Failed to serialize MakeCredentials request"); assert_eq!( - &attestation_object.auth_data.rp_id_hash.0, - &[ - 0xc2, 0x89, 0xc5, 0xca, 0x9b, 0x04, 0x60, 0xf9, 0x34, 0x6a, 0xb4, 0xe4, 0x2d, 0x84, - 0x27, 0x43, 0x40, 0x4d, 0x31, 0xf4, 0x84, 0x68, 0x25, 0xa6, 0xd0, 0x65, 0xbe, 0x59, - 0x7a, 0x87, 0x5, 0x1d - ] + req_serialized, MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP1, + "\nGot: {:X?}\nExpected: {:X?}", + req_serialized, MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP1 ); + let (attestation_object, _collected_client_data) = req + .handle_response_ctap1(Ok(()), &MAKE_CREDENTIALS_SAMPLE_RESPONSE_CTAP1) + .expect("Failed to handle CTAP1 response"); + + let expected = AttestationObject { + auth_data: AuthenticatorData { + rp_id_hash: RpIdHash::from(&[ + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, + 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, + 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, + ]) + .unwrap(), + flags: AuthenticatorDataFlags::empty(), + counter: 0, + credential_data: Some(AttestedCredentialData { + aaguid: AAGuid::default(), + credential_id: vec![ + 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, + 0x35, 0xEF, 0xAA, 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, + 0x71, 0x7D, 0xA4, 0x85, 0x34, 0xC8, 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, + 0x5F, 0x50, 0xB5, 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD, 0x39, 0x6B, 0x64, + 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08, + 0xFE, 0x42, 0x00, 0x38, + ], + }), + extensions: Vec::new(), + }, + att_statement: AttestationStatement::FidoU2F(AttestationStatementFidoU2F { + sig: Signature(ByteBuf::from([])), + attestation_cert: vec![AttestationCertificate(vec![])], + // TODO(MS): Once handle_response_ctap1() can parse signature and certs, uncomment below + // sig: Signature(ByteBuf::from([ + // 0x30, 0x45, 0x02, 0x20, 0x13, 0xf7, 0x3c, 0x5d, 0x9d, 0x53, 0x0e, 0x8c, 0xc1, + // 0x5c, 0xc9, 0xbd, 0x96, 0xad, 0x58, 0x6d, 0x39, 0x36, 0x64, 0xe4, 0x62, 0xd5, + // 0xf0, 0x56, 0x12, 0x35, 0xe6, 0x35, 0x0f, 0x2b, 0x72, 0x89, 0x02, 0x21, 0x00, + // 0x90, 0x35, 0x7f, 0xf9, 0x10, 0xcc, 0xb5, 0x6a, 0xc5, 0xb5, 0x96, 0x51, 0x19, + // 0x48, 0x58, 0x1c, 0x8f, 0xdd, 0xb4, 0xa2, 0xb7, 0x99, 0x59, 0x94, 0x80, 0x78, + // 0xb0, 0x9f, 0x4b, 0xdc, 0x62, 0x29, + // ])), + // attestation_cert: vec![AttestationCertificate(vec![ + // 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, + // 0x02, 0x09, 0x00, 0x85, 0x9b, 0x72, 0x6c, 0xb2, 0x4b, 0x4c, 0x29, 0x30, 0x0a, + // 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30, 0x47, 0x31, + // 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, + // 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75, 0x62, + // 0x69, 0x63, 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, 0x06, + // 0x03, 0x55, 0x04, 0x0b, 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, + // 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, + // 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x1e, 0x17, 0x0d, 0x31, 0x36, 0x31, 0x32, + // 0x30, 0x34, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x17, 0x0d, 0x32, 0x36, + // 0x31, 0x32, 0x30, 0x32, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x30, 0x47, + // 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, + // 0x31, 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75, + // 0x62, 0x69, 0x63, 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, + // 0x06, 0x03, 0x55, 0x04, 0x0b, 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, + // 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, + // 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, + // 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, + // 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xad, 0x11, 0xeb, 0x0e, 0x88, 0x52, + // 0xe5, 0x3a, 0xd5, 0xdf, 0xed, 0x86, 0xb4, 0x1e, 0x61, 0x34, 0xa1, 0x8e, 0xc4, + // 0xe1, 0xaf, 0x8f, 0x22, 0x1a, 0x3c, 0x7d, 0x6e, 0x63, 0x6c, 0x80, 0xea, 0x13, + // 0xc3, 0xd5, 0x04, 0xff, 0x2e, 0x76, 0x21, 0x1b, 0xb4, 0x45, 0x25, 0xb1, 0x96, + // 0xc4, 0x4c, 0xb4, 0x84, 0x99, 0x79, 0xcf, 0x6f, 0x89, 0x6e, 0xcd, 0x2b, 0xb8, + // 0x60, 0xde, 0x1b, 0xf4, 0x37, 0x6b, 0xa3, 0x0d, 0x30, 0x0b, 0x30, 0x09, 0x06, + // 0x03, 0x55, 0x1d, 0x13, 0x04, 0x02, 0x30, 0x00, 0x30, 0x0a, 0x06, 0x08, 0x2a, + // 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x49, 0x00, 0x30, 0x46, 0x02, + // 0x21, 0x00, 0xe9, 0xa3, 0x9f, 0x1b, 0x03, 0x19, 0x75, 0x25, 0xf7, 0x37, 0x3e, + // 0x10, 0xce, 0x77, 0xe7, 0x80, 0x21, 0x73, 0x1b, 0x94, 0xd0, 0xc0, 0x3f, 0x3f, + // 0xda, 0x1f, 0xd2, 0x2d, 0xb3, 0xd0, 0x30, 0xe7, 0x02, 0x21, 0x00, 0xc4, 0xfa, + // 0xec, 0x34, 0x45, 0xa8, 0x20, 0xcf, 0x43, 0x12, 0x9c, 0xdb, 0x00, 0xaa, 0xbe, + // 0xfd, 0x9a, 0xe2, 0xd8, 0x74, 0xf9, 0xc5, 0xd3, 0x43, 0xcb, 0x2f, 0x11, 0x3d, + // 0xa2, 0x37, 0x23, 0xf3, + // ])], + }), + }; assert_eq!(attestation_object, expected); } + + #[rustfmt::skip] + pub const MAKE_CREDENTIALS_SAMPLE_RESPONSE_CTAP2: [u8; 666] = [ + 0x00, // status = success + 0xa3, // map(3) + 0x01, // unsigned(1) + 0x66, // text(6) + 0x70, 0x61, 0x63, 0x6b, 0x65, 0x64, // "packed" + 0x02, // unsigned(2) + 0x58, 0x9a, // bytes(154) + // authData + 0xc2, 0x89, 0xc5, 0xca, 0x9b, 0x04, 0x60, 0xf9, 0x34, 0x6a, 0xb4, 0xe4, 0x2d, 0x84, 0x27, // rp_id_hash + 0x43, 0x40, 0x4d, 0x31, 0xf4, 0x84, 0x68, 0x25, 0xa6, 0xd0, 0x65, 0xbe, 0x59, 0x7a, 0x87, // rp_id_hash + 0x05, 0x1d, // rp_id_hash + 0x41, // authData Flags + 0x00, 0x00, 0x00, 0x0b, // authData counter + 0xf8, 0xa0, 0x11, 0xf3, 0x8c, 0x0a, 0x4d, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1f, 0x9e, 0xdc, 0x7d, // AAGUID + 0x00, 0x10, // credential id length + 0x89, 0x59, 0xce, 0xad, 0x5b, 0x5c, 0x48, 0x16, 0x4e, 0x8a, 0xbc, 0xd6, 0xd9, 0x43, 0x5c, 0x6f, // credential id + 0xa3, 0x63, 0x61, 0x6c, 0x67, 0x65, 0x45, 0x53, 0x32, 0x35, 0x36, 0x61, 0x78, 0x58, 0x20, // credential public key + 0xf7, 0xc4, 0xf4, 0xa6, 0xf1, 0xd7, 0x95, 0x38, 0xdf, 0xa4, 0xc9, 0xac, 0x50, 0x84, 0x8d, + 0xf7, 0x08, 0xbc, 0x1c, 0x99, 0xf5, 0xe6, 0x0e, 0x51, 0xb4, 0x2a, 0x52, 0x1b, 0x35, 0xd3, + 0xb6, 0x9a, 0x61, 0x79, 0x58, 0x20, 0xde, 0x7b, 0x7d, 0x6c, 0xa5, 0x64, 0xe7, 0x0e, 0xa3, + 0x21, 0xa4, 0xd5, 0xd9, 0x6e, 0xa0, 0x0e, 0xf0, 0xe2, 0xdb, 0x89, 0xdd, 0x61, 0xd4, 0x89, + 0x4c, 0x15, 0xac, 0x58, 0x5b, 0xd2, 0x36, 0x84, + 0x03, // unsigned(3) + 0xa3, // map(3) + 0x63, // text(3) + 0x61, 0x6c, 0x67, // "alg" + 0x26, // -7 (ES256) + 0x63, // text(3) + 0x73, 0x69, 0x67, // "sig" + 0x58, 0x47, // bytes(71) + 0x30, 0x45, 0x02, 0x20, 0x13, 0xf7, 0x3c, 0x5d, 0x9d, 0x53, 0x0e, 0x8c, 0xc1, 0x5c, 0xc9, // signature + 0xbd, 0x96, 0xad, 0x58, 0x6d, 0x39, 0x36, 0x64, 0xe4, 0x62, 0xd5, 0xf0, 0x56, 0x12, 0x35, // .. + 0xe6, 0x35, 0x0f, 0x2b, 0x72, 0x89, 0x02, 0x21, 0x00, 0x90, 0x35, 0x7f, 0xf9, 0x10, 0xcc, // .. + 0xb5, 0x6a, 0xc5, 0xb5, 0x96, 0x51, 0x19, 0x48, 0x58, 0x1c, 0x8f, 0xdd, 0xb4, 0xa2, 0xb7, // .. + 0x99, 0x59, 0x94, 0x80, 0x78, 0xb0, 0x9f, 0x4b, 0xdc, 0x62, 0x29, // .. + 0x63, // text(3) + 0x78, 0x35, 0x63, // "x5c" + 0x81, // array(1) + 0x59, 0x01, 0x97, // bytes(407) + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, //certificate... + 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x09, 0x00, 0x85, 0x9b, 0x72, 0x6c, 0xb2, 0x4b, + 0x4c, 0x29, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30, + 0x47, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, + 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75, 0x62, 0x69, 0x63, + 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, 0x06, 0x03, 0x55, 0x04, 0x0b, + 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, + 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x1e, 0x17, + 0x0d, 0x31, 0x36, 0x31, 0x32, 0x30, 0x34, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x17, + 0x0d, 0x32, 0x36, 0x31, 0x32, 0x30, 0x32, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x30, + 0x47, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, + 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75, 0x62, 0x69, 0x63, + 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, 0x06, 0x03, 0x55, 0x04, 0x0b, + 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, + 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x59, 0x30, + 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, + 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xad, 0x11, 0xeb, 0x0e, 0x88, 0x52, + 0xe5, 0x3a, 0xd5, 0xdf, 0xed, 0x86, 0xb4, 0x1e, 0x61, 0x34, 0xa1, 0x8e, 0xc4, 0xe1, 0xaf, + 0x8f, 0x22, 0x1a, 0x3c, 0x7d, 0x6e, 0x63, 0x6c, 0x80, 0xea, 0x13, 0xc3, 0xd5, 0x04, 0xff, + 0x2e, 0x76, 0x21, 0x1b, 0xb4, 0x45, 0x25, 0xb1, 0x96, 0xc4, 0x4c, 0xb4, 0x84, 0x99, 0x79, + 0xcf, 0x6f, 0x89, 0x6e, 0xcd, 0x2b, 0xb8, 0x60, 0xde, 0x1b, 0xf4, 0x37, 0x6b, 0xa3, 0x0d, + 0x30, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x04, 0x02, 0x30, 0x00, 0x30, 0x0a, + 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x49, 0x00, 0x30, 0x46, + 0x02, 0x21, 0x00, 0xe9, 0xa3, 0x9f, 0x1b, 0x03, 0x19, 0x75, 0x25, 0xf7, 0x37, 0x3e, 0x10, + 0xce, 0x77, 0xe7, 0x80, 0x21, 0x73, 0x1b, 0x94, 0xd0, 0xc0, 0x3f, 0x3f, 0xda, 0x1f, 0xd2, + 0x2d, 0xb3, 0xd0, 0x30, 0xe7, 0x02, 0x21, 0x00, 0xc4, 0xfa, 0xec, 0x34, 0x45, 0xa8, 0x20, + 0xcf, 0x43, 0x12, 0x9c, 0xdb, 0x00, 0xaa, 0xbe, 0xfd, 0x9a, 0xe2, 0xd8, 0x74, 0xf9, 0xc5, + 0xd3, 0x43, 0xcb, 0x2f, 0x11, 0x3d, 0xa2, 0x37, 0x23, 0xf3, + ]; + + #[rustfmt::skip] + pub const MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP2: [u8; 244] = [ + // NOTE: This has been taken from CTAP2.0 spec, but the clientDataHash has been replaced + // to be able to operate with known values for CollectedClientData (spec doesn't say + // what values led to the provided example hash) + // 0xa5, // map(5) Replace line below with this one, once MakeCredentialOptions work + 0xa4, // map(4) + 0x01, // unsigned(1) - clientDataHash + 0x58, 0x20, // bytes(32) + 0xc1, 0xdd, 0x35, 0x5f, 0x3c, 0x81, 0x69, 0x23, 0xe0, 0x57, 0xca, 0x03, 0x8d, // hash + 0xba, 0xad, 0xb8, 0x5f, 0x95, 0x55, 0xcf, 0xc7, 0x62, 0x9b, 0x9d, 0x53, 0x66, // hash + 0x97, 0x53, 0x80, 0xd7, 0x69, 0x4f, // hash + 0x02, // unsigned(2) - rp + // 0xa2, // map(2) Replace line below with this one, once RelyingParty supports "name" + 0xa1, // map(1) + 0x62, // text(2) + 0x69, 0x64, // "id" + 0x6b, // text(11) + 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, // "example.com" + // TODO(MS): RelyingParty does not yet support optional fields "name" and "icon" + // 0x64, // text(4) + // 0x6e, 0x61, 0x6d, 0x65, // "name" + // 0x64, // text(4) + // 0x41, 0x63, 0x6d, 0x65, // "Acme" + 0x03, // unsigned(3) - user + 0xa4, // map(4) + 0x62, // text(2) + 0x69, 0x64, // "id" + 0x58, 0x20, // bytes(32) + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, // userid + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, // ... + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, // ... + 0x64, // text(4) + 0x69, 0x63, 0x6f, 0x6e, // "icon" + 0x78, 0x2b, // text(43) + 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, // "https://pics.example.com/00/p/aBjjjpqPb.png" + 0x2f, 0x70, 0x69, 0x63, 0x73, 0x2e, 0x65, 0x78, // .. + 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x30, 0x30, 0x2f, 0x70, // .. + 0x2f, 0x61, 0x42, 0x6a, 0x6a, 0x6a, 0x70, 0x71, 0x50, 0x62, 0x2e, 0x70, 0x6e, 0x67, // .. + 0x64, // text(4) + 0x6e, 0x61, 0x6d, 0x65, // "name" + 0x76, // text(22) + 0x6a, 0x6f, 0x68, 0x6e, 0x70, 0x73, 0x6d, 0x69, 0x74, // "johnpsmith@example.com" + 0x68, 0x40, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, // ... + 0x6b, // text(11) + 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, // "displayName" + 0x6d, // text(13) + 0x4a, 0x6f, 0x68, 0x6e, 0x20, 0x50, 0x2e, 0x20, 0x53, 0x6d, 0x69, 0x74, 0x68, // "John P. Smith" + 0x04, // unsigned(4) - pubKeyCredParams + 0x82, // array(2) + 0xa2, // map(2) + 0x63, // text(3) + 0x61, 0x6c, 0x67, // "alg" + 0x26, // -7 (ES256) + 0x64, // text(4) + 0x74, 0x79, 0x70, 0x65, // "type" + 0x6a, // text(10) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // "public-key" + 0xa2, // map(2) + 0x63, // text(3) + 0x61, 0x6c, 0x67, // "alg" + 0x39, 0x01, 0x00, // -257 (RS256) + 0x64, // text(4) + 0x74, 0x79, 0x70, 0x65, // "type" + 0x6a, // text(10) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // "public-key" + // TODO(MS): Options seem to be parsed differently than in the example here. + // 0x07, // unsigned(7) - options + // 0xa1, // map(1) + // 0x62, // text(2) + // 0x72, 0x6b, // "rk" + // 0xf5, // primitive(21) + ]; + + pub const MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP1: [u8; 73] = [ + // CBOR Header + 0x0, // leading zero + 0x1, // CMD U2F_Register + 0x0, // Flags + 0x0, 0x0, // zero bits + 0x0, 0x40, // size + // NOTE: This has been taken from CTAP2.0 spec, but the clientDataHash has been replaced + // to be able to operate with known values for CollectedClientData (spec doesn't say + // what values led to the provided example hash) + // clientDataHash: + 0xc1, 0xdd, 0x35, 0x5f, 0x3c, 0x81, 0x69, 0x23, 0xe0, 0x57, 0xca, 0x03, 0x8d, // hash + 0xba, 0xad, 0xb8, 0x5f, 0x95, 0x55, 0xcf, 0xc7, 0x62, 0x9b, 0x9d, 0x53, 0x66, // hash + 0x97, 0x53, 0x80, 0xd7, 0x69, 0x4f, // hash + // rpIdHash: + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, 0x34, 0xE2, + 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, 0x12, 0x55, 0x86, 0xCE, + 0x19, 0x47, 0x0, 0x0, // 2 trailing zeros from protocol + ]; + + pub const MAKE_CREDENTIALS_SAMPLE_RESPONSE_CTAP1: [u8; 792] = [ + 0x05, // Reserved Byte (1 Byte) + // User Public Key (65 Bytes) + 0x04, 0xE8, 0x76, 0x25, 0x89, 0x6E, 0xE4, 0xE4, 0x6D, 0xC0, 0x32, 0x76, 0x6E, 0x80, 0x87, + 0x96, 0x2F, 0x36, 0xDF, 0x9D, 0xFE, 0x8B, 0x56, 0x7F, 0x37, 0x63, 0x01, 0x5B, 0x19, 0x90, + 0xA6, 0x0E, 0x14, 0x27, 0xDE, 0x61, 0x2D, 0x66, 0x41, 0x8B, 0xDA, 0x19, 0x50, 0x58, 0x1E, + 0xBC, 0x5C, 0x8C, 0x1D, 0xAD, 0x71, 0x0C, 0xB1, 0x4C, 0x22, 0xF8, 0xC9, 0x70, 0x45, 0xF4, + 0x61, 0x2F, 0xB2, 0x0C, 0x91, // ... + 0x40, // Key Handle Length (1 Byte) + // Key Handle (Key Handle Length Bytes) + 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35, 0xEF, 0xAA, + 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D, 0xA4, 0x85, 0x34, 0xC8, + 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5, 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD, + 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08, + 0xFE, 0x42, 0x00, 0x38, // ... + // X.509 Cert (Variable length Cert) + 0x30, 0x82, 0x02, 0x4A, 0x30, 0x82, 0x01, 0x32, 0xA0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x04, + 0x04, 0x6C, 0x88, 0x22, 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, + 0x01, 0x0B, 0x05, 0x00, 0x30, 0x2E, 0x31, 0x2C, 0x30, 0x2A, 0x06, 0x03, 0x55, 0x04, 0x03, + 0x13, 0x23, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6F, 0x20, 0x55, 0x32, 0x46, 0x20, 0x52, 0x6F, + 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6C, 0x20, 0x34, 0x35, + 0x37, 0x32, 0x30, 0x30, 0x36, 0x33, 0x31, 0x30, 0x20, 0x17, 0x0D, 0x31, 0x34, 0x30, 0x38, + 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5A, 0x18, 0x0F, 0x32, 0x30, 0x35, 0x30, + 0x30, 0x39, 0x30, 0x34, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5A, 0x30, 0x2C, 0x31, 0x2A, + 0x30, 0x28, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C, 0x21, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6F, + 0x20, 0x55, 0x32, 0x46, 0x20, 0x45, 0x45, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6C, 0x20, + 0x32, 0x34, 0x39, 0x31, 0x38, 0x32, 0x33, 0x32, 0x34, 0x37, 0x37, 0x30, 0x30, 0x59, 0x30, + 0x13, 0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01, 0x06, 0x08, 0x2A, 0x86, 0x48, + 0xCE, 0x3D, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0x3C, 0xCA, 0xB9, 0x2C, 0xCB, 0x97, + 0x28, 0x7E, 0xE8, 0xE6, 0x39, 0x43, 0x7E, 0x21, 0xFC, 0xD6, 0xB6, 0xF1, 0x65, 0xB2, 0xD5, + 0xA3, 0xF3, 0xDB, 0x13, 0x1D, 0x31, 0xC1, 0x6B, 0x74, 0x2B, 0xB4, 0x76, 0xD8, 0xD1, 0xE9, + 0x90, 0x80, 0xEB, 0x54, 0x6C, 0x9B, 0xBD, 0xF5, 0x56, 0xE6, 0x21, 0x0F, 0xD4, 0x27, 0x85, + 0x89, 0x9E, 0x78, 0xCC, 0x58, 0x9E, 0xBE, 0x31, 0x0F, 0x6C, 0xDB, 0x9F, 0xF4, 0xA3, 0x3B, + 0x30, 0x39, 0x30, 0x22, 0x06, 0x09, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0xC4, 0x0A, 0x02, + 0x04, 0x15, 0x31, 0x2E, 0x33, 0x2E, 0x36, 0x2E, 0x31, 0x2E, 0x34, 0x2E, 0x31, 0x2E, 0x34, + 0x31, 0x34, 0x38, 0x32, 0x2E, 0x31, 0x2E, 0x32, 0x30, 0x13, 0x06, 0x0B, 0x2B, 0x06, 0x01, + 0x04, 0x01, 0x82, 0xE5, 0x1C, 0x02, 0x01, 0x01, 0x04, 0x04, 0x03, 0x02, 0x04, 0x30, 0x30, + 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B, 0x05, 0x00, 0x03, + 0x82, 0x01, 0x01, 0x00, 0x9F, 0x9B, 0x05, 0x22, 0x48, 0xBC, 0x4C, 0xF4, 0x2C, 0xC5, 0x99, + 0x1F, 0xCA, 0xAB, 0xAC, 0x9B, 0x65, 0x1B, 0xBE, 0x5B, 0xDC, 0xDC, 0x8E, 0xF0, 0xAD, 0x2C, + 0x1C, 0x1F, 0xFB, 0x36, 0xD1, 0x87, 0x15, 0xD4, 0x2E, 0x78, 0xB2, 0x49, 0x22, 0x4F, 0x92, + 0xC7, 0xE6, 0xE7, 0xA0, 0x5C, 0x49, 0xF0, 0xE7, 0xE4, 0xC8, 0x81, 0xBF, 0x2E, 0x94, 0xF4, + 0x5E, 0x4A, 0x21, 0x83, 0x3D, 0x74, 0x56, 0x85, 0x1D, 0x0F, 0x6C, 0x14, 0x5A, 0x29, 0x54, + 0x0C, 0x87, 0x4F, 0x30, 0x92, 0xC9, 0x34, 0xB4, 0x3D, 0x22, 0x2B, 0x89, 0x62, 0xC0, 0xF4, + 0x10, 0xCE, 0xF1, 0xDB, 0x75, 0x89, 0x2A, 0xF1, 0x16, 0xB4, 0x4A, 0x96, 0xF5, 0xD3, 0x5A, + 0xDE, 0xA3, 0x82, 0x2F, 0xC7, 0x14, 0x6F, 0x60, 0x04, 0x38, 0x5B, 0xCB, 0x69, 0xB6, 0x5C, + 0x99, 0xE7, 0xEB, 0x69, 0x19, 0x78, 0x67, 0x03, 0xC0, 0xD8, 0xCD, 0x41, 0xE8, 0xF7, 0x5C, + 0xCA, 0x44, 0xAA, 0x8A, 0xB7, 0x25, 0xAD, 0x8E, 0x79, 0x9F, 0xF3, 0xA8, 0x69, 0x6A, 0x6F, + 0x1B, 0x26, 0x56, 0xE6, 0x31, 0xB1, 0xE4, 0x01, 0x83, 0xC0, 0x8F, 0xDA, 0x53, 0xFA, 0x4A, + 0x8F, 0x85, 0xA0, 0x56, 0x93, 0x94, 0x4A, 0xE1, 0x79, 0xA1, 0x33, 0x9D, 0x00, 0x2D, 0x15, + 0xCA, 0xBD, 0x81, 0x00, 0x90, 0xEC, 0x72, 0x2E, 0xF5, 0xDE, 0xF9, 0x96, 0x5A, 0x37, 0x1D, + 0x41, 0x5D, 0x62, 0x4B, 0x68, 0xA2, 0x70, 0x7C, 0xAD, 0x97, 0xBC, 0xDD, 0x17, 0x85, 0xAF, + 0x97, 0xE2, 0x58, 0xF3, 0x3D, 0xF5, 0x6A, 0x03, 0x1A, 0xA0, 0x35, 0x6D, 0x8E, 0x8D, 0x5E, + 0xBC, 0xAD, 0xC7, 0x4E, 0x07, 0x16, 0x36, 0xC6, 0xB1, 0x10, 0xAC, 0xE5, 0xCC, 0x9B, 0x90, + 0xDF, 0xEA, 0xCA, 0xE6, 0x40, 0xFF, 0x1B, 0xB0, 0xF1, 0xFE, 0x5D, 0xB4, 0xEF, 0xF7, 0xA9, + 0x5F, 0x06, 0x07, 0x33, 0xF5, // ... + // Signature (variable Length) + 0x30, 0x45, 0x02, 0x20, 0x32, 0x47, 0x79, 0xC6, 0x8F, 0x33, 0x80, 0x28, 0x8A, 0x11, 0x97, + 0xB6, 0x09, 0x5F, 0x7A, 0x6E, 0xB9, 0xB1, 0xB1, 0xC1, 0x27, 0xF6, 0x6A, 0xE1, 0x2A, 0x99, + 0xFE, 0x85, 0x32, 0xEC, 0x23, 0xB9, 0x02, 0x21, 0x00, 0xE3, 0x95, 0x16, 0xAC, 0x4D, 0x61, + 0xEE, 0x64, 0x04, 0x4D, 0x50, 0xB4, 0x15, 0xA6, 0xA4, 0xD4, 0xD8, 0x4B, 0xA6, 0xD8, 0x95, + 0xCB, 0x5A, 0xB7, 0xA1, 0xAA, 0x7D, 0x08, 0x1D, 0xE3, 0x41, 0xFA, // ... + ]; } diff --git a/src/ctap2/commands/mod.rs b/src/ctap2/commands/mod.rs index 704c7aaa..e982e476 100644 --- a/src/ctap2/commands/mod.rs +++ b/src/ctap2/commands/mod.rs @@ -1,6 +1,7 @@ use crate::transport::errors::{ApduErrorStatus, HIDError}; use crate::transport::FidoDevice; use serde_cbor::{error::Error as CborError, Value}; +use serde_json as json; use std::error::Error as StdErrorT; use std::fmt; use std::io::{Read, Write}; @@ -21,6 +22,7 @@ where /// Retryable wraps an error type and may ask manager to retry sending a /// command, this is useful for ctap1 where token will reply with "condition not /// sufficient" because user needs to press the button. +#[derive(Debug)] pub(crate) enum Retryable { Retry, Error(T), @@ -319,6 +321,7 @@ pub enum CommandError { Serialization(CborError), Parsing(CborError), StatusCode(StatusCode, Option), + Json(json::Error), } impl fmt::Display for CommandError { @@ -338,6 +341,7 @@ impl fmt::Display for CommandError { CommandError::StatusCode(ref code, ref value) => { write!(f, "CommandError: Unexpected code: {:?} ({:?})", code, value) } + CommandError::Json(ref e) => write!(f, "CommandError: Json serializing error: {}", e), } } } diff --git a/src/ctap2/server.rs b/src/ctap2/server.rs index 4749453a..2ace89c6 100644 --- a/src/ctap2/server.rs +++ b/src/ctap2/server.rs @@ -20,7 +20,7 @@ pub struct RpIdHash(pub [u8; 32]); impl fmt::Debug for RpIdHash { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let value = base64::encode_config(&self.0[..], base64::URL_SAFE_NO_PAD); + let value = base64::encode_config(&self.0, base64::URL_SAFE_NO_PAD); write!(f, "RpIdHash({})", value) } } @@ -51,7 +51,10 @@ pub struct RelyingParty { // in the example "A PublicKeyCredentialRpEntity DOM object defined as follows:" // inconsistent with https://w3c.github.io/webauthn/#sctn-rp-credential-params pub id: String, - // TODO(baloo): need to add name and icon fields (optionals) + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, } impl RelyingParty { @@ -62,11 +65,10 @@ impl RelyingParty { pub fn hash(&self) -> RpIdHash { let mut hasher = Sha256::new(); - hasher.input(&self.id.as_bytes()); + hasher.input(&self.id); let mut output = [0u8; 32]; - let len = output.len(); - output.copy_from_slice(&hasher.result().as_slice()[..len]); + output.copy_from_slice(&hasher.result().as_slice()); RpIdHash(output) } @@ -260,7 +262,7 @@ impl Serialize for PublicKeyCredentialDescriptor { { let mut map = serializer.serialize_map(Some(3))?; map.serialize_entry("type", "public-key")?; - map.serialize_entry("id", &ByteBuf::from(&self.id[..]))?; + map.serialize_entry("id", &ByteBuf::from(self.id.clone()))?; map.serialize_entry("transports", &self.transports)?; map.end() } @@ -357,6 +359,8 @@ mod test { fn serialize_rp() { let rp = RelyingParty { id: String::from("Acme"), + name: None, + icon: None, }; let payload = rp.to_ctap2(); From c35619e76358656cf4e113ed71e48803a7d0f7ce Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus <> Date: Wed, 12 May 2021 13:38:24 +0200 Subject: [PATCH 03/15] Adjust tests with RelyingParty and name-field --- src/ctap2/commands/make_credentials.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/ctap2/commands/make_credentials.rs b/src/ctap2/commands/make_credentials.rs index 8273b8cf..dc3019b9 100644 --- a/src/ctap2/commands/make_credentials.rs +++ b/src/ctap2/commands/make_credentials.rs @@ -326,7 +326,7 @@ pub mod test { }, RelyingParty { id: String::from("example.com"), - name: None, + name: Some(String::from("Acme")), icon: None, }, Some(User { @@ -632,7 +632,7 @@ pub mod test { ]; #[rustfmt::skip] - pub const MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP2: [u8; 244] = [ + pub const MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP2: [u8; 254] = [ // NOTE: This has been taken from CTAP2.0 spec, but the clientDataHash has been replaced // to be able to operate with known values for CollectedClientData (spec doesn't say // what values led to the provided example hash) @@ -644,17 +644,15 @@ pub mod test { 0xba, 0xad, 0xb8, 0x5f, 0x95, 0x55, 0xcf, 0xc7, 0x62, 0x9b, 0x9d, 0x53, 0x66, // hash 0x97, 0x53, 0x80, 0xd7, 0x69, 0x4f, // hash 0x02, // unsigned(2) - rp - // 0xa2, // map(2) Replace line below with this one, once RelyingParty supports "name" - 0xa1, // map(1) + 0xa2, // map(2) Replace line below with this one, once RelyingParty supports "name" 0x62, // text(2) 0x69, 0x64, // "id" 0x6b, // text(11) 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, // "example.com" - // TODO(MS): RelyingParty does not yet support optional fields "name" and "icon" - // 0x64, // text(4) - // 0x6e, 0x61, 0x6d, 0x65, // "name" - // 0x64, // text(4) - // 0x41, 0x63, 0x6d, 0x65, // "Acme" + 0x64, // text(4) + 0x6e, 0x61, 0x6d, 0x65, // "name" + 0x64, // text(4) + 0x41, 0x63, 0x6d, 0x65, // "Acme" 0x03, // unsigned(3) - user 0xa4, // map(4) 0x62, // text(2) From 26f231aacd5f9cc1b4a87f7fe3c54584ca1417f2 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus <> Date: Wed, 12 May 2021 13:51:10 +0200 Subject: [PATCH 04/15] Fix serialization of MakeCredentialOptions (individually serialize members) --- src/ctap2/commands/make_credentials.rs | 70 ++++++++++++++------------ 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/src/ctap2/commands/make_credentials.rs b/src/ctap2/commands/make_credentials.rs index dc3019b9..fd5c80a5 100644 --- a/src/ctap2/commands/make_credentials.rs +++ b/src/ctap2/commands/make_credentials.rs @@ -25,30 +25,37 @@ use std::io; #[derive(Copy, Clone, Debug, Serialize)] #[cfg_attr(test, derive(Deserialize))] pub struct MakeCredentialsOptions { - #[serde(rename = "rk")] - pub resident_key: bool, - #[serde(rename = "uv")] - pub user_validation: bool, + #[serde(rename = "rk", skip_serializing_if = "Option::is_none")] + pub resident_key: Option, + #[serde(rename = "uv", skip_serializing_if = "Option::is_none")] + pub user_validation: Option, } impl Default for MakeCredentialsOptions { fn default() -> Self { Self { - resident_key: false, - user_validation: true, + resident_key: None, + user_validation: Some(true), } } } +impl MakeCredentialsOptions { + fn has_some(&self) -> bool { + self.resident_key.is_some() || self.user_validation.is_some() + } +} + pub(crate) trait UserValidation { fn ask_user_validation(&self) -> bool; } -impl UserValidation for Option { +impl UserValidation for MakeCredentialsOptions { fn ask_user_validation(&self) -> bool { - match *self { - Some(ref e) if e.user_validation => true, - _ => false, + if let Some(e) = self.user_validation { + e + } else { + false } } } @@ -72,7 +79,7 @@ pub struct MakeCredentials { // from the client to the authenticator for authenticator extensions during // the processing of these calls. extensions: Map, - options: Option, + options: MakeCredentialsOptions, pin: Option, // TODO(MS): pin_protocol } @@ -84,7 +91,7 @@ impl MakeCredentials { user: Option, pub_cred_params: Vec, exclude_list: Vec, - options: Option, + options: MakeCredentialsOptions, pin: Option, ) -> Self { Self { @@ -115,7 +122,7 @@ impl Serialize for MakeCredentials { if !self.extensions.is_empty() { map_len += 1; } - if self.options.is_some() { + if self.options.has_some() { map_len += 1; } @@ -134,7 +141,7 @@ impl Serialize for MakeCredentials { if !self.extensions.is_empty() { map.serialize_entry(&6, &self.extensions)?; } - if self.options.is_some() { + if self.options.has_some() { map.serialize_entry(&7, &self.options)?; } map.end() @@ -301,7 +308,7 @@ impl RequestCtap2 for MakeCredentials { #[cfg(test)] pub mod test { - use super::MakeCredentials; + use super::{MakeCredentials, MakeCredentialsOptions}; use crate::ctap2::attestation::{ AAGuid, AttestationCertificate, AttestationObject, AttestationStatement, AttestationStatementFidoU2F, AttestationStatementPacked, AttestedCredentialData, @@ -344,11 +351,10 @@ pub mod test { PublicKeyCredentialParameters { alg: Alg::RS256 }, ], Vec::new(), - None, - // Some(MakeCredentialsOptions { - // resident_key: true, - // user_validation: false, - // }), + MakeCredentialsOptions { + resident_key: Some(true), + user_validation: None, + }, None, ); @@ -464,11 +470,10 @@ pub mod test { PublicKeyCredentialParameters { alg: Alg::RS256 }, ], Vec::new(), - None, - // Some(MakeCredentialsOptions { - // resident_key: true, - // user_validation: false, - // }), + MakeCredentialsOptions { + resident_key: Some(true), + user_validation: None, + }, None, ); @@ -632,12 +637,11 @@ pub mod test { ]; #[rustfmt::skip] - pub const MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP2: [u8; 254] = [ + pub const MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP2: [u8; 260] = [ // NOTE: This has been taken from CTAP2.0 spec, but the clientDataHash has been replaced // to be able to operate with known values for CollectedClientData (spec doesn't say // what values led to the provided example hash) - // 0xa5, // map(5) Replace line below with this one, once MakeCredentialOptions work - 0xa4, // map(4) + 0xa5, // map(5) 0x01, // unsigned(1) - clientDataHash 0x58, 0x20, // bytes(32) 0xc1, 0xdd, 0x35, 0x5f, 0x3c, 0x81, 0x69, 0x23, 0xe0, 0x57, 0xca, 0x03, 0x8d, // hash @@ -696,11 +700,11 @@ pub mod test { 0x6a, // text(10) 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // "public-key" // TODO(MS): Options seem to be parsed differently than in the example here. - // 0x07, // unsigned(7) - options - // 0xa1, // map(1) - // 0x62, // text(2) - // 0x72, 0x6b, // "rk" - // 0xf5, // primitive(21) + 0x07, // unsigned(7) - options + 0xa1, // map(1) + 0x62, // text(2) + 0x72, 0x6b, // "rk" + 0xf5, // primitive(21) ]; pub const MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP1: [u8; 73] = [ From f7dd900050302cb7e097c2482583977ef3aba09d Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus <> Date: Wed, 12 May 2021 16:34:39 +0200 Subject: [PATCH 05/15] Temporarily parse credential_public_key just as a byte-vector, until COSE-crate is added and able to actually parse it --- src/ctap2/attestation.rs | 18 +++++++++--------- src/ctap2/commands/make_credentials.rs | 20 +++++++++++++++++++- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/ctap2/attestation.rs b/src/ctap2/attestation.rs index 814935c1..5fc5f7cf 100644 --- a/src/ctap2/attestation.rs +++ b/src/ctap2/attestation.rs @@ -1,11 +1,10 @@ +use super::server::Alg; +use super::utils::from_slice_stream; use crate::ctap2::server::RpIdHash; -use std::fmt; -#[cfg(test)] -use std::io::{self, Write}; - #[cfg(test)] use byteorder::{BigEndian, WriteBytesExt}; use nom::{ + combinator::rest, cond, do_parse, map, map_res, named, number::complete::{be_u16, be_u32, be_u8}, take, Err as NomErr, IResult, @@ -19,11 +18,10 @@ use serde::{ ser::{Error as SerError, SerializeMap}, Serializer, }; - use serde_bytes::ByteBuf; - -use super::server::Alg; -use super::utils::from_slice_stream; +use std::fmt; +#[cfg(test)] +use std::io::{self, Write}; #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct Extension {} @@ -113,6 +111,7 @@ pub struct AttestedCredentialData { pub credential_id: Vec, // TODO(MS): unimplemented! // pub credential_public_key: crate::ctap2::commands::client_pin::PublicKey, + pub credential_public_key: Vec, } fn serde_to_nom<'a, Output>(input: &'a [u8]) -> IResult<&'a [u8], Output> @@ -132,11 +131,12 @@ named!( aaguid: map_res!(take!(16), AAGuid::from) >> cred_len: be_u16 >> credential_id: map!(take!(cred_len), Vec::from) + >> credential_public_key: rest // >> credential_public_key: serde_to_nom >> (AttestedCredentialData { aaguid, credential_id, - // credential_public_key, + credential_public_key: credential_public_key.to_vec(), }) ) ); diff --git a/src/ctap2/commands/make_credentials.rs b/src/ctap2/commands/make_credentials.rs index fd5c80a5..4f38bbb5 100644 --- a/src/ctap2/commands/make_credentials.rs +++ b/src/ctap2/commands/make_credentials.rs @@ -204,7 +204,7 @@ impl RequestCtap1 for MakeCredentials { ) ); - let (_rest, (_public_key, key_handle)) = parse_register(input) + let (_rest, (public_key, key_handle)) = parse_register(input) .map_err(|e| { error!("error while parsing registration = {:?}", e); io::Error::new(io::ErrorKind::Other, "unable to parse registration") @@ -238,6 +238,7 @@ impl RequestCtap1 for MakeCredentials { // see This is the (uncompressed) x,y-representation of a curve point on the P-256 NIST elliptic curve. // https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html //credential_public_key: PublicKey::new(EllipticCurve::P256, Vec::from(public_key)), + credential_public_key: Vec::from(public_key), }), extensions: Vec::new(), }; @@ -387,6 +388,15 @@ pub mod test { 0x89, 0x59, 0xce, 0xad, 0x5b, 0x5c, 0x48, 0x16, 0x4e, 0x8a, 0xbc, 0xd6, 0xd9, 0x43, 0x5c, 0x6f, ], + credential_public_key: vec![ + 0xa3, 0x63, 0x61, 0x6c, 0x67, 0x65, 0x45, 0x53, 0x32, 0x35, 0x36, 0x61, + 0x78, 0x58, 0x20, 0xf7, 0xc4, 0xf4, 0xa6, 0xf1, 0xd7, 0x95, 0x38, 0xdf, + 0xa4, 0xc9, 0xac, 0x50, 0x84, 0x8d, 0xf7, 0x08, 0xbc, 0x1c, 0x99, 0xf5, + 0xe6, 0x0e, 0x51, 0xb4, 0x2a, 0x52, 0x1b, 0x35, 0xd3, 0xb6, 0x9a, 0x61, + 0x79, 0x58, 0x20, 0xde, 0x7b, 0x7d, 0x6c, 0xa5, 0x64, 0xe7, 0x0e, 0xa3, + 0x21, 0xa4, 0xd5, 0xd9, 0x6e, 0xa0, 0x0e, 0xf0, 0xe2, 0xdb, 0x89, 0xdd, + 0x61, 0xd4, 0x89, 0x4c, 0x15, 0xac, 0x58, 0x5b, 0xd2, 0x36, 0x84, + ], }), extensions: Vec::new(), }, @@ -510,6 +520,14 @@ pub mod test { 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08, 0xFE, 0x42, 0x00, 0x38, ], + credential_public_key: vec![ + 0x04, 0xE8, 0x76, 0x25, 0x89, 0x6E, 0xE4, 0xE4, 0x6D, 0xC0, 0x32, 0x76, + 0x6E, 0x80, 0x87, 0x96, 0x2F, 0x36, 0xDF, 0x9D, 0xFE, 0x8B, 0x56, 0x7F, + 0x37, 0x63, 0x01, 0x5B, 0x19, 0x90, 0xA6, 0x0E, 0x14, 0x27, 0xDE, 0x61, + 0x2D, 0x66, 0x41, 0x8B, 0xDA, 0x19, 0x50, 0x58, 0x1E, 0xBC, 0x5C, 0x8C, + 0x1D, 0xAD, 0x71, 0x0C, 0xB1, 0x4C, 0x22, 0xF8, 0xC9, 0x70, 0x45, 0xF4, + 0x61, 0x2F, 0xB2, 0x0C, 0x91, + ], }), extensions: Vec::new(), }, From bf12cdb89c4713c6d40dc53746dcc51568984671 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus <> Date: Mon, 17 May 2021 13:06:51 +0200 Subject: [PATCH 06/15] Add Option Unparsed to AttestationStatement, to currently skip parsing it (which would need a DER-parser) --- src/ctap2/attestation.rs | 3 +- src/ctap2/commands/make_credentials.rs | 153 ++++++++++++++++--------- 2 files changed, 101 insertions(+), 55 deletions(-) diff --git a/src/ctap2/attestation.rs b/src/ctap2/attestation.rs index 5fc5f7cf..09fcb3a3 100644 --- a/src/ctap2/attestation.rs +++ b/src/ctap2/attestation.rs @@ -276,6 +276,7 @@ pub enum AttestationStatement { //AndroidSafetyNet, FidoU2F(AttestationStatementFidoU2F), // TODO(baloo): should we do an Unknow version? most likely + Unparsed(Vec), } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -432,7 +433,7 @@ impl Serialize for AttestationObject { map.serialize_entry(&1, &"packed")?; map.serialize_entry(&3, v)?; } - AttestationStatement::FidoU2F(_) => { + AttestationStatement::FidoU2F(_) | AttestationStatement::Unparsed(_) => { unimplemented!(); } } diff --git a/src/ctap2/commands/make_credentials.rs b/src/ctap2/commands/make_credentials.rs index 4f38bbb5..eb055ef6 100644 --- a/src/ctap2/commands/make_credentials.rs +++ b/src/ctap2/commands/make_credentials.rs @@ -1,8 +1,8 @@ use super::{Command, CommandError, Request, RequestCtap1, RequestCtap2, Retryable, StatusCode}; use crate::consts::{PARAMETER_SIZE, U2F_REGISTER, U2F_REQUEST_USER_PRESENCE}; use crate::ctap2::attestation::{ - AAGuid, AttestationObject, AttestationStatement, AttestationStatementFidoU2F, - AttestedCredentialData, AuthenticatorData, AuthenticatorDataFlags, + AAGuid, AttestationObject, AttestationStatement, AttestedCredentialData, AuthenticatorData, + AuthenticatorDataFlags, }; use crate::ctap2::client_data::CollectedClientData; use crate::ctap2::server::{ @@ -204,7 +204,7 @@ impl RequestCtap1 for MakeCredentials { ) ); - let (_rest, (public_key, key_handle)) = parse_register(input) + let (rest, (public_key, key_handle)) = parse_register(input) .map_err(|e| { error!("error while parsing registration = {:?}", e); io::Error::new(io::ErrorKind::Other, "unable to parse registration") @@ -245,8 +245,7 @@ impl RequestCtap1 for MakeCredentials { // TODO(MS) // let att_statement_u2f = AttestationStatementFidoU2F::new(cert, signature); - let att_statement_u2f = AttestationStatementFidoU2F::new(&[], &[]); - let att_statement = AttestationStatement::FidoU2F(att_statement_u2f); + let att_statement = AttestationStatement::Unparsed(rest.to_vec()); let attestation_object = AttestationObject { auth_data, att_statement, @@ -312,8 +311,8 @@ pub mod test { use super::{MakeCredentials, MakeCredentialsOptions}; use crate::ctap2::attestation::{ AAGuid, AttestationCertificate, AttestationObject, AttestationStatement, - AttestationStatementFidoU2F, AttestationStatementPacked, AttestedCredentialData, - AuthenticatorData, AuthenticatorDataFlags, Signature, + AttestationStatementPacked, AttestedCredentialData, AuthenticatorData, + AuthenticatorDataFlags, Signature, }; use crate::ctap2::client_data::{Challenge, CollectedClientData, TokenBinding, WebauthnType}; use crate::ctap2::commands::{RequestCtap1, RequestCtap2}; @@ -531,53 +530,99 @@ pub mod test { }), extensions: Vec::new(), }, - att_statement: AttestationStatement::FidoU2F(AttestationStatementFidoU2F { - sig: Signature(ByteBuf::from([])), - attestation_cert: vec![AttestationCertificate(vec![])], - // TODO(MS): Once handle_response_ctap1() can parse signature and certs, uncomment below - // sig: Signature(ByteBuf::from([ - // 0x30, 0x45, 0x02, 0x20, 0x13, 0xf7, 0x3c, 0x5d, 0x9d, 0x53, 0x0e, 0x8c, 0xc1, - // 0x5c, 0xc9, 0xbd, 0x96, 0xad, 0x58, 0x6d, 0x39, 0x36, 0x64, 0xe4, 0x62, 0xd5, - // 0xf0, 0x56, 0x12, 0x35, 0xe6, 0x35, 0x0f, 0x2b, 0x72, 0x89, 0x02, 0x21, 0x00, - // 0x90, 0x35, 0x7f, 0xf9, 0x10, 0xcc, 0xb5, 0x6a, 0xc5, 0xb5, 0x96, 0x51, 0x19, - // 0x48, 0x58, 0x1c, 0x8f, 0xdd, 0xb4, 0xa2, 0xb7, 0x99, 0x59, 0x94, 0x80, 0x78, - // 0xb0, 0x9f, 0x4b, 0xdc, 0x62, 0x29, - // ])), - // attestation_cert: vec![AttestationCertificate(vec![ - // 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, - // 0x02, 0x09, 0x00, 0x85, 0x9b, 0x72, 0x6c, 0xb2, 0x4b, 0x4c, 0x29, 0x30, 0x0a, - // 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30, 0x47, 0x31, - // 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, - // 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75, 0x62, - // 0x69, 0x63, 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, 0x06, - // 0x03, 0x55, 0x04, 0x0b, 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, - // 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, - // 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x1e, 0x17, 0x0d, 0x31, 0x36, 0x31, 0x32, - // 0x30, 0x34, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x17, 0x0d, 0x32, 0x36, - // 0x31, 0x32, 0x30, 0x32, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x30, 0x47, - // 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, - // 0x31, 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75, - // 0x62, 0x69, 0x63, 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, - // 0x06, 0x03, 0x55, 0x04, 0x0b, 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, - // 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, - // 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, - // 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, - // 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xad, 0x11, 0xeb, 0x0e, 0x88, 0x52, - // 0xe5, 0x3a, 0xd5, 0xdf, 0xed, 0x86, 0xb4, 0x1e, 0x61, 0x34, 0xa1, 0x8e, 0xc4, - // 0xe1, 0xaf, 0x8f, 0x22, 0x1a, 0x3c, 0x7d, 0x6e, 0x63, 0x6c, 0x80, 0xea, 0x13, - // 0xc3, 0xd5, 0x04, 0xff, 0x2e, 0x76, 0x21, 0x1b, 0xb4, 0x45, 0x25, 0xb1, 0x96, - // 0xc4, 0x4c, 0xb4, 0x84, 0x99, 0x79, 0xcf, 0x6f, 0x89, 0x6e, 0xcd, 0x2b, 0xb8, - // 0x60, 0xde, 0x1b, 0xf4, 0x37, 0x6b, 0xa3, 0x0d, 0x30, 0x0b, 0x30, 0x09, 0x06, - // 0x03, 0x55, 0x1d, 0x13, 0x04, 0x02, 0x30, 0x00, 0x30, 0x0a, 0x06, 0x08, 0x2a, - // 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x49, 0x00, 0x30, 0x46, 0x02, - // 0x21, 0x00, 0xe9, 0xa3, 0x9f, 0x1b, 0x03, 0x19, 0x75, 0x25, 0xf7, 0x37, 0x3e, - // 0x10, 0xce, 0x77, 0xe7, 0x80, 0x21, 0x73, 0x1b, 0x94, 0xd0, 0xc0, 0x3f, 0x3f, - // 0xda, 0x1f, 0xd2, 0x2d, 0xb3, 0xd0, 0x30, 0xe7, 0x02, 0x21, 0x00, 0xc4, 0xfa, - // 0xec, 0x34, 0x45, 0xa8, 0x20, 0xcf, 0x43, 0x12, 0x9c, 0xdb, 0x00, 0xaa, 0xbe, - // 0xfd, 0x9a, 0xe2, 0xd8, 0x74, 0xf9, 0xc5, 0xd3, 0x43, 0xcb, 0x2f, 0x11, 0x3d, - // 0xa2, 0x37, 0x23, 0xf3, - // ])], - }), + att_statement: AttestationStatement::Unparsed(vec![ + 0x30, 0x82, 0x02, 0x4A, 0x30, 0x82, 0x01, 0x32, 0xA0, 0x03, 0x02, 0x01, 0x02, 0x02, + 0x04, 0x04, 0x6C, 0x88, 0x22, 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, + 0x0D, 0x01, 0x01, 0x0B, 0x05, 0x00, 0x30, 0x2E, 0x31, 0x2C, 0x30, 0x2A, 0x06, 0x03, + 0x55, 0x04, 0x03, 0x13, 0x23, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6F, 0x20, 0x55, 0x32, + 0x46, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x53, 0x65, 0x72, 0x69, + 0x61, 0x6C, 0x20, 0x34, 0x35, 0x37, 0x32, 0x30, 0x30, 0x36, 0x33, 0x31, 0x30, 0x20, + 0x17, 0x0D, 0x31, 0x34, 0x30, 0x38, 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x5A, 0x18, 0x0F, 0x32, 0x30, 0x35, 0x30, 0x30, 0x39, 0x30, 0x34, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x5A, 0x30, 0x2C, 0x31, 0x2A, 0x30, 0x28, 0x06, 0x03, 0x55, 0x04, + 0x03, 0x0C, 0x21, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6F, 0x20, 0x55, 0x32, 0x46, 0x20, + 0x45, 0x45, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6C, 0x20, 0x32, 0x34, 0x39, 0x31, + 0x38, 0x32, 0x33, 0x32, 0x34, 0x37, 0x37, 0x30, 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, + 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01, 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, + 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0x3C, 0xCA, 0xB9, 0x2C, 0xCB, 0x97, 0x28, + 0x7E, 0xE8, 0xE6, 0x39, 0x43, 0x7E, 0x21, 0xFC, 0xD6, 0xB6, 0xF1, 0x65, 0xB2, 0xD5, + 0xA3, 0xF3, 0xDB, 0x13, 0x1D, 0x31, 0xC1, 0x6B, 0x74, 0x2B, 0xB4, 0x76, 0xD8, 0xD1, + 0xE9, 0x90, 0x80, 0xEB, 0x54, 0x6C, 0x9B, 0xBD, 0xF5, 0x56, 0xE6, 0x21, 0x0F, 0xD4, + 0x27, 0x85, 0x89, 0x9E, 0x78, 0xCC, 0x58, 0x9E, 0xBE, 0x31, 0x0F, 0x6C, 0xDB, 0x9F, + 0xF4, 0xA3, 0x3B, 0x30, 0x39, 0x30, 0x22, 0x06, 0x09, 0x2B, 0x06, 0x01, 0x04, 0x01, + 0x82, 0xC4, 0x0A, 0x02, 0x04, 0x15, 0x31, 0x2E, 0x33, 0x2E, 0x36, 0x2E, 0x31, 0x2E, + 0x34, 0x2E, 0x31, 0x2E, 0x34, 0x31, 0x34, 0x38, 0x32, 0x2E, 0x31, 0x2E, 0x32, 0x30, + 0x13, 0x06, 0x0B, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0xE5, 0x1C, 0x02, 0x01, 0x01, + 0x04, 0x04, 0x03, 0x02, 0x04, 0x30, 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, + 0xF7, 0x0D, 0x01, 0x01, 0x0B, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, 0x00, 0x9F, 0x9B, + 0x05, 0x22, 0x48, 0xBC, 0x4C, 0xF4, 0x2C, 0xC5, 0x99, 0x1F, 0xCA, 0xAB, 0xAC, 0x9B, + 0x65, 0x1B, 0xBE, 0x5B, 0xDC, 0xDC, 0x8E, 0xF0, 0xAD, 0x2C, 0x1C, 0x1F, 0xFB, 0x36, + 0xD1, 0x87, 0x15, 0xD4, 0x2E, 0x78, 0xB2, 0x49, 0x22, 0x4F, 0x92, 0xC7, 0xE6, 0xE7, + 0xA0, 0x5C, 0x49, 0xF0, 0xE7, 0xE4, 0xC8, 0x81, 0xBF, 0x2E, 0x94, 0xF4, 0x5E, 0x4A, + 0x21, 0x83, 0x3D, 0x74, 0x56, 0x85, 0x1D, 0x0F, 0x6C, 0x14, 0x5A, 0x29, 0x54, 0x0C, + 0x87, 0x4F, 0x30, 0x92, 0xC9, 0x34, 0xB4, 0x3D, 0x22, 0x2B, 0x89, 0x62, 0xC0, 0xF4, + 0x10, 0xCE, 0xF1, 0xDB, 0x75, 0x89, 0x2A, 0xF1, 0x16, 0xB4, 0x4A, 0x96, 0xF5, 0xD3, + 0x5A, 0xDE, 0xA3, 0x82, 0x2F, 0xC7, 0x14, 0x6F, 0x60, 0x04, 0x38, 0x5B, 0xCB, 0x69, + 0xB6, 0x5C, 0x99, 0xE7, 0xEB, 0x69, 0x19, 0x78, 0x67, 0x03, 0xC0, 0xD8, 0xCD, 0x41, + 0xE8, 0xF7, 0x5C, 0xCA, 0x44, 0xAA, 0x8A, 0xB7, 0x25, 0xAD, 0x8E, 0x79, 0x9F, 0xF3, + 0xA8, 0x69, 0x6A, 0x6F, 0x1B, 0x26, 0x56, 0xE6, 0x31, 0xB1, 0xE4, 0x01, 0x83, 0xC0, + 0x8F, 0xDA, 0x53, 0xFA, 0x4A, 0x8F, 0x85, 0xA0, 0x56, 0x93, 0x94, 0x4A, 0xE1, 0x79, + 0xA1, 0x33, 0x9D, 0x00, 0x2D, 0x15, 0xCA, 0xBD, 0x81, 0x00, 0x90, 0xEC, 0x72, 0x2E, + 0xF5, 0xDE, 0xF9, 0x96, 0x5A, 0x37, 0x1D, 0x41, 0x5D, 0x62, 0x4B, 0x68, 0xA2, 0x70, + 0x7C, 0xAD, 0x97, 0xBC, 0xDD, 0x17, 0x85, 0xAF, 0x97, 0xE2, 0x58, 0xF3, 0x3D, 0xF5, + 0x6A, 0x03, 0x1A, 0xA0, 0x35, 0x6D, 0x8E, 0x8D, 0x5E, 0xBC, 0xAD, 0xC7, 0x4E, 0x07, + 0x16, 0x36, 0xC6, 0xB1, 0x10, 0xAC, 0xE5, 0xCC, 0x9B, 0x90, 0xDF, 0xEA, 0xCA, 0xE6, + 0x40, 0xFF, 0x1B, 0xB0, 0xF1, 0xFE, 0x5D, 0xB4, 0xEF, 0xF7, 0xA9, 0x5F, 0x06, 0x07, + 0x33, 0xF5, // ... + 0x30, 0x45, 0x02, 0x20, 0x32, 0x47, 0x79, 0xC6, 0x8F, 0x33, 0x80, 0x28, 0x8A, 0x11, + 0x97, 0xB6, 0x09, 0x5F, 0x7A, 0x6E, 0xB9, 0xB1, 0xB1, 0xC1, 0x27, 0xF6, 0x6A, 0xE1, + 0x2A, 0x99, 0xFE, 0x85, 0x32, 0xEC, 0x23, 0xB9, 0x02, 0x21, 0x00, 0xE3, 0x95, 0x16, + 0xAC, 0x4D, 0x61, 0xEE, 0x64, 0x04, 0x4D, 0x50, 0xB4, 0x15, 0xA6, 0xA4, 0xD4, 0xD8, + 0x4B, 0xA6, 0xD8, 0x95, 0xCB, 0x5A, 0xB7, 0xA1, 0xAA, 0x7D, 0x08, 0x1D, 0xE3, 0x41, + 0xFA, // ... + // TODO(MS): Once handle_response_ctap1() can parse signature and certs, uncomment below + // sig: Signature(ByteBuf::from([ + // 0x30, 0x45, 0x02, 0x20, 0x13, 0xf7, 0x3c, 0x5d, 0x9d, 0x53, 0x0e, 0x8c, 0xc1, 0x5c, + // 0xc9, 0xbd, 0x96, 0xad, 0x58, 0x6d, 0x39, 0x36, 0x64, 0xe4, 0x62, 0xd5, 0xf0, 0x56, + // 0x12, 0x35, 0xe6, 0x35, 0x0f, 0x2b, 0x72, 0x89, 0x02, 0x21, 0x00, 0x90, 0x35, 0x7f, + // 0xf9, 0x10, 0xcc, 0xb5, 0x6a, 0xc5, 0xb5, 0x96, 0x51, 0x19, 0x48, 0x58, 0x1c, 0x8f, + // 0xdd, 0xb4, 0xa2, 0xb7, 0x99, 0x59, 0x94, 0x80, 0x78, 0xb0, 0x9f, 0x4b, 0xdc, 0x62, + // 0x29, + // ])), + // attestation_cert: vec![AttestationCertificate(vec![ + // 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, + // 0x09, 0x00, 0x85, 0x9b, 0x72, 0x6c, 0xb2, 0x4b, 0x4c, 0x29, 0x30, 0x0a, 0x06, 0x08, + // 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30, 0x47, 0x31, 0x0b, 0x30, 0x09, + // 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x14, 0x30, 0x12, 0x06, + // 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x54, + // 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, 0x06, 0x03, 0x55, 0x04, 0x0b, 0x0c, 0x19, + // 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x20, + // 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x1e, 0x17, + // 0x0d, 0x31, 0x36, 0x31, 0x32, 0x30, 0x34, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, + // 0x17, 0x0d, 0x32, 0x36, 0x31, 0x32, 0x30, 0x32, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, + // 0x5a, 0x30, 0x47, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, + // 0x55, 0x53, 0x31, 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, + // 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, + // 0x06, 0x03, 0x55, 0x04, 0x0b, 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, + // 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, + // 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, + // 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, + // 0x42, 0x00, 0x04, 0xad, 0x11, 0xeb, 0x0e, 0x88, 0x52, 0xe5, 0x3a, 0xd5, 0xdf, 0xed, + // 0x86, 0xb4, 0x1e, 0x61, 0x34, 0xa1, 0x8e, 0xc4, 0xe1, 0xaf, 0x8f, 0x22, 0x1a, 0x3c, + // 0x7d, 0x6e, 0x63, 0x6c, 0x80, 0xea, 0x13, 0xc3, 0xd5, 0x04, 0xff, 0x2e, 0x76, 0x21, + // 0x1b, 0xb4, 0x45, 0x25, 0xb1, 0x96, 0xc4, 0x4c, 0xb4, 0x84, 0x99, 0x79, 0xcf, 0x6f, + // 0x89, 0x6e, 0xcd, 0x2b, 0xb8, 0x60, 0xde, 0x1b, 0xf4, 0x37, 0x6b, 0xa3, 0x0d, 0x30, + // 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x04, 0x02, 0x30, 0x00, 0x30, 0x0a, + // 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x49, 0x00, 0x30, + // 0x46, 0x02, 0x21, 0x00, 0xe9, 0xa3, 0x9f, 0x1b, 0x03, 0x19, 0x75, 0x25, 0xf7, 0x37, + // 0x3e, 0x10, 0xce, 0x77, 0xe7, 0x80, 0x21, 0x73, 0x1b, 0x94, 0xd0, 0xc0, 0x3f, 0x3f, + // 0xda, 0x1f, 0xd2, 0x2d, 0xb3, 0xd0, 0x30, 0xe7, 0x02, 0x21, 0x00, 0xc4, 0xfa, 0xec, + // 0x34, 0x45, 0xa8, 0x20, 0xcf, 0x43, 0x12, 0x9c, 0xdb, 0x00, 0xaa, 0xbe, 0xfd, 0x9a, + // 0xe2, 0xd8, 0x74, 0xf9, 0xc5, 0xd3, 0x43, 0xcb, 0x2f, 0x11, 0x3d, 0xa2, 0x37, 0x23, + // 0xf3, + // ])], + ]), + // }), }; assert_eq!(attestation_object, expected); From 4345004ab692357a4acfe90257ee75d91911c684 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus <> Date: Thu, 20 May 2021 16:24:39 +0200 Subject: [PATCH 07/15] Add commands GetAssertion and GetAssertionNext. Still missing tests. --- src/ctap2/attestation.rs | 1 + src/ctap2/commands/get_assertion.rs | 680 +++++++++++++++++++++++ src/ctap2/commands/get_next_assertion.rs | 53 ++ src/ctap2/commands/make_credentials.rs | 3 +- src/ctap2/commands/mod.rs | 2 + src/transport/hid.rs | 3 +- src/u2fprotocol.rs | 4 +- 7 files changed, 741 insertions(+), 5 deletions(-) create mode 100644 src/ctap2/commands/get_assertion.rs create mode 100644 src/ctap2/commands/get_next_assertion.rs diff --git a/src/ctap2/attestation.rs b/src/ctap2/attestation.rs index 09fcb3a3..9e86bb07 100644 --- a/src/ctap2/attestation.rs +++ b/src/ctap2/attestation.rs @@ -289,6 +289,7 @@ pub struct AttestationStatementFidoU2F { pub attestation_cert: Vec, } +#[allow(dead_code)] // TODO(MS): Remove me, once we can parse AttestationStatements and use this function impl AttestationStatementFidoU2F { pub fn new(cert: &[u8], signature: &[u8]) -> Self { AttestationStatementFidoU2F { diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs new file mode 100644 index 00000000..a020a459 --- /dev/null +++ b/src/ctap2/commands/get_assertion.rs @@ -0,0 +1,680 @@ +use super::{Command, CommandError, RequestCtap1, RequestCtap2, Retryable, StatusCode}; +use crate::consts::{ + PARAMETER_SIZE, U2F_AUTHENTICATE, U2F_CHECK_IS_REGISTERED, U2F_REQUEST_USER_PRESENCE, +}; +use crate::ctap2::attestation::{AuthenticatorData, AuthenticatorDataFlags}; +use crate::ctap2::client_data::CollectedClientData; +use crate::ctap2::commands::get_next_assertion::GetNextAssertion; +use crate::ctap2::commands::make_credentials::{Pin, UserValidation}; +use crate::ctap2::server::{PublicKeyCredentialDescriptor, RelyingParty, User}; +use crate::transport::errors::{ApduErrorStatus, HIDError}; +use crate::transport::FidoDevice; +use crate::u2ftypes::{U2FAPDUHeader, U2FDevice}; +use nom::{ + do_parse, named, + number::complete::{be_u32, be_u8}, +}; +use serde::{ + de::{Error as DesError, MapAccess, Visitor}, + ser::{Error as SerError, SerializeMap}, + Deserialize, Deserializer, Serialize, Serializer, +}; +use serde_bytes::ByteBuf; +use serde_cbor::{de::from_slice, ser, Value}; +use serde_json::{value as json_value, Map}; +use std::fmt; +use std::io; + +#[derive(Copy, Clone, Debug, Serialize)] +#[cfg_attr(test, derive(Deserialize))] +pub struct GetAssertionOptions { + #[serde(rename = "uv", skip_serializing_if = "Option::is_none")] + pub user_validation: Option, + #[serde(rename = "up", skip_serializing_if = "Option::is_none")] + pub user_presence: Option, +} + +impl Default for GetAssertionOptions { + fn default() -> Self { + Self { + user_presence: None, + user_validation: Some(true), + } + } +} + +impl GetAssertionOptions { + pub(crate) fn has_some(&self) -> bool { + self.user_presence.is_some() || self.user_validation.is_some() + } +} + +impl UserValidation for GetAssertionOptions { + fn ask_user_validation(&self) -> bool { + if let Some(e) = self.user_validation { + e + } else { + false + } + } +} + +#[derive(Debug)] +pub struct GetAssertion { + client_data: CollectedClientData, + rp: RelyingParty, + allow_list: Vec, + + // https://www.w3.org/TR/webauthn/#client-extension-input + // The client extension input, which is a value that can be encoded in JSON, + // is passed from the WebAuthn Relying Party to the client in the get() or + // create() call, while the CBOR authenticator extension input is passed + // from the client to the authenticator for authenticator extensions during + // the processing of these calls. + extensions: Map, + options: GetAssertionOptions, + + pin: Option, + //TODO(MS): pinProtocol +} + +impl GetAssertion { + pub fn new( + client_data: CollectedClientData, + rp: RelyingParty, + allow_list: Vec, + options: GetAssertionOptions, + pin: Option, + ) -> Self { + Self { + client_data, + rp, + allow_list, + // TODO(baloo): need to sort those out once final api is in + extensions: Map::new(), + options, + pin, + } + } +} + +impl Serialize for GetAssertion { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // let pin_auth = &self.pin_auth; + + // Need to define how many elements are going to be in the map + // beforehand + let mut map_len = 2; + if !self.allow_list.is_empty() { + map_len += 1; + } + if !self.extensions.is_empty() { + map_len += 1; + } + if self.options.has_some() { + map_len += 1; + } + // if pin_auth.is_some() { + // map_len += 2; + // } + + let mut map = serializer.serialize_map(Some(map_len))?; + map.serialize_entry(&1, &self.rp)?; + + let client_data_hash = self + .client_data + .hash() + .map_err(|e| S::Error::custom(format!("error while hashing client data: {}", e)))?; + map.serialize_entry(&2, &client_data_hash)?; + if !self.allow_list.is_empty() { + map.serialize_entry(&3, &self.allow_list)?; + } + if !self.extensions.is_empty() { + map.serialize_entry(&4, &self.extensions)?; + } + if self.options.has_some() { + map.serialize_entry(&5, &self.options)?; + } + // if let Some(pin_auth) = pin_auth { + // map.serialize_entry(&6, &pin_auth)?; + // map.serialize_entry(&7, &1)?; + // } + map.end() + } +} + +impl RequestCtap1 for GetAssertion { + type Output = AssertionObject; + + fn apdu_format(&self, dev: &mut Dev) -> Result, HIDError> + where + Dev: io::Read + io::Write + fmt::Debug + FidoDevice, + { + /// This command is used to check which key_handle is valid for this + /// token this is sent before a GetAssertion command, to determine which + /// is valid for a specific token and which key_handle GetAssertion + /// should send to the token. + #[derive(Debug)] + struct GetAssertionCheck<'assertion> { + key_handle: &'assertion [u8], + client_data: &'assertion CollectedClientData, + rp: &'assertion RelyingParty, + } + + impl<'assertion> RequestCtap1 for GetAssertionCheck<'assertion> { + type Output = (); + + fn apdu_format(&self, _dev: &mut Dev) -> Result, HIDError> + where + Dev: U2FDevice + io::Read + io::Write + fmt::Debug, + { + let flags = U2F_CHECK_IS_REGISTERED; + // TODO(MS): Need to check "up" here. If up==false, set to 0x08? Or not? Spec is ambiguous + let mut auth_data = + Vec::with_capacity(2 * PARAMETER_SIZE + 1 + self.key_handle.len()); + + auth_data.extend_from_slice( + self.client_data + .hash() + .map_err(|e| HIDError::Command(CommandError::Json(e)))? + .as_ref(), + ); + auth_data.extend_from_slice(self.rp.hash().as_ref()); + auth_data.extend_from_slice(&[self.key_handle.len() as u8]); + auth_data.extend_from_slice(self.key_handle); + let cmd = U2F_AUTHENTICATE; + let apdu = U2FAPDUHeader::serialize(cmd, flags, &auth_data)?; + Ok(apdu) + } + + fn handle_response_ctap1( + &self, + status: Result<(), ApduErrorStatus>, + _input: &[u8], + ) -> Result> { + match status { + Ok(_) | Err(ApduErrorStatus::ConditionsNotSatisfied) => Ok(()), + _ => Err(Retryable::Error(HIDError::DeviceError)), + } + } + } + + let key_handle = self + .allow_list + .iter() + .find_map(|allowed_handle| { + let check_command = GetAssertionCheck { + key_handle: allowed_handle.id.as_ref(), + client_data: &self.client_data, + rp: &self.rp, + }; + let res = dev.send_apdu(&check_command); + match res { + Ok(_) => Some(allowed_handle.id.clone()), + _ => None, + } + }) + .ok_or(HIDError::DeviceNotSupported)?; + + debug!("sending key_handle = {:?}", key_handle); + + let flags = if self.options.user_presence.unwrap_or(false) { + U2F_REQUEST_USER_PRESENCE + } else { + 0 + }; + let mut auth_data = + Vec::with_capacity(2 * PARAMETER_SIZE + 1 /* key_handle_len */ + key_handle.len()); + + auth_data.extend_from_slice( + self.client_data + .hash() + .map_err(|e| HIDError::Command(CommandError::Json(e)))? + .as_ref(), + ); + auth_data.extend_from_slice(self.rp.hash().as_ref()); + auth_data.extend_from_slice(&[key_handle.len() as u8]); + auth_data.extend_from_slice(key_handle.as_ref()); + + let cmd = U2F_AUTHENTICATE; + let apdu = U2FAPDUHeader::serialize(cmd, flags, &auth_data)?; + Ok(apdu) + } + + fn handle_response_ctap1( + &self, + status: Result<(), ApduErrorStatus>, + input: &[u8], + ) -> Result> { + if Err(ApduErrorStatus::ConditionsNotSatisfied) == status { + return Err(Retryable::Retry); + } + if status.is_err() { + return Err(Retryable::Error(HIDError::DeviceError)); + } + + named!( + parse_authentication<(u8, u32)>, + do_parse!(user_presence: be_u8 >> counter: be_u32 >> (user_presence, counter)) + ); + + let (user_presence, counter, signature) = match parse_authentication(input) { + Ok((input, (user_presence, counter))) => { + let signature = Vec::from(input); + Ok((user_presence, counter, signature)) + } + Err(e) => { + error!("error while parsing authentication: {:?}", e); + Err(io::Error::new( + io::ErrorKind::Other, + "unable to parse authentication", + )) + .map_err(|e| HIDError::IO(None, e)) + .map_err(Retryable::Error) + } + }?; + + let mut flags = AuthenticatorDataFlags::empty(); + if user_presence == 1 { + flags |= AuthenticatorDataFlags::USER_PRESENT; + } + let auth_data = AuthenticatorData { + rp_id_hash: self.rp.hash(), + flags, + counter, + credential_data: None, + extensions: Vec::new(), + }; + let assertion = Assertion { + credentials: None, + signature, + public_key: None, + auth_data, + }; + + Ok(AssertionObject(vec![assertion])) + } +} + +impl RequestCtap2 for GetAssertion { + type Output = AssertionObject; + + fn command() -> Command { + Command::GetAssertion + } + + fn wire_format(&self, _dev: &mut Dev) -> Result, HIDError> + where + Dev: FidoDevice + io::Read + io::Write + fmt::Debug, + { + // TODO(MS): Add GetInfo-request here and others (See CommandDevice::new) + Ok(ser::to_vec(&self).map_err(CommandError::Serialization)?) + } + + fn handle_response_ctap2( + &self, + dev: &mut Dev, + input: &[u8], + ) -> Result + where + Dev: FidoDevice + io::Read + io::Write + fmt::Debug, + { + if input.is_empty() { + return Err(CommandError::InputTooSmall).map_err(HIDError::Command); + } + + let status: StatusCode = input[0].into(); + debug!("response status code: {:?}", status); + if input.len() > 1 { + if status.is_ok() { + let assertion: GetAssertionResponse = + from_slice(&input[1..]).map_err(CommandError::Parsing)?; + let number_of_credentials = assertion.number_of_credentials.unwrap_or(1); + let mut assertions = Vec::with_capacity(number_of_credentials); + assertions.push(assertion.into()); + + let msg = GetNextAssertion; + for _ in (1..number_of_credentials).rev() { + let new_cred = dev.send_cbor(&msg)?; + assertions.push(new_cred.into()); + } + + Ok(AssertionObject(assertions)) + } else { + let data: Value = from_slice(&input[1..]).map_err(CommandError::Parsing)?; + Err(CommandError::StatusCode(status, Some(data))).map_err(HIDError::Command) + } + } else if status.is_ok() { + Err(CommandError::InputTooSmall).map_err(HIDError::Command) + } else { + Err(CommandError::StatusCode(status, None)).map_err(HIDError::Command) + } + } +} + +#[derive(Debug, PartialEq)] +pub struct Assertion { + credentials: Option, + auth_data: AuthenticatorData, + signature: Vec, + public_key: Option, +} + +impl From for Assertion { + fn from(r: GetAssertionResponse) -> Self { + Assertion { + credentials: r.credentials, + auth_data: r.auth_data, + signature: r.signature, + public_key: r.public_key, + } + } +} + +// TODO(baloo): Move this to src/ctap2/mod.rs? +#[derive(Debug, PartialEq)] +pub struct AssertionObject(Vec); + +impl AssertionObject { + pub fn u2f_sign_data(&self) -> Vec { + if let Some(first) = self.0.first() { + first.signature.clone() + } else { + Vec::new() + } + } +} + +pub(crate) struct GetAssertionResponse { + credentials: Option, + auth_data: AuthenticatorData, + signature: Vec, + public_key: Option, + number_of_credentials: Option, +} + +impl<'de> Deserialize<'de> for GetAssertionResponse { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct GetAssertionResponseVisitor; + + impl<'de> Visitor<'de> for GetAssertionResponseVisitor { + type Value = GetAssertionResponse; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a byte array") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'de>, + { + let mut credentials = None; + let mut auth_data = None; + let mut signature = None; + let mut public_key = None; + let mut number_of_credentials = None; + + while let Some(key) = map.next_key()? { + match key { + 1 => { + if credentials.is_some() { + return Err(M::Error::duplicate_field("credentials")); + } + credentials = Some(map.next_value()?); + } + 2 => { + if auth_data.is_some() { + return Err(M::Error::duplicate_field("auth_data")); + } + auth_data = Some(map.next_value()?); + } + 3 => { + if signature.is_some() { + return Err(M::Error::duplicate_field("signature")); + } + let signature_bytes: ByteBuf = map.next_value()?; + let signature_bytes: Vec = signature_bytes.into_vec(); + signature = Some(signature_bytes); + } + 4 => { + if public_key.is_some() { + return Err(M::Error::duplicate_field("public_key")); + } + public_key = map.next_value()?; + } + 5 => { + if number_of_credentials.is_some() { + return Err(M::Error::duplicate_field("number_of_credentials")); + } + number_of_credentials = Some(map.next_value()?); + } + k => return Err(M::Error::custom(format!("unexpected key: {:?}", k))), + } + } + + let auth_data = auth_data.ok_or_else(|| M::Error::missing_field("auth_data"))?; + let signature = signature.ok_or_else(|| M::Error::missing_field("signature"))?; + + Ok(GetAssertionResponse { + credentials, + auth_data, + signature, + public_key, + number_of_credentials, + }) + } + } + + deserializer.deserialize_bytes(GetAssertionResponseVisitor) + } +} + +#[cfg(test)] +pub mod test { + use super::{Assertion, GetAssertion, GetAssertionOptions}; + use crate::consts::{ + HIDCmd, SW_CONDITIONS_NOT_SATISFIED, SW_NO_ERROR, U2F_CHECK_IS_REGISTERED, + U2F_REQUEST_USER_PRESENCE, + }; + use crate::ctap2::attestation::{AuthenticatorData, AuthenticatorDataFlags}; + use crate::ctap2::client_data::{Challenge, CollectedClientData, TokenBinding, WebauthnType}; + use crate::ctap2::commands::get_assertion::AssertionObject; + use crate::ctap2::commands::RequestCtap1; + use crate::ctap2::server::{PublicKeyCredentialDescriptor, RelyingParty, RpIdHash, Transport}; + use crate::transport::FidoDevice; + use crate::u2fprotocol::tests::platform::TestDevice; + use crate::u2ftypes::U2FDevice; + use rand::{thread_rng, RngCore}; + + #[test] + fn test_get_assertion_ctap2() {} + + fn fill_device_ctap1(device: &mut TestDevice, cid: [u8; 4], flags: u8, answer_status: [u8; 2]) { + // ctap2 request + let mut msg = cid.to_vec(); + msg.extend(&[HIDCmd::Msg.into(), 0x00, 0x8A]); // cmd + bcnt + msg.extend(&[0x00, 0x2]); // U2F_AUTHENTICATE + msg.extend(&[flags]); + msg.extend(&[0x00, 0x00, 0x00]); + msg.extend(&[0x81]); // Data len - 7 + msg.extend(&CLIENT_DATA_HASH); + msg.extend(&RELYING_PARTY_HASH[..18]); + device.add_write(&msg, 0); + + // Continuation package + let mut msg = cid.to_vec(); + msg.extend(vec![0x00]); // SEQ + msg.extend(&RELYING_PARTY_HASH[18..]); + msg.extend(&[KEY_HANDLE.len() as u8]); + msg.extend(&KEY_HANDLE[..44]); + device.add_write(&msg, 0); + + let mut msg = cid.to_vec(); + msg.extend(vec![0x01]); // SEQ + msg.extend(&KEY_HANDLE[44..]); + device.add_write(&msg, 0); + + // fido response + let mut msg = cid.to_vec(); + msg.extend(&[HIDCmd::Msg.into(), 0x0, 0x4D]); // cmd + bcnt + msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP1[0..57]); + device.add_read(&msg, 0); + + let mut msg = cid.to_vec(); + msg.extend(&[0x0]); // SEQ + msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP1[57..]); + msg.extend(&answer_status); + device.add_read(&msg, 0); + } + + #[test] + fn test_get_assertion_ctap1() { + let assertion = GetAssertion::new( + CollectedClientData { + webauthn_type: WebauthnType::Create, + challenge: Challenge::from(vec![0x00, 0x01, 0x02, 0x03]), + origin: String::from("example.com"), + cross_origin: None, + token_binding: Some(TokenBinding::Present(vec![0x00, 0x01, 0x02, 0x03])), + }, + RelyingParty { + id: String::from("example.com"), + name: Some(String::from("Acme")), + icon: None, + }, + vec![PublicKeyCredentialDescriptor { + id: vec![ + 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35, + 0xEF, 0xAA, 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D, + 0xA4, 0x85, 0x34, 0xC8, 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5, + 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD, 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5, + 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08, 0xFE, 0x42, 0x00, 0x38, + ], + transports: vec![Transport::USB], + }], + GetAssertionOptions { + user_presence: Some(true), + user_validation: None, + }, + None, + ); + let mut device = TestDevice::new(); // not really used (all functions ignore it) + // channel id + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + + device.set_cid(cid); + + // ctap2 request + fill_device_ctap1( + &mut device, + cid, + U2F_CHECK_IS_REGISTERED, + SW_CONDITIONS_NOT_SATISFIED, + ); + let ctap1_request = assertion.apdu_format(&mut device).unwrap(); + // Check if the request is going to be correct + assert_eq!(ctap1_request, GET_ASSERTION_SAMPLE_REQUEST_CTAP1); + + // Now do it again, but parse the actual response + fill_device_ctap1( + &mut device, + cid, + U2F_CHECK_IS_REGISTERED, + SW_CONDITIONS_NOT_SATISFIED, + ); + fill_device_ctap1(&mut device, cid, U2F_REQUEST_USER_PRESENCE, SW_NO_ERROR); + + let response = device.send_apdu(&assertion).unwrap(); + + // Check if response is correct + let expected_auth_data = AuthenticatorData { + rp_id_hash: RpIdHash(RELYING_PARTY_HASH), + flags: AuthenticatorDataFlags::USER_PRESENT, + counter: 0x3B, + credential_data: None, + extensions: Vec::new(), + }; + + let expected_assertion = Assertion { + credentials: None, + signature: vec![ + 0x30, 0x44, 0x02, 0x20, 0x7B, 0xDE, 0x0A, 0x52, 0xAC, 0x1F, 0x4C, 0x8B, 0x27, 0xE0, + 0x03, 0xA3, 0x70, 0xCD, 0x66, 0xA4, 0xC7, 0x11, 0x8D, 0xD2, 0x2D, 0x54, 0x47, 0x83, + 0x5F, 0x45, 0xB9, 0x9C, 0x68, 0x42, 0x3F, 0xF7, 0x02, 0x20, 0x3C, 0x51, 0x7B, 0x47, + 0x87, 0x7F, 0x85, 0x78, 0x2D, 0xE1, 0x00, 0x86, 0xA7, 0x83, 0xD1, 0xE7, 0xDF, 0x4E, + 0x36, 0x39, 0xE7, 0x71, 0xF5, 0xF6, 0xAF, 0xA3, 0x5A, 0xAD, 0x53, 0x73, 0x85, 0x8E, + ], + public_key: None, + auth_data: expected_auth_data, + }; + + let expected = AssertionObject(vec![expected_assertion]); + + assert_eq!(response, expected); + } + + const CLIENT_DATA_HASH: [u8; 32] = [ + 0xc1, 0xdd, 0x35, 0x5f, 0x3c, 0x81, 0x69, 0x23, 0xe0, 0x57, 0xca, 0x03, 0x8d, 0xba, 0xad, + 0xb8, 0x5f, 0x95, 0x55, 0xcf, 0xc7, 0x62, 0x9b, 0x9d, 0x53, 0x66, 0x97, 0x53, 0x80, 0xd7, + 0x69, 0x4f, + ]; + + const RELYING_PARTY_HASH: [u8; 32] = [ + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, 0x34, 0xE2, + 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, 0x12, 0x55, 0x86, 0xCE, + 0x19, 0x47, + ]; + const KEY_HANDLE: [u8; 64] = [ + 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35, 0xEF, 0xAA, + 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D, 0xA4, 0x85, 0x34, 0xC8, + 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5, 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD, + 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08, + 0xFE, 0x42, 0x00, 0x38, + ]; + + const GET_ASSERTION_SAMPLE_REQUEST_CTAP1: [u8; 138] = [ + // CBOR Header + 0x0, // leading zero + 0x2, // CMD U2F_Authenticate + 0x3, // Flags (user presence) + 0x0, 0x0, // zero bits + 0x0, 0x81, // size + // NOTE: This has been taken from CTAP2.0 spec, but the clientDataHash has been replaced + // to be able to operate with known values for CollectedClientData (spec doesn't say + // what values led to the provided example hash) + // clientDataHash: + 0xc1, 0xdd, 0x35, 0x5f, 0x3c, 0x81, 0x69, 0x23, 0xe0, 0x57, 0xca, 0x03, 0x8d, // hash + 0xba, 0xad, 0xb8, 0x5f, 0x95, 0x55, 0xcf, 0xc7, 0x62, 0x9b, 0x9d, 0x53, 0x66, // hash + 0x97, 0x53, 0x80, 0xd7, 0x69, 0x4f, // hash + // rpIdHash: + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, 0x34, 0xE2, + 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, 0x12, 0x55, 0x86, 0xCE, + 0x19, 0x47, // .. + // Key Handle Length (1 Byte): + 0x40, // .. + // Key Handle (Key Handle Length Bytes): + 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35, 0xEF, 0xAA, + 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D, 0xA4, 0x85, 0x34, 0xC8, + 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5, 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD, + 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08, + 0xFE, 0x42, 0x00, 0x38, 0x0, 0x0, // 2 trailing zeros from protocol + ]; + + const GET_ASSERTION_SAMPLE_RESPONSE_CTAP1: [u8; 75] = [ + 0x01, // User Presence (1 Byte) + 0x00, 0x00, 0x00, 0x3B, // Sign Count (4 Bytes) + // Signature (variable Length) + 0x30, 0x44, 0x02, 0x20, 0x7B, 0xDE, 0x0A, 0x52, 0xAC, 0x1F, 0x4C, 0x8B, 0x27, 0xE0, 0x03, + 0xA3, 0x70, 0xCD, 0x66, 0xA4, 0xC7, 0x11, 0x8D, 0xD2, 0x2D, 0x54, 0x47, 0x83, 0x5F, 0x45, + 0xB9, 0x9C, 0x68, 0x42, 0x3F, 0xF7, 0x02, 0x20, 0x3C, 0x51, 0x7B, 0x47, 0x87, 0x7F, 0x85, + 0x78, 0x2D, 0xE1, 0x00, 0x86, 0xA7, 0x83, 0xD1, 0xE7, 0xDF, 0x4E, 0x36, 0x39, 0xE7, 0x71, + 0xF5, 0xF6, 0xAF, 0xA3, 0x5A, 0xAD, 0x53, 0x73, 0x85, 0x8E, + ]; +} diff --git a/src/ctap2/commands/get_next_assertion.rs b/src/ctap2/commands/get_next_assertion.rs new file mode 100644 index 00000000..a3e5f1a8 --- /dev/null +++ b/src/ctap2/commands/get_next_assertion.rs @@ -0,0 +1,53 @@ +use super::{Command, CommandError, RequestCtap2, StatusCode}; +use crate::ctap2::commands::get_assertion::GetAssertionResponse; +use crate::transport::errors::HIDError; +use crate::u2ftypes::U2FDevice; +use serde_cbor::{de::from_slice, Value}; + +#[derive(Debug)] +pub(crate) struct GetNextAssertion; + +impl RequestCtap2 for GetNextAssertion { + type Output = GetAssertionResponse; + + fn command() -> Command { + Command::GetNextAssertion + } + + fn wire_format(&self, _dev: &mut Dev) -> Result, HIDError> + where + Dev: U2FDevice, + { + Ok(Vec::new()) + } + + fn handle_response_ctap2( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result + where + Dev: U2FDevice, + { + if input.is_empty() { + return Err(CommandError::InputTooSmall).map_err(HIDError::Command); + } + + let status: StatusCode = input[0].into(); + debug!("response status code: {:?}", status); + if input.len() > 1 { + if status.is_ok() { + let assertion = from_slice(&input[1..]).map_err(CommandError::Parsing)?; + // TODO(baloo): check assertion response does not have numberOfCredentials + Ok(assertion) + } else { + let data: Value = from_slice(&input[1..]).map_err(CommandError::Parsing)?; + Err(CommandError::StatusCode(status, Some(data))).map_err(HIDError::Command) + } + } else if status.is_ok() { + Err(CommandError::InputTooSmall).map_err(HIDError::Command) + } else { + Err(CommandError::StatusCode(status, None)).map_err(HIDError::Command) + } + } +} diff --git a/src/ctap2/commands/make_credentials.rs b/src/ctap2/commands/make_credentials.rs index eb055ef6..37729ab6 100644 --- a/src/ctap2/commands/make_credentials.rs +++ b/src/ctap2/commands/make_credentials.rs @@ -41,7 +41,7 @@ impl Default for MakeCredentialsOptions { } impl MakeCredentialsOptions { - fn has_some(&self) -> bool { + pub(crate) fn has_some(&self) -> bool { self.resident_key.is_some() || self.user_validation.is_some() } } @@ -267,6 +267,7 @@ impl RequestCtap2 for MakeCredentials { where Dev: U2FDevice + io::Read + io::Write + fmt::Debug, { + // TODO(MS): Add GetInfo-request here and others (See CommandDevice::new) Ok(ser::to_vec(&self).map_err(CommandError::Serialization)?) } diff --git a/src/ctap2/commands/mod.rs b/src/ctap2/commands/mod.rs index e982e476..159b14d6 100644 --- a/src/ctap2/commands/mod.rs +++ b/src/ctap2/commands/mod.rs @@ -6,8 +6,10 @@ use std::error::Error as StdErrorT; use std::fmt; use std::io::{Read, Write}; +pub(crate) mod get_assertion; #[allow(dead_code)] // TODO(MS): Remove me asap pub(crate) mod get_info; +pub(crate) mod get_next_assertion; pub(crate) mod get_version; pub(crate) mod make_credentials; diff --git a/src/transport/hid.rs b/src/transport/hid.rs index 8c6cbc50..1e614099 100644 --- a/src/transport/hid.rs +++ b/src/transport/hid.rs @@ -140,8 +140,7 @@ where } (cmd, data) }; - - trace!("u2f_read({:?}) cmd={:?}: {:#04X?}", self.id(), cmd, &&data); + trace!("u2f_read({:?}) cmd={:?}: {:#04X?}", self.id(), cmd, &data); Ok((cmd, data)) } } diff --git a/src/u2fprotocol.rs b/src/u2fprotocol.rs index d41d31ec..3f023a8b 100644 --- a/src/u2fprotocol.rs +++ b/src/u2fprotocol.rs @@ -294,7 +294,7 @@ pub(crate) mod tests { assert!(!self.writes.is_empty(), "Ran out of expected write values!"); let check = self.writes.remove(0); assert_eq!(check.len(), bytes.len()); - assert_eq!(&check[..], bytes); + assert_eq!(&check, bytes); Ok(bytes.len()) } @@ -309,7 +309,7 @@ pub(crate) mod tests { assert!(!self.reads.is_empty(), "Ran out of read values!"); let check = self.reads.remove(0); assert_eq!(check.len(), bytes.len()); - bytes.clone_from_slice(&check[..]); + bytes.clone_from_slice(&check); Ok(check.len()) } } From d62a52ac8dae64ed7aa05fe529412163b563320e Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus <> Date: Fri, 21 May 2021 15:31:38 +0200 Subject: [PATCH 08/15] WIP: Add clientpin command with all the crypto functionality commented out --- src/ctap2/commands/client_pin.rs | 445 ++++++++++++++++++++++++++++ src/ctap2/commands/get_assertion.rs | 4 +- src/ctap2/commands/mod.rs | 8 + src/ctap2/crypto.rs | 272 +++++++++++++++++ src/ctap2/mod.rs | 1 + 5 files changed, 729 insertions(+), 1 deletion(-) create mode 100644 src/ctap2/commands/client_pin.rs create mode 100644 src/ctap2/crypto.rs diff --git a/src/ctap2/commands/client_pin.rs b/src/ctap2/commands/client_pin.rs new file mode 100644 index 00000000..4e2305d4 --- /dev/null +++ b/src/ctap2/commands/client_pin.rs @@ -0,0 +1,445 @@ +use super::{get_info::AuthenticatorInfo, Command, CommandError, RequestCtap2, StatusCode}; +use crate::ctap2::client_data::ClientDataHash; +use crate::ctap2::crypto::{ECDHSecret, PublicKey}; +use crate::transport::errors::HIDError; +use crate::u2ftypes::U2FDevice; +use serde::{ + de::{Error as SerdeError, MapAccess, Visitor}, + ser::SerializeMap, + Deserialize, Deserializer, Serialize, Serializer, +}; +use serde_bytes::ByteBuf; +use serde_cbor::de::from_slice; +use serde_cbor::ser::to_vec; +use serde_cbor::Value; +use sha2::{Digest, Sha256}; +use std::error::Error as StdErrorT; +use std::fmt; + +// use serde::Deserialize; cfg[test] + +#[derive(Debug, Copy, Clone)] +#[repr(u8)] +pub enum PINSubcommand { + GetRetries = 0x01, + GetKeyAgreement = 0x02, + SetPIN = 0x03, + ChangePIN = 0x04, + GetPINToken = 0x05, +} + +#[derive(Debug)] +pub(crate) struct ClientPIN { + pin_protocol: u8, + subcommand: PINSubcommand, + key_agreement: Option, + pin_auth: Option<[u8; 16]>, + new_pin_enc: Option, + pin_hash_enc: Option, +} + +impl Default for ClientPIN { + fn default() -> Self { + ClientPIN { + pin_protocol: 0, + subcommand: PINSubcommand::GetRetries, + key_agreement: None, + pin_auth: None, + new_pin_enc: None, + pin_hash_enc: None, + } + } +} + +impl Serialize for ClientPIN { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Need to define how many elements are going to be in the map + // beforehand + let mut map_len = 2; + if self.key_agreement.is_some() { + map_len += 1; + } + if self.pin_auth.is_some() { + map_len += 1; + } + if self.new_pin_enc.is_some() { + map_len += 1; + } + if self.pin_hash_enc.is_some() { + map_len += 1; + } + + let mut map = serializer.serialize_map(Some(map_len))?; + map.serialize_entry(&1, &self.pin_protocol)?; + let command: u8 = self.subcommand as u8; + map.serialize_entry(&2, &command)?; + if let Some(ref key_agreement) = self.key_agreement { + map.serialize_entry(&3, key_agreement)?; + } + if let Some(ref pin_auth) = self.pin_auth { + map.serialize_entry(&4, pin_auth)?; + } + if let Some(ref new_pin_enc) = self.new_pin_enc { + map.serialize_entry(&5, new_pin_enc)?; + } + if let Some(ref pin_hash_enc) = self.pin_hash_enc { + map.serialize_entry(&6, pin_hash_enc)?; + } + + map.end() + } +} + +pub(crate) trait ClientPINSubCommand { + type Output; + fn as_client_pin(&self) -> Result; + fn parse_response_payload(&self, input: &[u8]) -> Result; +} + +struct ClientPinResponse { + key_agreement: Option, + pin_token: Option, + /// Number of PIN attempts remaining before lockout. + _retries: Option, +} + +impl<'de> Deserialize<'de> for ClientPinResponse { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct ClientPinResponseVisitor; + + impl<'de> Visitor<'de> for ClientPinResponseVisitor { + type Value = ClientPinResponse; + + 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 key_agreement = None; + let mut pin_token = None; + let mut retries = None; + + while let Some(key) = map.next_key()? { + match key { + 1 => { + if key_agreement.is_some() { + return Err(SerdeError::duplicate_field("key_agreement")); + } + key_agreement = map.next_value()?; + } + 2 => { + if pin_token.is_some() { + return Err(SerdeError::duplicate_field("pin_token")); + } + pin_token = map.next_value()?; + } + 3 => { + if retries.is_some() { + return Err(SerdeError::duplicate_field("retries")); + } + retries = Some(map.next_value()?); + } + k => return Err(M::Error::custom(format!("unexpected key: {:?}", k))), + } + } + Ok(ClientPinResponse { + key_agreement, + pin_token, + _retries: retries, + }) + } + } + + deserializer.deserialize_bytes(ClientPinResponseVisitor) + } +} + +#[derive(Debug)] +pub struct GetKeyAgreement { + pin_protocol: u8, +} + +impl GetKeyAgreement { + pub fn new(info: &AuthenticatorInfo) -> Result { + if info.pin_protocols.contains(&1) { + Ok(GetKeyAgreement { pin_protocol: 1 }) + } else { + Err(CommandError::UnsupportedPinProtocol) + } + } +} + +impl ClientPINSubCommand for GetKeyAgreement { + type Output = KeyAgreement; + + fn as_client_pin(&self) -> Result { + Ok(ClientPIN { + pin_protocol: self.pin_protocol, + subcommand: PINSubcommand::GetKeyAgreement, + ..ClientPIN::default() + }) + } + + fn parse_response_payload(&self, input: &[u8]) -> Result { + let value: Value = from_slice(input).map_err(CommandError::Parsing)?; + debug!("GetKeyAgreement::parse_response_payload {:?}", value); + + let get_pin_response: ClientPinResponse = + from_slice(input).map_err(CommandError::Parsing)?; + if let Some(key_agreement) = get_pin_response.key_agreement { + Ok(KeyAgreement(key_agreement)) + } else { + Err(CommandError::MissingRequiredField("key_agreement")) + } + } +} + +#[derive(Debug)] +pub struct GetPinToken<'sc, 'pin> { + pin_protocol: u8, + shared_secret: &'sc ECDHSecret, + pin: &'pin Pin, +} + +impl<'sc, 'pin> GetPinToken<'sc, 'pin> { + pub fn new( + info: &AuthenticatorInfo, + shared_secret: &'sc ECDHSecret, + pin: &'pin Pin, + ) -> Result { + if info.pin_protocols.contains(&1) { + Ok(GetPinToken { + pin_protocol: 1, + shared_secret, + pin, + }) + } else { + Err(CommandError::UnsupportedPinProtocol) + } + } +} + +impl<'sc, 'pin> ClientPINSubCommand for GetPinToken<'sc, 'pin> { + type Output = PinToken; + + fn as_client_pin(&self) -> Result { + let iv = [0u8; 16]; + let input = self.pin.for_pin_token(); + trace!("pin_hash = {:#04X?}", &input.as_ref()); + let pin_hash_enc = self.shared_secret.encrypt(input.as_ref(), &iv)?; + trace!("pin_hash_enc = {:#04X?}", &pin_hash_enc); + + Ok(ClientPIN { + pin_protocol: self.pin_protocol, + subcommand: PINSubcommand::GetPINToken, + key_agreement: Some(self.shared_secret.my_public_key().clone()), + pin_hash_enc: Some(ByteBuf::from(pin_hash_enc)), + ..ClientPIN::default() + }) + } + + fn parse_response_payload(&self, input: &[u8]) -> Result { + let value: Value = from_slice(input).map_err(CommandError::Parsing)?; + debug!("GetKeyAgreement::parse_response_payload {:?}", value); + + let get_pin_response: ClientPinResponse = + from_slice(input).map_err(CommandError::Parsing)?; + if let Some(encrypted_pin_token) = get_pin_response.pin_token { + let iv = [0u8; 16]; + let pin_token = self + .shared_secret + .decrypt(encrypted_pin_token.as_ref(), &iv)?; + let pin_token = PinToken(pin_token); + Ok(pin_token) + } else { + Err(CommandError::MissingRequiredField("key_agreement")) + } + } +} + +impl RequestCtap2 for T +where + T: ClientPINSubCommand, + T: fmt::Debug, +{ + type Output = ::Output; + + fn command() -> Command { + Command::ClientPin + } + + fn wire_format(&self, _dev: &mut Dev) -> Result, HIDError> + where + Dev: U2FDevice, + { + let client_pin = self.as_client_pin()?; + let output = to_vec(&client_pin).map_err(CommandError::Serialization)?; + trace!("client subcommmand: {:#04X?}", &output); + + Ok(output) + } + + fn handle_response_ctap2( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result + where + Dev: U2FDevice, + { + trace!("Client pin subcomand response:{:#04X?}", &input); + + if input.is_empty() { + return Err(CommandError::InputTooSmall).map_err(HIDError::Command); + } + let status: StatusCode = input[0].into(); + debug!("response status code: {:?}", status); + if input.len() > 1 { + if status.is_ok() { + ::parse_response_payload(self, &input[1..]) + .map_err(HIDError::Command) + } else { + let data: Value = from_slice(&input[1..]).map_err(CommandError::Parsing)?; + Err(CommandError::StatusCode(status, Some(data))).map_err(HIDError::Command) + } + } else if status.is_ok() { + Err(CommandError::InputTooSmall).map_err(HIDError::Command) + } else { + Err(CommandError::StatusCode(status, None)).map_err(HIDError::Command) + } + } +} + +#[derive(Debug)] +pub struct KeyAgreement(PublicKey); + +impl KeyAgreement { + pub fn shared_secret(&self) -> Result { + unimplemented!(); + // self.0 + // .complete_handshake() + // .map_err(|_| Error::ECDH) + // .map(ECDHSecret) + } +} + +#[derive(Debug, Deserialize)] +pub struct EncryptedPinToken(ByteBuf); + +impl AsRef<[u8]> for EncryptedPinToken { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +#[derive(Debug)] +pub struct PinToken(Vec); + +impl PinToken { + pub fn auth(&self, _client_hash_data: &ClientDataHash) -> Result { + if self.0.len() < 4 { + return Err(PinError::PinIsTooShort); + } + + let bytes = self.0.as_slice(); + if bytes.len() > 64 { + return Err(PinError::PinIsTooLong(bytes.len())); + } + + unimplemented!(); + /*let mut mac = + Hmac::::new_varkey(self.as_ref()).map_err(|_| PinError::InvalidKeyLen)?; + mac.input(client_hash_data.as_ref()); + + let mut out = [0u8; 16]; + out.copy_from_slice(&mac.result().code().as_slice()[0..16]); + + Ok(PinAuth(out))*/ + } +} + +impl AsRef<[u8]> for PinToken { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +#[derive(Debug)] +#[cfg_attr(test, derive(Deserialize))] +pub struct PinAuth([u8; 16]); + +impl AsRef<[u8]> for PinAuth { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl Serialize for PinAuth { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serde_bytes::serialize(&self.0[..], serializer) + } +} + +pub struct Pin(String); + +impl fmt::Debug for Pin { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Pin(redacted)") + } +} + +impl Pin { + pub fn new(value: &str) -> Pin { + Pin(String::from(value)) + } + + pub fn for_pin_token(&self) -> PinAuth { + let mut hasher = Sha256::new(); + hasher.input(&self.0.as_bytes()); + + let mut output = [0u8; 16]; + let len = output.len(); + output.copy_from_slice(&hasher.result().as_slice()[..len]); + + PinAuth(output) + } +} + +#[derive(Debug, Copy, Clone)] +pub enum PinError { + PinIsTooShort, + PinIsTooLong(usize), + InvalidKeyLen, +} + +impl fmt::Display for PinError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + PinError::PinIsTooShort => write!(f, "PinError: pin is too short"), + PinError::PinIsTooLong(len) => write!(f, "PinError: pin is too long ({})", len), + PinError::InvalidKeyLen => write!(f, "PinError: invalid key len"), + } + } +} + +impl StdErrorT for PinError { + fn description(&self) -> &str { + match *self { + PinError::PinIsTooShort => "PinError: pin is too short", + PinError::PinIsTooLong(_) => "PinError: pin is too long", + PinError::InvalidKeyLen => "PinError: hmac invalid key len", + } + } +} diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index a020a459..27e644a6 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -493,7 +493,9 @@ pub mod test { use rand::{thread_rng, RngCore}; #[test] - fn test_get_assertion_ctap2() {} + fn test_get_assertion_ctap2() { + // TODO(MS) Get some example data + } fn fill_device_ctap1(device: &mut TestDevice, cid: [u8; 4], flags: u8, answer_status: [u8; 2]) { // ctap2 request diff --git a/src/ctap2/commands/mod.rs b/src/ctap2/commands/mod.rs index 159b14d6..7478eb34 100644 --- a/src/ctap2/commands/mod.rs +++ b/src/ctap2/commands/mod.rs @@ -1,3 +1,4 @@ +use super::crypto; use crate::transport::errors::{ApduErrorStatus, HIDError}; use crate::transport::FidoDevice; use serde_cbor::{error::Error as CborError, Value}; @@ -6,6 +7,7 @@ use std::error::Error as StdErrorT; use std::fmt; use std::io::{Read, Write}; +pub(crate) mod client_pin; pub(crate) mod get_assertion; #[allow(dead_code)] // TODO(MS): Remove me asap pub(crate) mod get_info; @@ -324,6 +326,8 @@ pub enum CommandError { Parsing(CborError), StatusCode(StatusCode, Option), Json(json::Error), + Crypto(crypto::CryptoError), + UnsupportedPinProtocol, } impl fmt::Display for CommandError { @@ -344,6 +348,10 @@ impl fmt::Display for CommandError { write!(f, "CommandError: Unexpected code: {:?} ({:?})", code, value) } CommandError::Json(ref e) => write!(f, "CommandError: Json serializing error: {}", e), + CommandError::Crypto(ref e) => write!(f, "CommandError: Crypto error: {:?}", e), + CommandError::UnsupportedPinProtocol => { + write!(f, "CommandError: Pin protocol is not supported") + } } } } diff --git a/src/ctap2/crypto.rs b/src/ctap2/crypto.rs new file mode 100644 index 00000000..5881e097 --- /dev/null +++ b/src/ctap2/crypto.rs @@ -0,0 +1,272 @@ +use crate::ctap2::commands::CommandError; +use serde::{ + de::{Error as SerdeError, MapAccess, Visitor}, + ser::SerializeMap, + Deserialize, Deserializer, Serialize, Serializer, +}; +use serde_bytes::ByteBuf; +use serde_cbor::Value; +use std::convert::TryFrom; +use std::fmt; + +/// Errors that can be returned from COSE functions. +#[derive(Debug, PartialEq)] +pub enum CryptoError { + // DecodingFailure, + // LibraryFailure, + // MalformedInput, + // MissingHeader, + // UnexpectedHeaderValue, + // UnexpectedTag, + // UnexpectedType, + // Unimplemented, + // VerificationFailed, + // SigningFailed, + // InvalidArgument, + UnknownSignatureScheme, +} + +/// An enum identifying supported signature algorithms. +/// Currently ES256 (ECDSA with P256 and SHA256), ES384 (ECDSA with P384 and SHA384) +/// ES512 (ECDSA with P521 and SHA512), and PS256 (RSASSA-PSS with SHA256) +/// are supported. Note that with PS256, the salt length is defined +/// to be 32 bytes. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SignatureAlgorithm { + ES256 = 1, + ES384 = 2, + ES512 = 3, + PS256 = 4, +} + +impl TryFrom for SignatureAlgorithm { + type Error = CryptoError; + + fn try_from(value: u64) -> Result { + match value { + 1 => Ok(SignatureAlgorithm::ES256), + 2 => Ok(SignatureAlgorithm::ES384), + 3 => Ok(SignatureAlgorithm::ES512), + 4 => Ok(SignatureAlgorithm::PS256), + _ => Err(CryptoError::UnknownSignatureScheme), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PublicKey { + pub curve: SignatureAlgorithm, + // TODO(baloo): yeah, I know jcj :) I shouldn't be using bytes in asn.1 here :p + pub bytes: Vec, +} +impl PublicKey { + fn affine_coordinates(&self) -> Result<(ByteBuf, ByteBuf), CommandError> { + unimplemented!(); + /* + let name = self.curve.to_openssl_name(); + let group = EcGroup::from_curve_name(name)?; + + let mut ctx = BigNumContext::new().unwrap(); + let point = EcPoint::from_bytes(&group, &self.bytes, &mut ctx).unwrap(); + + let mut x = BigNum::new()?; + let mut y = BigNum::new()?; + + point.affine_coordinates_gfp(&group, &mut x, &mut y, &mut ctx)?; + //point.affine_coordinates_gf2m(&group, &mut x, &mut y, &mut ctx)?; + + Ok((x.to_vec().into(), y.to_vec().into())) + */ + } + + pub fn new(curve: SignatureAlgorithm, bytes: Vec) -> Self { + PublicKey { curve, bytes } + } +} + +const KEY_TYPE: u8 = 1; + +// https://tools.ietf.org/html/rfc8152#section-13 +#[repr(u8)] +pub enum KeyType { + OKP = 1, + EC2 = 2, + Symmetric = 4, +} + +impl<'de> Deserialize<'de> for PublicKey { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct PublicKeyVisitor; + + impl<'de> Visitor<'de> for PublicKeyVisitor { + type Value = PublicKey; + + 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 curve: Option = None; + let mut x: Option = None; + let mut y: Option = None; + + while let Some(key) = map.next_key()? { + trace!("cose key {:?}", key); + match key { + -1 => { + if curve.is_some() { + return Err(SerdeError::duplicate_field("curve")); + } + let value: u64 = map.next_value()?; + let val = SignatureAlgorithm::try_from(value).map_err(|_| { + SerdeError::custom(format!("unsupported curve {}", value)) + })?; + curve = Some(val); + } + -2 => { + if x.is_some() { + return Err(SerdeError::duplicate_field("x")); + } + let value = map.next_value()?; + + x = Some(value); + } + -3 => { + if y.is_some() { + return Err(SerdeError::duplicate_field("y")); + } + let value = map.next_value()?; + + y = Some(value); + } + _ => { + // TODO(baloo): need to check key_type (1) + // + // This unknown field should raise an error, but + // there is a couple of field I(baloo) do not understand + // yet. I(baloo) chose to ignore silently the + // error instead because of that + let value: Value = map.next_value()?; + trace!("cose unknown value {:?}:{:?}", key, value); + } + }; + } + + if let Some(_curve) = curve { + if let Some(_x) = x { + if let Some(_y) = y { + unimplemented!(); + // let pub_key = curve.affine_to_key(&x, &y).map_err(|e| { + // SerdeError::custom(format!("nss error: {:?}", e)) + // })?; + // Ok(pub_key) + } else { + Err(SerdeError::custom("missing required field: y")) + } + } else { + Err(SerdeError::custom("missing required field: x")) + } + } else { + Err(SerdeError::custom("missing required field: curve")) + } + } + } + + deserializer.deserialize_bytes(PublicKeyVisitor) + } +} + +impl Serialize for PublicKey { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(4))?; + map.serialize_entry(&KEY_TYPE, &(KeyType::EC2 as u8))?; + map.serialize_entry(&-1, &(self.curve as u8))?; + + let (x, y) = self + .affine_coordinates() + .map_err(|e| serde::ser::Error::custom(format!("NSS error: {:?}", e)))?; + + map.serialize_entry(&-2, &x)?; + map.serialize_entry(&-3, &y)?; + map.end() + } +} + +impl AsRef<[u8]> for PublicKey { + fn as_ref(&self) -> &[u8] { + self.bytes.as_ref() + } +} + +#[derive(Clone)] +pub struct ECDHSecret { + curve: SignatureAlgorithm, + remote: PublicKey, + my: PublicKey, + shared_secret: Vec, +} + +impl ECDHSecret { + pub fn my_public_key(&self) -> &PublicKey { + &self.my + } + + pub fn shared_secret(&self) -> &[u8] { + self.shared_secret.as_ref() + } + + pub fn encrypt(&self, _input: &[u8], _iv: &[u8]) -> Result, CommandError> { + unimplemented!(); + /*let cipher = Cipher::aes_256_cbc(); + + // TODO(baloo): This might trigger a panic if size is not big enough + let mut output = vec![0; input.len() * 2]; + output.resize(input.len() * 2, 0); + let mut encrypter = Crypter::new(cipher, Mode::Encrypt, self.0.shared_secret(), Some(iv)) + .map_err(Error::Openssl)?; + encrypter.pad(false); + let mut out_size = 0; + out_size += encrypter.update(input, output.as_mut_slice())?; + out_size += encrypter.finalize(output.as_mut_slice())?; + output.truncate(out_size); + Ok(output)*/ + } + + pub fn decrypt(&self, _input: &[u8], _iv: &[u8]) -> Result, CommandError> { + unimplemented!(); + /*let cipher = Cipher::aes_256_cbc(); + + // TODO(baloo): This might trigger a panic if size is not big enough + let mut output = vec![0; input.len() * 2]; + output.resize(input.len() * 2, 0); + let mut encrypter = Crypter::new(cipher, Mode::Decrypt, self.0.shared_secret(), Some(iv)) + .map_err(Error::Openssl)?; + encrypter.pad(false); + let mut out_size = 0; + out_size += encrypter.update(input, output.as_mut_slice())?; + out_size += encrypter.finalize(output.as_mut_slice())?; + output.truncate(out_size); + + Ok(output)*/ + } +} + +impl fmt::Debug for ECDHSecret { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "ECDHSecret(remote: {:?}, my: {:?})", + self.remote, + self.my_public_key() + ) + } +} diff --git a/src/ctap2/mod.rs b/src/ctap2/mod.rs index b0687bb0..1538dccd 100644 --- a/src/ctap2/mod.rs +++ b/src/ctap2/mod.rs @@ -4,6 +4,7 @@ pub mod commands; pub(crate) mod attestation; pub mod client_data; +pub mod crypto; pub(crate) mod server; pub(crate) mod utils; // TODO: More here soon From 15eb9edb7b29cc77c8538f434f8b2afb8ea4d247 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus <> Date: Tue, 25 May 2021 10:56:06 +0200 Subject: [PATCH 09/15] Remove duplicate Pin-definition --- src/ctap2/commands/get_assertion.rs | 3 ++- src/ctap2/commands/make_credentials.rs | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 27e644a6..66e5d0a7 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -4,8 +4,9 @@ use crate::consts::{ }; use crate::ctap2::attestation::{AuthenticatorData, AuthenticatorDataFlags}; use crate::ctap2::client_data::CollectedClientData; +use crate::ctap2::commands::client_pin::Pin; use crate::ctap2::commands::get_next_assertion::GetNextAssertion; -use crate::ctap2::commands::make_credentials::{Pin, UserValidation}; +use crate::ctap2::commands::make_credentials::UserValidation; use crate::ctap2::server::{PublicKeyCredentialDescriptor, RelyingParty, User}; use crate::transport::errors::{ApduErrorStatus, HIDError}; use crate::transport::FidoDevice; diff --git a/src/ctap2/commands/make_credentials.rs b/src/ctap2/commands/make_credentials.rs index 37729ab6..04c7f92c 100644 --- a/src/ctap2/commands/make_credentials.rs +++ b/src/ctap2/commands/make_credentials.rs @@ -5,6 +5,7 @@ use crate::ctap2::attestation::{ AuthenticatorDataFlags, }; use crate::ctap2::client_data::CollectedClientData; +use crate::ctap2::commands::client_pin::Pin; use crate::ctap2::server::{ PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, User, }; @@ -60,9 +61,6 @@ impl UserValidation for MakeCredentialsOptions { } } -#[derive(Debug)] -pub struct Pin(String); // TODO(MS): unimplemented! Requires more crypto - #[derive(Debug)] pub struct MakeCredentials { client_data: CollectedClientData, From 2980fb849e99186e470ba2fdc7a89271816570e9 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus <> Date: Tue, 25 May 2021 16:11:11 +0200 Subject: [PATCH 10/15] MakeCredentials and GetAssertion now do not store Pin but PinAuth, which has to be calculated before hand --- src/ctap2/commands/get_assertion.rs | 24 +++++----- src/ctap2/commands/make_credentials.rs | 15 ++++-- src/ctap2/commands/mod.rs | 65 ++++++++++++++++++++++++++ src/transport/hid.rs | 11 +++++ src/transport/mod.rs | 3 ++ 5 files changed, 101 insertions(+), 17 deletions(-) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 66e5d0a7..14b9b69a 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -4,7 +4,7 @@ use crate::consts::{ }; use crate::ctap2::attestation::{AuthenticatorData, AuthenticatorDataFlags}; use crate::ctap2::client_data::CollectedClientData; -use crate::ctap2::commands::client_pin::Pin; +use crate::ctap2::commands::client_pin::PinAuth; use crate::ctap2::commands::get_next_assertion::GetNextAssertion; use crate::ctap2::commands::make_credentials::UserValidation; use crate::ctap2::server::{PublicKeyCredentialDescriptor, RelyingParty, User}; @@ -75,7 +75,7 @@ pub struct GetAssertion { extensions: Map, options: GetAssertionOptions, - pin: Option, + pin_auth: Option, //TODO(MS): pinProtocol } @@ -85,7 +85,7 @@ impl GetAssertion { rp: RelyingParty, allow_list: Vec, options: GetAssertionOptions, - pin: Option, + pin_auth: Option, ) -> Self { Self { client_data, @@ -94,7 +94,7 @@ impl GetAssertion { // TODO(baloo): need to sort those out once final api is in extensions: Map::new(), options, - pin, + pin_auth, } } } @@ -104,8 +104,6 @@ impl Serialize for GetAssertion { where S: Serializer, { - // let pin_auth = &self.pin_auth; - // Need to define how many elements are going to be in the map // beforehand let mut map_len = 2; @@ -118,9 +116,9 @@ impl Serialize for GetAssertion { if self.options.has_some() { map_len += 1; } - // if pin_auth.is_some() { - // map_len += 2; - // } + if self.pin_auth.is_some() { + map_len += 2; + } let mut map = serializer.serialize_map(Some(map_len))?; map.serialize_entry(&1, &self.rp)?; @@ -139,10 +137,10 @@ impl Serialize for GetAssertion { if self.options.has_some() { map.serialize_entry(&5, &self.options)?; } - // if let Some(pin_auth) = pin_auth { - // map.serialize_entry(&6, &pin_auth)?; - // map.serialize_entry(&7, &1)?; - // } + if let Some(pin_auth) = &self.pin_auth { + map.serialize_entry(&6, &pin_auth)?; + map.serialize_entry(&7, &1)?; + } map.end() } } diff --git a/src/ctap2/commands/make_credentials.rs b/src/ctap2/commands/make_credentials.rs index 04c7f92c..12acdbbd 100644 --- a/src/ctap2/commands/make_credentials.rs +++ b/src/ctap2/commands/make_credentials.rs @@ -5,7 +5,7 @@ use crate::ctap2::attestation::{ AuthenticatorDataFlags, }; use crate::ctap2::client_data::CollectedClientData; -use crate::ctap2::commands::client_pin::Pin; +use crate::ctap2::commands::client_pin::PinAuth; use crate::ctap2::server::{ PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, User, }; @@ -78,7 +78,7 @@ pub struct MakeCredentials { // the processing of these calls. extensions: Map, options: MakeCredentialsOptions, - pin: Option, + pin_auth: Option, // TODO(MS): pin_protocol } @@ -90,7 +90,7 @@ impl MakeCredentials { pub_cred_params: Vec, exclude_list: Vec, options: MakeCredentialsOptions, - pin: Option, + pin_auth: Option, ) -> Self { Self { client_data, @@ -101,7 +101,7 @@ impl MakeCredentials { // TODO(baloo): need to sort those out once final api is in extensions: Map::new(), options, - pin, + pin_auth, } } } @@ -123,6 +123,9 @@ impl Serialize for MakeCredentials { if self.options.has_some() { map_len += 1; } + if self.pin_auth.is_some() { + map_len += 2; + } let mut map = serializer.serialize_map(Some(map_len))?; let client_data_hash = self @@ -142,6 +145,10 @@ impl Serialize for MakeCredentials { if self.options.has_some() { map.serialize_entry(&7, &self.options)?; } + if let Some(pin_auth) = &self.pin_auth { + map.serialize_entry(&8, &pin_auth)?; + map.serialize_entry(&9, &1)?; // version + } map.end() } } diff --git a/src/ctap2/commands/mod.rs b/src/ctap2/commands/mod.rs index 7478eb34..31c4c396 100644 --- a/src/ctap2/commands/mod.rs +++ b/src/ctap2/commands/mod.rs @@ -1,4 +1,7 @@ use super::crypto; +use crate::ctap2::client_data::ClientDataHash; +use crate::ctap2::commands::client_pin::{GetKeyAgreement, GetPinToken, Pin, PinAuth, PinError}; +use crate::ctap2::commands::get_info::GetInfo; use crate::transport::errors::{ApduErrorStatus, HIDError}; use crate::transport::FidoDevice; use serde_cbor::{error::Error as CborError, Value}; @@ -328,6 +331,7 @@ pub enum CommandError { Json(json::Error), Crypto(crypto::CryptoError), UnsupportedPinProtocol, + Pin(PinError), } impl fmt::Display for CommandError { @@ -352,17 +356,71 @@ impl fmt::Display for CommandError { CommandError::UnsupportedPinProtocol => { write!(f, "CommandError: Pin protocol is not supported") } + CommandError::Pin(ref p) => write!(f, "CommandError: Pin error: {}", p), } } } impl StdErrorT for CommandError {} +pub(crate) fn calculate_pin_auth( + dev: &mut Dev, + client_data_hash: &ClientDataHash, + pin: &Option, +) -> Result, HIDError> +where + Dev: FidoDevice, +{ + let info = if let Some(authenticator_info) = dev.get_authenticator_info().cloned() { + authenticator_info + } else { + let info_command = GetInfo::default(); + let info = dev.send_cbor(&info_command)?; + debug!("infos: {:?}", info); + + dev.set_authenticator_info(info.clone()); + info + }; + + let pin_auth = if info.options.client_pin == Some(true) { + let pin = pin + .as_ref() + .ok_or(HIDError::Command(CommandError::StatusCode( + StatusCode::PinRequired, + None, + )))?; + + let shared_secret = if let Some(shared_secret) = dev.get_shared_secret().cloned() { + shared_secret + } else { + let pin_command = GetKeyAgreement::new(&info)?; + let device_key_agreement = dev.send_cbor(&pin_command)?; + let shared_secret = device_key_agreement.shared_secret()?; + dev.set_shared_secret(shared_secret.clone()); + shared_secret + }; + + let pin_command = GetPinToken::new(&info, &shared_secret, &pin)?; + let pin_token = dev.send_cbor(&pin_command)?; + + Some( + pin_token + .auth(&client_data_hash) + .map_err(CommandError::Pin)?, + ) + } else { + None + }; + + Ok(pin_auth) +} + #[cfg(test)] pub mod tests { use super::*; use crate::consts::CID_BROADCAST; use crate::ctap2::commands::get_info::AuthenticatorInfo; + use crate::ctap2::crypto::ECDHSecret; use crate::transport::hid::HIDDevice; use crate::u2fprotocol::tests::platform::TestDevice; use crate::u2ftypes::U2FDevice; @@ -379,6 +437,13 @@ pub mod tests { self.authenticator_info = Some(authenticator_info); } + fn set_shared_secret(&mut self, _: ECDHSecret) { + // Nothing + } + fn get_shared_secret(&self) -> std::option::Option<&ECDHSecret> { + None + } + fn new(_: Self::BuildParameters) -> Result where Self::BuildParameters: Sized, diff --git a/src/transport/hid.rs b/src/transport/hid.rs index 1e614099..f33d3503 100644 --- a/src/transport/hid.rs +++ b/src/transport/hid.rs @@ -2,6 +2,7 @@ use crate::consts::{HIDCmd, CID_BROADCAST}; use crate::ctap2::commands::get_info::{AuthenticatorInfo, GetInfo}; use crate::ctap2::commands::get_version::GetVersion; use crate::ctap2::commands::{RequestCtap1, RequestCtap2, Retryable}; +use crate::ctap2::crypto::ECDHSecret; use crate::transport::{ errors::{ApduErrorStatus, HIDError}, FidoDevice, Nonce, @@ -35,6 +36,8 @@ where fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo>; fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo); + fn set_shared_secret(&mut self, secret: ECDHSecret); + fn get_shared_secret(&self) -> Option<&ECDHSecret>; fn initialize(&mut self, noncecmd: Nonce) -> Result<(), HIDError> where @@ -247,4 +250,12 @@ where fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> { ::get_authenticator_info(self) } + + fn set_shared_secret(&mut self, shared_secret: ECDHSecret) { + ::set_shared_secret(self, shared_secret) + } + + fn get_shared_secret(&self) -> Option<&ECDHSecret> { + ::get_shared_secret(self) + } } diff --git a/src/transport/mod.rs b/src/transport/mod.rs index 6bcb340d..3ddbbec8 100644 --- a/src/transport/mod.rs +++ b/src/transport/mod.rs @@ -1,6 +1,7 @@ use crate::consts::Capability; use crate::ctap2::commands::get_info::AuthenticatorInfo; use crate::ctap2::commands::{Request, RequestCtap1, RequestCtap2}; +use crate::ctap2::crypto::ECDHSecret; use crate::u2ftypes::U2FDevice; use std::fmt; @@ -103,4 +104,6 @@ where fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo>; fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo); + fn set_shared_secret(&mut self, secret: ECDHSecret); + fn get_shared_secret(&self) -> Option<&ECDHSecret>; } From 5f3930c9b774d920d7e4b7db332877c74324b561 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus <> Date: Tue, 15 Jun 2021 09:30:05 +0200 Subject: [PATCH 11/15] MakeCredentials: Remove pointless alias for HIDError --- src/ctap2/commands/make_credentials.rs | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/ctap2/commands/make_credentials.rs b/src/ctap2/commands/make_credentials.rs index 12acdbbd..d9b7b5d5 100644 --- a/src/ctap2/commands/make_credentials.rs +++ b/src/ctap2/commands/make_credentials.rs @@ -9,7 +9,7 @@ use crate::ctap2::commands::client_pin::PinAuth; use crate::ctap2::server::{ PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, User, }; -use crate::transport::errors::{ApduErrorStatus, HIDError as TransportError}; +use crate::transport::errors::{ApduErrorStatus, HIDError}; use crate::u2ftypes::{U2FAPDUHeader, U2FDevice}; use nom::{do_parse, named, number::complete::be_u8, tag, take}; #[cfg(test)] @@ -158,7 +158,7 @@ impl Request<(AttestationObject, CollectedClientData)> for MakeCredentials {} impl RequestCtap1 for MakeCredentials { type Output = (AttestationObject, CollectedClientData); - fn apdu_format(&self, _dev: &mut Dev) -> Result, TransportError> + fn apdu_format(&self, _dev: &mut Dev) -> Result, HIDError> where Dev: U2FDevice, { @@ -179,7 +179,7 @@ impl RequestCtap1 for MakeCredentials { register_data.extend_from_slice( self.client_data .hash() - .map_err(|e| TransportError::Command(CommandError::Json(e)))? + .map_err(|e| HIDError::Command(CommandError::Json(e)))? .as_ref(), ); register_data.extend_from_slice(self.rp.hash().as_ref()); @@ -193,7 +193,7 @@ impl RequestCtap1 for MakeCredentials { &self, status: Result<(), ApduErrorStatus>, input: &[u8], - ) -> Result> { + ) -> Result> { if Err(ApduErrorStatus::ConditionsNotSatisfied) == status { return Err(Retryable::Retry); } @@ -214,7 +214,7 @@ impl RequestCtap1 for MakeCredentials { error!("error while parsing registration = {:?}", e); io::Error::new(io::ErrorKind::Other, "unable to parse registration") }) - .map_err(|e| TransportError::IO(None, e)) + .map_err(|e| HIDError::IO(None, e)) .map_err(Retryable::Error)?; // TODO(MS): This is currently not parsed within the crate, but outside by a user-provided function @@ -226,7 +226,7 @@ impl RequestCtap1 for MakeCredentials { // let err = io::Error::new(io::ErrorKind::Other, "Failed to parse x509 certificate"); // let err = error::Error::from(err); // let err = CommandError::Parsing(err); - // let err = TransportError::Command(err); + // let err = HIDError::Command(err); // Retryable::Error(err) // }) // .map(|(sig, cert)| (sig, &rest[..rest.len() - sig.len()]))?; @@ -268,7 +268,7 @@ impl RequestCtap2 for MakeCredentials { Command::MakeCredentials } - fn wire_format(&self, _dev: &mut Dev) -> Result, TransportError> + fn wire_format(&self, _dev: &mut Dev) -> Result, HIDError> where Dev: U2FDevice + io::Read + io::Write + fmt::Debug, { @@ -280,12 +280,12 @@ impl RequestCtap2 for MakeCredentials { &self, _dev: &mut Dev, input: &[u8], - ) -> Result + ) -> Result where Dev: U2FDevice + io::Read + io::Write + fmt::Debug, { if input.is_empty() { - return Err(TransportError::Command(CommandError::InputTooSmall)); + return Err(HIDError::Command(CommandError::InputTooSmall)); } let status: StatusCode = input[0].into(); @@ -297,17 +297,15 @@ impl RequestCtap2 for MakeCredentials { Ok((attestation, client_data)) } else { let data: Value = from_slice(&input[1..]).map_err(CommandError::Parsing)?; - Err(TransportError::Command(CommandError::StatusCode( + Err(HIDError::Command(CommandError::StatusCode( status, Some(data), ))) } } else if status.is_ok() { - Err(TransportError::Command(CommandError::InputTooSmall)) + Err(HIDError::Command(CommandError::InputTooSmall)) } else { - Err(TransportError::Command(CommandError::StatusCode( - status, None, - ))) + Err(HIDError::Command(CommandError::StatusCode(status, None))) } } } From 1b085b26915901a2067f430c26c5dd03d92de9ad Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus <> Date: Fri, 18 Jun 2021 15:33:39 +0200 Subject: [PATCH 12/15] GetInfo: Fix wrong parsing of response. Forgot to strip away the status-byte --- src/ctap2/commands/get_info.rs | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/ctap2/commands/get_info.rs b/src/ctap2/commands/get_info.rs index 8ab83edf..6281a306 100644 --- a/src/ctap2/commands/get_info.rs +++ b/src/ctap2/commands/get_info.rs @@ -1,11 +1,11 @@ -use super::{Command, CommandError, RequestCtap2}; +use super::{Command, CommandError, RequestCtap2, StatusCode}; use crate::transport::errors::HIDError; use crate::u2ftypes::U2FDevice; use serde::{ de::{Error as SError, MapAccess, Visitor}, Deserialize, Deserializer, Serialize, }; -use serde_cbor::de::from_slice; +use serde_cbor::{de::from_slice, Value}; use std::fmt; #[derive(Serialize, PartialEq, Eq, Clone)] @@ -120,10 +120,22 @@ impl RequestCtap2 for GetInfo { if input.is_empty() { return Err(CommandError::InputTooSmall).map_err(HIDError::Command); } + + let status: StatusCode = input[0].into(); + if input.len() > 1 { - trace!("parsing authenticator info data: {:#04X?}", &input); - let authenticator_info = from_slice(&input).map_err(CommandError::Deserializing)?; - Ok(authenticator_info) + if status.is_ok() { + trace!("parsing authenticator info data: {:#04X?}", &input); + let authenticator_info = + from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + Ok(authenticator_info) + } else { + let data: Value = from_slice(&input[1..]).map_err(CommandError::Parsing)?; + Err(HIDError::Command(CommandError::StatusCode( + status, + Some(data), + ))) + } } else { Err(CommandError::InputTooSmall).map_err(HIDError::Command) } @@ -365,13 +377,14 @@ pub mod tests { // ctap2 response let mut msg = cid.to_vec(); - msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x59]); // cmd + bcnt - msg.extend(&AUTHENTICATOR_INFO_PAYLOAD[0..(IN_HID_RPT_SIZE - 7)]); + msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x5A]); // cmd + bcnt + msg.extend(vec![0]); // Status code: Success + msg.extend(&AUTHENTICATOR_INFO_PAYLOAD[0..(IN_HID_RPT_SIZE - 8)]); device.add_read(&msg, 0); // Continuation package let mut msg = cid.to_vec(); msg.extend(vec![0x00]); // SEQ - msg.extend(&AUTHENTICATOR_INFO_PAYLOAD[(IN_HID_RPT_SIZE - 7)..]); + msg.extend(&AUTHENTICATOR_INFO_PAYLOAD[(IN_HID_RPT_SIZE - 8)..]); device.add_read(&msg, 0); device .init(Nonce::Use(nonce)) From e2062552b815fbf49cb3487db5c0e32f9d572aa8 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus <> Date: Fri, 5 Nov 2021 12:41:17 +0100 Subject: [PATCH 13/15] Make naming of CommandError variants consistent and remove redundant 'Parsing'-variant --- src/ctap2/commands/client_pin.rs | 12 ++++++------ src/ctap2/commands/get_assertion.rs | 6 +++--- src/ctap2/commands/get_info.rs | 2 +- src/ctap2/commands/get_next_assertion.rs | 4 ++-- src/ctap2/commands/make_credentials.rs | 8 ++++---- src/ctap2/commands/mod.rs | 6 ++---- 6 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/ctap2/commands/client_pin.rs b/src/ctap2/commands/client_pin.rs index 4e2305d4..987dc3b6 100644 --- a/src/ctap2/commands/client_pin.rs +++ b/src/ctap2/commands/client_pin.rs @@ -190,11 +190,11 @@ impl ClientPINSubCommand for GetKeyAgreement { } fn parse_response_payload(&self, input: &[u8]) -> Result { - let value: Value = from_slice(input).map_err(CommandError::Parsing)?; + let value: Value = from_slice(input).map_err(CommandError::Deserializing)?; debug!("GetKeyAgreement::parse_response_payload {:?}", value); let get_pin_response: ClientPinResponse = - from_slice(input).map_err(CommandError::Parsing)?; + from_slice(input).map_err(CommandError::Deserializing)?; if let Some(key_agreement) = get_pin_response.key_agreement { Ok(KeyAgreement(key_agreement)) } else { @@ -248,11 +248,11 @@ impl<'sc, 'pin> ClientPINSubCommand for GetPinToken<'sc, 'pin> { } fn parse_response_payload(&self, input: &[u8]) -> Result { - let value: Value = from_slice(input).map_err(CommandError::Parsing)?; + let value: Value = from_slice(input).map_err(CommandError::Deserializing)?; debug!("GetKeyAgreement::parse_response_payload {:?}", value); let get_pin_response: ClientPinResponse = - from_slice(input).map_err(CommandError::Parsing)?; + from_slice(input).map_err(CommandError::Deserializing)?; if let Some(encrypted_pin_token) = get_pin_response.pin_token { let iv = [0u8; 16]; let pin_token = self @@ -282,7 +282,7 @@ where Dev: U2FDevice, { let client_pin = self.as_client_pin()?; - let output = to_vec(&client_pin).map_err(CommandError::Serialization)?; + let output = to_vec(&client_pin).map_err(CommandError::Serializing)?; trace!("client subcommmand: {:#04X?}", &output); Ok(output) @@ -308,7 +308,7 @@ where ::parse_response_payload(self, &input[1..]) .map_err(HIDError::Command) } else { - let data: Value = from_slice(&input[1..]).map_err(CommandError::Parsing)?; + let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; Err(CommandError::StatusCode(status, Some(data))).map_err(HIDError::Command) } } else if status.is_ok() { diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 14b9b69a..d8e329a3 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -310,7 +310,7 @@ impl RequestCtap2 for GetAssertion { Dev: FidoDevice + io::Read + io::Write + fmt::Debug, { // TODO(MS): Add GetInfo-request here and others (See CommandDevice::new) - Ok(ser::to_vec(&self).map_err(CommandError::Serialization)?) + Ok(ser::to_vec(&self).map_err(CommandError::Serializing)?) } fn handle_response_ctap2( @@ -330,7 +330,7 @@ impl RequestCtap2 for GetAssertion { if input.len() > 1 { if status.is_ok() { let assertion: GetAssertionResponse = - from_slice(&input[1..]).map_err(CommandError::Parsing)?; + from_slice(&input[1..]).map_err(CommandError::Deserializing)?; let number_of_credentials = assertion.number_of_credentials.unwrap_or(1); let mut assertions = Vec::with_capacity(number_of_credentials); assertions.push(assertion.into()); @@ -343,7 +343,7 @@ impl RequestCtap2 for GetAssertion { Ok(AssertionObject(assertions)) } else { - let data: Value = from_slice(&input[1..]).map_err(CommandError::Parsing)?; + let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; Err(CommandError::StatusCode(status, Some(data))).map_err(HIDError::Command) } } else if status.is_ok() { diff --git a/src/ctap2/commands/get_info.rs b/src/ctap2/commands/get_info.rs index 6281a306..3e0ed71e 100644 --- a/src/ctap2/commands/get_info.rs +++ b/src/ctap2/commands/get_info.rs @@ -130,7 +130,7 @@ impl RequestCtap2 for GetInfo { from_slice(&input[1..]).map_err(CommandError::Deserializing)?; Ok(authenticator_info) } else { - let data: Value = from_slice(&input[1..]).map_err(CommandError::Parsing)?; + let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; Err(HIDError::Command(CommandError::StatusCode( status, Some(data), diff --git a/src/ctap2/commands/get_next_assertion.rs b/src/ctap2/commands/get_next_assertion.rs index a3e5f1a8..6b7d146e 100644 --- a/src/ctap2/commands/get_next_assertion.rs +++ b/src/ctap2/commands/get_next_assertion.rs @@ -37,11 +37,11 @@ impl RequestCtap2 for GetNextAssertion { debug!("response status code: {:?}", status); if input.len() > 1 { if status.is_ok() { - let assertion = from_slice(&input[1..]).map_err(CommandError::Parsing)?; + let assertion = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; // TODO(baloo): check assertion response does not have numberOfCredentials Ok(assertion) } else { - let data: Value = from_slice(&input[1..]).map_err(CommandError::Parsing)?; + let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; Err(CommandError::StatusCode(status, Some(data))).map_err(HIDError::Command) } } else if status.is_ok() { diff --git a/src/ctap2/commands/make_credentials.rs b/src/ctap2/commands/make_credentials.rs index d9b7b5d5..690f94ce 100644 --- a/src/ctap2/commands/make_credentials.rs +++ b/src/ctap2/commands/make_credentials.rs @@ -225,7 +225,7 @@ impl RequestCtap1 for MakeCredentials { // error!("error while parsing cert = {:?}", e); // let err = io::Error::new(io::ErrorKind::Other, "Failed to parse x509 certificate"); // let err = error::Error::from(err); - // let err = CommandError::Parsing(err); + // let err = CommandError::Deserializing(err); // let err = HIDError::Command(err); // Retryable::Error(err) // }) @@ -273,7 +273,7 @@ impl RequestCtap2 for MakeCredentials { Dev: U2FDevice + io::Read + io::Write + fmt::Debug, { // TODO(MS): Add GetInfo-request here and others (See CommandDevice::new) - Ok(ser::to_vec(&self).map_err(CommandError::Serialization)?) + Ok(ser::to_vec(&self).map_err(CommandError::Serializing)?) } fn handle_response_ctap2( @@ -292,11 +292,11 @@ impl RequestCtap2 for MakeCredentials { debug!("response status code: {:?}", status); if input.len() > 1 { if status.is_ok() { - let attestation = from_slice(&input[1..]).map_err(CommandError::Parsing)?; + let attestation = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; let client_data = self.client_data.clone(); Ok((attestation, client_data)) } else { - let data: Value = from_slice(&input[1..]).map_err(CommandError::Parsing)?; + let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; Err(HIDError::Command(CommandError::StatusCode( status, Some(data), diff --git a/src/ctap2/commands/mod.rs b/src/ctap2/commands/mod.rs index 31c4c396..df91acfb 100644 --- a/src/ctap2/commands/mod.rs +++ b/src/ctap2/commands/mod.rs @@ -325,8 +325,7 @@ pub enum CommandError { InputTooSmall, MissingRequiredField(&'static str), Deserializing(CborError), - Serialization(CborError), - Parsing(CborError), + Serializing(CborError), StatusCode(StatusCode, Option), Json(json::Error), Crypto(crypto::CryptoError), @@ -344,10 +343,9 @@ impl fmt::Display for CommandError { CommandError::Deserializing(ref e) => { write!(f, "CommandError: Error while parsing: {}", e) } - CommandError::Serialization(ref e) => { + CommandError::Serializing(ref e) => { write!(f, "CommandError: Error while serializing: {}", e) } - CommandError::Parsing(ref e) => write!(f, "CommandError: Error while parsing: {}", e), CommandError::StatusCode(ref code, ref value) => { write!(f, "CommandError: Unexpected code: {:?} ({:?})", code, value) } From a147e508711102e39e7c8ea75a5b48d9f3541143 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus <> Date: Fri, 5 Nov 2021 12:53:46 +0100 Subject: [PATCH 14/15] Replace custom error with invalid_length (even though the usage is a bit weird) --- src/ctap2/attestation.rs | 14 +++++++++----- src/ctap2/client_data.rs | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/ctap2/attestation.rs b/src/ctap2/attestation.rs index 9e86bb07..01eb11a2 100644 --- a/src/ctap2/attestation.rs +++ b/src/ctap2/attestation.rs @@ -89,11 +89,10 @@ impl<'de> Deserialize<'de> for AAGuid { where E: SerdeError, { - if v.len() != 16 { - return Err(E::custom("expecting 16 bytes data")); - } - let mut buf = [0u8; 16]; + if v.len() != buf.len() { + return Err(E::invalid_length(v.len(), &"16")); + } buf.copy_from_slice(v); @@ -203,7 +202,12 @@ impl<'de> Deserialize<'de> for AuthenticatorData { parse_ad(v) .map(|(_input, value)| value) .map_err(|e| match e { - NomErr::Incomplete(len) => E::custom(format!("need {:?} more bytes", len)), + NomErr::Incomplete(nom::Needed::Size(len)) => { + E::invalid_length(v.len(), &format!("{}", v.len() + len).as_ref()) + } + NomErr::Incomplete(nom::Needed::Unknown) => { + E::invalid_length(v.len(), &"unknown") // We don't know the expected value + } // TODO(baloo): is that enough? should we be more // specific on the error type? e => E::custom(e.to_string()), diff --git a/src/ctap2/client_data.rs b/src/ctap2/client_data.rs index 0d98c726..1665dab0 100644 --- a/src/ctap2/client_data.rs +++ b/src/ctap2/client_data.rs @@ -174,7 +174,7 @@ impl<'de> Deserialize<'de> for ClientDataHash { { let mut out = [0u8; 32]; if out.len() != v.len() { - return Err(E::custom("unexpected byte len")); + return Err(E::invalid_length(v.len(), &"32")); } out.copy_from_slice(v); Ok(ClientDataHash(out)) From 386d61641708ad54a3f6d12363b47ec921171f47 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus <> Date: Mon, 8 Nov 2021 11:06:34 +0100 Subject: [PATCH 15/15] Use From-trait of HIDError to avoid verbose map_err-calls, by using the shorter .into() --- src/ctap2/commands/client_pin.rs | 8 ++++---- src/ctap2/commands/get_assertion.rs | 8 ++++---- src/ctap2/commands/get_info.rs | 9 +++------ src/ctap2/commands/get_next_assertion.rs | 8 ++++---- 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/ctap2/commands/client_pin.rs b/src/ctap2/commands/client_pin.rs index 987dc3b6..4770bb79 100644 --- a/src/ctap2/commands/client_pin.rs +++ b/src/ctap2/commands/client_pin.rs @@ -299,7 +299,7 @@ where trace!("Client pin subcomand response:{:#04X?}", &input); if input.is_empty() { - return Err(CommandError::InputTooSmall).map_err(HIDError::Command); + return Err(CommandError::InputTooSmall.into()); } let status: StatusCode = input[0].into(); debug!("response status code: {:?}", status); @@ -309,12 +309,12 @@ where .map_err(HIDError::Command) } else { let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; - Err(CommandError::StatusCode(status, Some(data))).map_err(HIDError::Command) + Err(CommandError::StatusCode(status, Some(data)).into()) } } else if status.is_ok() { - Err(CommandError::InputTooSmall).map_err(HIDError::Command) + Err(CommandError::InputTooSmall.into()) } else { - Err(CommandError::StatusCode(status, None)).map_err(HIDError::Command) + Err(CommandError::StatusCode(status, None).into()) } } } diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index d8e329a3..ed902de4 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -322,7 +322,7 @@ impl RequestCtap2 for GetAssertion { Dev: FidoDevice + io::Read + io::Write + fmt::Debug, { if input.is_empty() { - return Err(CommandError::InputTooSmall).map_err(HIDError::Command); + return Err(CommandError::InputTooSmall.into()); } let status: StatusCode = input[0].into(); @@ -344,12 +344,12 @@ impl RequestCtap2 for GetAssertion { Ok(AssertionObject(assertions)) } else { let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; - Err(CommandError::StatusCode(status, Some(data))).map_err(HIDError::Command) + Err(CommandError::StatusCode(status, Some(data)).into()) } } else if status.is_ok() { - Err(CommandError::InputTooSmall).map_err(HIDError::Command) + Err(CommandError::InputTooSmall.into()) } else { - Err(CommandError::StatusCode(status, None)).map_err(HIDError::Command) + Err(CommandError::StatusCode(status, None).into()) } } } diff --git a/src/ctap2/commands/get_info.rs b/src/ctap2/commands/get_info.rs index 3e0ed71e..9dfc120a 100644 --- a/src/ctap2/commands/get_info.rs +++ b/src/ctap2/commands/get_info.rs @@ -118,7 +118,7 @@ impl RequestCtap2 for GetInfo { Dev: U2FDevice, { if input.is_empty() { - return Err(CommandError::InputTooSmall).map_err(HIDError::Command); + return Err(CommandError::InputTooSmall.into()); } let status: StatusCode = input[0].into(); @@ -131,13 +131,10 @@ impl RequestCtap2 for GetInfo { Ok(authenticator_info) } else { let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; - Err(HIDError::Command(CommandError::StatusCode( - status, - Some(data), - ))) + Err(CommandError::StatusCode(status, Some(data)).into()) } } else { - Err(CommandError::InputTooSmall).map_err(HIDError::Command) + Err(CommandError::InputTooSmall.into()) } } } diff --git a/src/ctap2/commands/get_next_assertion.rs b/src/ctap2/commands/get_next_assertion.rs index 6b7d146e..dd5b23ed 100644 --- a/src/ctap2/commands/get_next_assertion.rs +++ b/src/ctap2/commands/get_next_assertion.rs @@ -30,7 +30,7 @@ impl RequestCtap2 for GetNextAssertion { Dev: U2FDevice, { if input.is_empty() { - return Err(CommandError::InputTooSmall).map_err(HIDError::Command); + return Err(CommandError::InputTooSmall.into()); } let status: StatusCode = input[0].into(); @@ -42,12 +42,12 @@ impl RequestCtap2 for GetNextAssertion { Ok(assertion) } else { let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; - Err(CommandError::StatusCode(status, Some(data))).map_err(HIDError::Command) + Err(CommandError::StatusCode(status, Some(data)).into()) } } else if status.is_ok() { - Err(CommandError::InputTooSmall).map_err(HIDError::Command) + Err(CommandError::InputTooSmall.into()) } else { - Err(CommandError::StatusCode(status, None)).map_err(HIDError::Command) + Err(CommandError::StatusCode(status, None).into()) } } }