From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../rust/authenticator/src/ctap2/attestation.rs | 1129 +++++++++++++++ .../rust/authenticator/src/ctap2/client_data.rs | 339 +++++ .../src/ctap2/commands/authenticator_config.rs | 225 +++ .../src/ctap2/commands/bio_enrollment.rs | 660 +++++++++ .../authenticator/src/ctap2/commands/client_pin.rs | 851 +++++++++++ .../src/ctap2/commands/credential_management.rs | 457 ++++++ .../src/ctap2/commands/get_assertion.rs | 1494 +++++++++++++++++++ .../authenticator/src/ctap2/commands/get_info.rs | 1054 ++++++++++++++ .../src/ctap2/commands/get_next_assertion.rs | 54 + .../src/ctap2/commands/get_version.rs | 121 ++ .../src/ctap2/commands/make_credentials.rs | 1061 ++++++++++++++ .../rust/authenticator/src/ctap2/commands/mod.rs | 477 ++++++ .../rust/authenticator/src/ctap2/commands/reset.rs | 123 ++ .../authenticator/src/ctap2/commands/selection.rs | 123 ++ third_party/rust/authenticator/src/ctap2/mod.rs | 1518 ++++++++++++++++++++ .../rust/authenticator/src/ctap2/preflight.rs | 530 +++++++ third_party/rust/authenticator/src/ctap2/server.rs | 629 ++++++++ third_party/rust/authenticator/src/ctap2/utils.rs | 39 + 18 files changed, 10884 insertions(+) create mode 100644 third_party/rust/authenticator/src/ctap2/attestation.rs create mode 100644 third_party/rust/authenticator/src/ctap2/client_data.rs create mode 100644 third_party/rust/authenticator/src/ctap2/commands/authenticator_config.rs create mode 100644 third_party/rust/authenticator/src/ctap2/commands/bio_enrollment.rs create mode 100644 third_party/rust/authenticator/src/ctap2/commands/client_pin.rs create mode 100644 third_party/rust/authenticator/src/ctap2/commands/credential_management.rs create mode 100644 third_party/rust/authenticator/src/ctap2/commands/get_assertion.rs create mode 100644 third_party/rust/authenticator/src/ctap2/commands/get_info.rs create mode 100644 third_party/rust/authenticator/src/ctap2/commands/get_next_assertion.rs create mode 100644 third_party/rust/authenticator/src/ctap2/commands/get_version.rs create mode 100644 third_party/rust/authenticator/src/ctap2/commands/make_credentials.rs create mode 100644 third_party/rust/authenticator/src/ctap2/commands/mod.rs create mode 100644 third_party/rust/authenticator/src/ctap2/commands/reset.rs create mode 100644 third_party/rust/authenticator/src/ctap2/commands/selection.rs create mode 100644 third_party/rust/authenticator/src/ctap2/mod.rs create mode 100644 third_party/rust/authenticator/src/ctap2/preflight.rs create mode 100644 third_party/rust/authenticator/src/ctap2/server.rs create mode 100644 third_party/rust/authenticator/src/ctap2/utils.rs (limited to 'third_party/rust/authenticator/src/ctap2') diff --git a/third_party/rust/authenticator/src/ctap2/attestation.rs b/third_party/rust/authenticator/src/ctap2/attestation.rs new file mode 100644 index 0000000000..af33b159b7 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/attestation.rs @@ -0,0 +1,1129 @@ +use super::utils::{from_slice_stream, read_be_u16, read_be_u32, read_byte}; +use crate::crypto::COSEAlgorithm; +use crate::ctap2::server::{CredentialProtectionPolicy, RpIdHash}; +use crate::ctap2::utils::serde_parse_err; +use crate::{crypto::COSEKey, errors::AuthenticatorError}; +use base64::Engine; +use serde::ser::{Error as SerError, SerializeMap, Serializer}; +use serde::{ + de::{Error as SerdeError, Unexpected, Visitor}, + Deserialize, Deserializer, Serialize, +}; +use serde_cbor; +use std::fmt; +use std::io::{Cursor, Read}; + +#[derive(Debug, PartialEq, Eq)] +pub enum HmacSecretResponse { + /// This is returned by MakeCredential calls to display if CredRandom was + /// successfully generated + Confirmed(bool), + /// This is returned by GetAssertion: + /// AES256-CBC(shared_secret, HMAC-SHA265(CredRandom, salt1) || HMAC-SHA265(CredRandom, salt2)) + Secret(Vec), +} + +impl Serialize for HmacSecretResponse { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + HmacSecretResponse::Confirmed(x) => serializer.serialize_bool(*x), + HmacSecretResponse::Secret(x) => serializer.serialize_bytes(x), + } + } +} +impl<'de> Deserialize<'de> for HmacSecretResponse { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct HmacSecretResponseVisitor; + + impl<'de> Visitor<'de> for HmacSecretResponseVisitor { + type Value = HmacSecretResponse; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a byte array or a boolean") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: SerdeError, + { + Ok(HmacSecretResponse::Secret(v.to_vec())) + } + + fn visit_bool(self, v: bool) -> Result + where + E: SerdeError, + { + Ok(HmacSecretResponse::Confirmed(v)) + } + } + deserializer.deserialize_any(HmacSecretResponseVisitor) + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Default)] +pub struct Extension { + #[serde(rename = "credProtect", skip_serializing_if = "Option::is_none")] + pub cred_protect: Option, + #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")] + pub hmac_secret: Option, + #[serde(rename = "minPinLength", skip_serializing_if = "Option::is_none")] + pub min_pin_length: Option, +} + +impl Extension { + pub fn has_some(&self) -> bool { + self.min_pin_length.is_some() || self.hmac_secret.is_some() || self.cred_protect.is_some() + } +} + +#[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(AuthenticatorError::InternalError(String::from( + "Failed to parse AAGuid", + ))) + } 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({:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x})", + 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, + { + let mut buf = [0u8; 16]; + if v.len() != buf.len() { + return Err(E::invalid_length(v.len(), &"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, + pub credential_public_key: COSEKey, +} + +fn parse_attested_cred_data( + data: &mut R, +) -> Result { + let mut aaguid_raw = [0u8; 16]; + data.read_exact(&mut aaguid_raw) + .map_err(|_| serde_parse_err("AAGuid"))?; + let aaguid = AAGuid(aaguid_raw); + let cred_len = read_be_u16(data)?; + let mut credential_id = vec![0u8; cred_len as usize]; + data.read_exact(&mut credential_id) + .map_err(|_| serde_parse_err("CredentialId"))?; + let credential_public_key = from_slice_stream(data)?; + Ok(AttestedCredentialData { + aaguid, + credential_id, + credential_public_key, + }) +} + +bitflags! { + // Defining an exhaustive list of flags here ensures that `from_bits_truncate` is lossless and + // that `from_bits` never returns None. + pub struct AuthenticatorDataFlags: u8 { + const USER_PRESENT = 0x01; + const RESERVED_1 = 0x02; + const USER_VERIFIED = 0x04; + const RESERVED_3 = 0x08; + const RESERVED_4 = 0x10; + const RESERVED_5 = 0x20; + 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: Extension, +} + +impl AuthenticatorData { + pub fn to_vec(&self) -> Vec { + match serde_cbor::value::to_value(self) { + Ok(serde_cbor::value::Value::Bytes(out)) => out, + _ => unreachable!(), // Serialize is guaranteed to produce bytes + } + } +} + +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, input: &[u8]) -> Result + where + E: SerdeError, + { + let mut cursor = Cursor::new(input); + let mut rp_id_hash_raw = [0u8; 32]; + cursor + .read_exact(&mut rp_id_hash_raw) + .map_err(|_| serde_parse_err("32 bytes"))?; + let rp_id_hash = RpIdHash(rp_id_hash_raw); + + // preserve the flags, even if some reserved values are set. + let flags = AuthenticatorDataFlags::from_bits_truncate(read_byte(&mut cursor)?); + let counter = read_be_u32(&mut cursor)?; + let mut credential_data = None; + if flags.contains(AuthenticatorDataFlags::ATTESTED) { + credential_data = Some(parse_attested_cred_data(&mut cursor)?); + } + + let extensions = if flags.contains(AuthenticatorDataFlags::EXTENSION_DATA) { + from_slice_stream(&mut cursor)? + } else { + Default::default() + }; + + // TODO(baloo): we should check for end of buffer and raise a parse + // parse error if data is still in the buffer + Ok(AuthenticatorData { + rp_id_hash, + flags, + counter, + credential_data, + extensions, + }) + } + } + + deserializer.deserialize_bytes(AuthenticatorDataVisitor) + } +} + +impl Serialize for AuthenticatorData { + // see https://www.w3.org/TR/webauthn-2/#sctn-authenticator-data + // Authenticator Data + // Name Length (in bytes) + // rpIdHash 32 + // flags 1 + // signCount 4 + // attestedCredentialData variable (if present) + // extensions variable (if present) + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut data = Vec::new(); + data.extend(self.rp_id_hash.0); // (1) "rpIDHash", len=32 + data.extend([self.flags.bits()]); // (2) "flags", len=1 (u8) + data.extend(self.counter.to_be_bytes()); // (3) "signCount", len=4, 32-bit unsigned big-endian integer. + + if let Some(cred) = &self.credential_data { + // see https://www.w3.org/TR/webauthn-2/#sctn-attested-credential-data + // Attested Credential Data + // Name Length (in bytes) + // aaguid 16 + // credentialIdLength 2 + // credentialId L + // credentialPublicKey variable + data.extend(cred.aaguid.0); // (1) "aaguid", len=16 + data.extend((cred.credential_id.len() as u16).to_be_bytes()); // (2) "credentialIdLength", len=2, 16-bit unsigned big-endian integer + data.extend(&cred.credential_id); // (3) "credentialId", len= see (2) + data.extend( + // (4) "credentialPublicKey", len=variable + &serde_cbor::to_vec(&cred.credential_public_key) + .map_err(|_| SerError::custom("Failed to serialize auth_data"))?, + ); + } + // If we have parsed extension data, then we should serialize it even if the authenticator + // failed to set the extension data flag. + // If we don't have parsed extension data, then what we output depends on the flag. + // If the flag is set, we output the empty CBOR map. If it is not set, we output nothing. + if self.extensions.has_some() || self.flags.contains(AuthenticatorDataFlags::EXTENSION_DATA) + { + data.extend( + // (5) "extensions", len=variable + &serde_cbor::to_vec(&self.extensions) + .map_err(|_| SerError::custom("Failed to serialize auth_data"))?, + ); + } + + serializer.serialize_bytes(&data) + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +/// x509 encoded attestation certificate +pub struct AttestationCertificate(#[serde(with = "serde_bytes")] pub Vec); + +impl AsRef<[u8]> for AttestationCertificate { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +#[derive(Serialize, Deserialize, PartialEq, Eq)] +pub struct Signature(#[serde(with = "serde_bytes")] pub Vec); + +impl fmt::Debug for Signature { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let value = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&self.0); + write!(f, "Signature({value})") + } +} + +impl AsRef<[u8]> for Signature { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl From<&[u8]> for Signature { + fn from(sig: &[u8]) -> Signature { + Signature(sig.to_vec()) + } +} + +#[derive(Debug, PartialEq, Eq, Deserialize)] +// The tag and content attributes here are really for AttestationObject, which contains an +// "internally tagged" AttestationStatement. +#[serde(tag = "fmt", content = "attStmt", rename_all = "lowercase")] +pub enum AttestationStatement { + #[serde(deserialize_with = "deserialize_none_att_stmt")] + None, + Packed(AttestationStatementPacked), + #[serde(rename = "fido-u2f")] + FidoU2F(AttestationStatementFidoU2F), + // The remaining attestation statement formats are deserialized as serde_cbor::Values---we do + // not perform any validation of their contents. These are expected to be used primarily when + // anonymizing attestation objects that contain attestation statements in these formats. + #[serde(rename = "android-key")] + AndroidKey(serde_cbor::Value), + #[serde(rename = "android-safetynet")] + AndroidSafetyNet(serde_cbor::Value), + Apple(serde_cbor::Value), + Tpm(serde_cbor::Value), +} + +// AttestationStatement::None is serialized as the empty map. We need to enforce +// the emptyness condition manually while deserializing. +fn deserialize_none_att_stmt<'de, D>(deserializer: D) -> Result<(), D::Error> +where + D: Deserializer<'de>, +{ + let map = >::deserialize(deserializer)?; + + if !map.is_empty() { + return Err(D::Error::invalid_value(Unexpected::Map, &"the empty map")); + } + + Ok(()) +} + +// Not all crypto-backends currently provide "crypto::verify()", so we do not implement it yet. +// Also not sure, if we really need it. Would be a sanity-check only, to verify the signature is valid, +// before sendig it out. +// impl AttestationStatement { +// pub fn verify(&self, data: &[u8]) -> Result { +// match self { +// AttestationStatement::None => Ok(true), +// AttestationStatement::Unparsed(_) => Err(AuthenticatorError::Custom( +// "Unparsed attestation object can't be used to verify signature.".to_string(), +// )), +// AttestationStatement::FidoU2F(att) => { +// let res = crypto::verify( +// crypto::SignatureAlgorithm::ES256, +// &att.attestation_cert[0].as_ref(), +// att.sig.as_ref(), +// data, +// )?; +// Ok(res) +// } +// AttestationStatement::Packed(att) => { +// if att.alg != Alg::ES256 { +// return Err(AuthenticatorError::Custom( +// "Verification only supported for ES256".to_string(), +// )); +// } +// let res = crypto::verify( +// crypto::SignatureAlgorithm::ES256, +// att.attestation_cert[0].as_ref(), +// att.sig.as_ref(), +// data, +// )?; +// Ok(res) +// } +// } +// } +// } + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +// See https://www.w3.org/TR/webauthn-2/#sctn-fido-u2f-attestation +// u2fStmtFormat = { +// x5c: [ attestnCert: bytes ], +// sig: bytes +// } +pub struct AttestationStatementFidoU2F { + /// Certificate chain in x509 format + #[serde(rename = "x5c")] + pub attestation_cert: Vec, // (1) "x5c" + pub sig: Signature, // (2) "sig" +} + +impl AttestationStatementFidoU2F { + pub fn new(cert: &[u8], signature: &[u8]) -> Self { + AttestationStatementFidoU2F { + attestation_cert: vec![AttestationCertificate(Vec::from(cert))], + sig: Signature::from(signature), + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +// https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation +// packedStmtFormat = { +// alg: COSEAlgorithmIdentifier, +// sig: bytes, +// x5c: [ attestnCert: bytes, * (caCert: bytes) ] +// } // +// { +// alg: COSEAlgorithmIdentifier +// sig: bytes, +// } +pub struct AttestationStatementPacked { + pub alg: COSEAlgorithm, // (1) "alg" + pub sig: Signature, // (2) "sig" + /// Certificate chain in x509 format + #[serde(rename = "x5c", skip_serializing_if = "Vec::is_empty", default)] + pub attestation_cert: Vec, // (3) "x5c" +} + +// A WebAuthn attestation object is a CBOR map with keys "fmt", "attStmt", and "authData". The +// "fmt" field determines the type of "attStmt". The flatten attribute here turns the tag and +// content attributes on AttestationStatement (defined above) into expected keys for +// AttestationObject, which allows us to derive Deserialize. Like many of our other structs, the +// derived Deserialize implementation is permissive: it does not enforce CTAP2 canonical CBOR +// encoding and it allows repeated keys (the last one wins). +#[derive(Debug, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AttestationObject { + pub auth_data: AuthenticatorData, + #[serde(flatten)] + pub att_stmt: AttestationStatement, +} + +impl AttestationObject { + pub fn anonymize(&mut self) { + // Remove the attestation statement and the AAGUID from the authenticator data. + self.att_stmt = AttestationStatement::None; + if let Some(credential_data) = self.auth_data.credential_data.as_mut() { + credential_data.aaguid = AAGuid::default(); + } + } +} + +impl Serialize for AttestationObject { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let map_len = 3; + let mut map = serializer.serialize_map(Some(map_len))?; + + // CTAP2 canonical CBOR order for these entries is ("fmt", "attStmt", "authData") + // as strings are sorted by length and then lexically. + // see https://www.w3.org/TR/webauthn-2/#attestation-object + match self.att_stmt { + AttestationStatement::None => { + map.serialize_entry(&"fmt", &"none")?; // (1) "fmt" + let v = std::collections::BTreeMap::<(), ()>::new(); + map.serialize_entry(&"attStmt", &v)?; // (2) "attStmt" + } + AttestationStatement::Packed(ref v) => { + map.serialize_entry(&"fmt", &"packed")?; // (1) "fmt" + map.serialize_entry(&"attStmt", v)?; // (2) "attStmt" + } + AttestationStatement::FidoU2F(ref v) => { + map.serialize_entry(&"fmt", &"fido-u2f")?; // (1) "fmt" + map.serialize_entry(&"attStmt", v)?; // (2) "attStmt" + } + AttestationStatement::AndroidKey(ref v) => { + map.serialize_entry(&"fmt", &"android-key")?; // (1) "fmt" + map.serialize_entry(&"attStmt", v)?; // (2) "attStmt" + } + AttestationStatement::AndroidSafetyNet(ref v) => { + map.serialize_entry(&"fmt", &"android-safetynet")?; // (1) "fmt" + map.serialize_entry(&"attStmt", v)?; // (2) "attStmt" + } + AttestationStatement::Apple(ref v) => { + map.serialize_entry(&"fmt", &"apple")?; // (1) "fmt" + map.serialize_entry(&"attStmt", v)?; // (2) "attStmt" + } + AttestationStatement::Tpm(ref v) => { + map.serialize_entry(&"fmt", &"tpm")?; // (1) "fmt" + map.serialize_entry(&"attStmt", v)?; // (2) "attStmt" + } + } + map.serialize_entry(&"authData", &self.auth_data)?; // (3) "authData" + map.end() + } +} + +#[cfg(test)] +pub mod test { + use super::super::utils::from_slice_stream; + use super::*; + use crate::crypto::{COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve}; + use serde_cbor::{from_slice, to_vec}; + + const SAMPLE_ATTESTATION_STMT_NONE: [u8; 19] = [ + 0xa2, // map(2) + 0x63, // text(3) + 0x66, 0x6d, 0x74, // "fmt" + 0x64, // text(4) + 0x6e, 0x6f, 0x6e, 0x65, // "none" + 0x67, // text(7) + 0x61, 0x74, 0x74, 0x53, 0x74, 0x6d, 0x74, // "attStmt" + 0xa0, // map(0) + ]; + + const SAMPLE_ATTESTATION_STMT_FIDO_U2F: [u8; 840] = [ + 0xa2, // map(2) + 0x63, // text(3) + 0x66, 0x6d, 0x74, // "fmt" + 0x68, // text(8) + 0x66, 0x69, 0x64, 0x6f, 0x2d, 0x75, 0x32, 0x66, // "fido-u2f" + 0x67, // text(7) + 0x61, 0x74, 0x74, 0x53, 0x74, 0x6d, 0x74, // "attStmt" + 0xa2, // map(2) + 0x63, // text(3) + 0x78, 0x35, 0x63, // "x5c" + 0x81, // array(1) + 0x59, 0x02, 0xdd, // bytes(733) + 0x30, 0x82, 0x02, 0xd9, 0x30, 0x82, 0x01, 0xc1, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x09, + 0x00, 0xdf, 0x92, 0xd9, 0xc4, 0xe2, 0xed, 0x66, 0x0a, 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, 0x6f, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x53, + 0x45, 0x31, 0x12, 0x30, 0x10, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x09, 0x59, 0x75, 0x62, + 0x69, 0x63, 0x6f, 0x20, 0x41, 0x42, 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, 0x31, 0x28, 0x30, + 0x26, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x1f, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, + 0x55, 0x32, 0x46, 0x20, 0x45, 0x45, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x20, 0x31, + 0x31, 0x35, 0x35, 0x31, 0x30, 0x39, 0x35, 0x39, 0x39, 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, 0x0a, 0x18, 0x6c, 0x6e, 0x4d, 0x0a, 0x6a, 0x52, 0x8a, + 0x44, 0x90, 0x9a, 0x7a, 0x24, 0x23, 0x68, 0x70, 0x28, 0xd4, 0xc5, 0x7e, 0xcc, 0xb7, 0x17, + 0xba, 0x12, 0x80, 0xb8, 0x5c, 0x2f, 0xc1, 0xe4, 0xe0, 0x61, 0x66, 0x8c, 0x3c, 0x20, 0xae, + 0xf3, 0x33, 0x50, 0xd1, 0x96, 0x45, 0x23, 0x8a, 0x2c, 0x39, 0x0b, 0xf5, 0xdf, 0xfa, 0x34, + 0xff, 0x25, 0x50, 0x2f, 0x47, 0x0f, 0x3d, 0x40, 0xb8, 0x88, 0xa3, 0x81, 0x81, 0x30, 0x7f, + 0x30, 0x13, 0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xc4, 0x0a, 0x0d, 0x01, 0x04, + 0x05, 0x04, 0x03, 0x05, 0x04, 0x03, 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, 0x37, 0x30, 0x13, 0x06, + 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xe5, 0x1c, 0x02, 0x01, 0x01, 0x04, 0x04, 0x03, + 0x02, 0x04, 0x30, 0x30, 0x21, 0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xe5, 0x1c, + 0x01, 0x01, 0x04, 0x04, 0x12, 0x04, 0x10, 0x2f, 0xc0, 0x57, 0x9f, 0x81, 0x13, 0x47, 0xea, + 0xb1, 0x16, 0xbb, 0x5a, 0x8d, 0xb9, 0x20, 0x2a, 0x30, 0x0c, 0x06, 0x03, 0x55, 0x1d, 0x13, + 0x01, 0x01, 0xff, 0x04, 0x02, 0x30, 0x00, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, + 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, 0x00, 0x82, 0xac, 0xaf, + 0x11, 0x30, 0xa9, 0x9b, 0xd1, 0x43, 0x27, 0xd2, 0xf8, 0xf9, 0xb0, 0x41, 0xa2, 0xa0, 0x4a, + 0x66, 0x85, 0x27, 0x24, 0x22, 0xe5, 0x7b, 0x14, 0xb0, 0xb8, 0xf8, 0x3b, 0x6f, 0x15, 0x45, + 0x66, 0x4b, 0xbf, 0x55, 0x68, 0x1e, 0xaf, 0x01, 0x58, 0x72, 0x2a, 0xbf, 0xce, 0xd2, 0xe4, + 0xac, 0x63, 0x3c, 0xec, 0x09, 0x59, 0x56, 0x45, 0x24, 0xb0, 0xf2, 0xe5, 0x17, 0xdd, 0x97, + 0x10, 0x98, 0xb9, 0x89, 0x15, 0x17, 0xec, 0xd0, 0xc5, 0x53, 0xa2, 0xe4, 0x73, 0x9f, 0x9d, + 0xe1, 0x3d, 0xaf, 0xd0, 0xd5, 0xd7, 0xb8, 0xac, 0x4a, 0x37, 0xf4, 0xf2, 0xcc, 0x30, 0xef, + 0x25, 0xcb, 0x00, 0x65, 0x2d, 0x19, 0xdb, 0x69, 0xd7, 0xda, 0x57, 0xbd, 0x1a, 0x9c, 0x1d, + 0x8e, 0xd8, 0x7d, 0x46, 0xd8, 0x0d, 0x2b, 0x3b, 0xdf, 0xd1, 0xd9, 0xef, 0x9d, 0x2b, 0x68, + 0x32, 0xd4, 0xad, 0x5b, 0xcd, 0x74, 0x21, 0x4c, 0xe6, 0xa6, 0x14, 0x1d, 0x16, 0xb2, 0xe9, + 0x3a, 0xcb, 0x2c, 0x88, 0xf6, 0x0a, 0x3e, 0xb6, 0xd5, 0xf6, 0x14, 0x71, 0x97, 0x59, 0x09, + 0x37, 0x3b, 0xc6, 0x77, 0x90, 0x23, 0x24, 0x57, 0x1a, 0x57, 0x3f, 0x60, 0xf0, 0x7b, 0xbe, + 0xd1, 0x7b, 0x92, 0xc8, 0xb5, 0x9f, 0xa2, 0x82, 0x10, 0xbf, 0xa8, 0xc6, 0x01, 0x22, 0x93, + 0x00, 0x1b, 0x39, 0xef, 0xe5, 0x7b, 0xf9, 0xcb, 0x1e, 0x3a, 0xca, 0x8a, 0x41, 0x30, 0xf8, + 0x3a, 0xf8, 0x66, 0x8f, 0x73, 0xde, 0xf2, 0x71, 0x1b, 0x20, 0xdc, 0x99, 0xe8, 0xa8, 0x04, + 0xee, 0xa3, 0xf7, 0x42, 0x71, 0x97, 0xb6, 0xb4, 0x51, 0xb3, 0x73, 0x5c, 0x23, 0xbc, 0x9b, + 0x1b, 0xe2, 0x74, 0xc2, 0x6d, 0x3b, 0xf9, 0x19, 0x6f, 0x8c, 0x4a, 0x4b, 0x71, 0x5f, 0x4b, + 0x95, 0xc4, 0xdb, 0x7b, 0x97, 0xe7, 0x59, 0x4e, 0xb4, 0x65, 0x64, 0x8c, 0x1c, 0x63, 0x73, + 0x69, 0x67, // "sig" + 0x58, 0x46, // bytes(70) + 0x30, 0x44, 0x02, 0x20, 0x48, 0x5a, 0x72, 0x40, 0xdf, 0x2c, 0x1e, 0x31, 0xa5, 0xb3, 0x0b, + 0x3b, 0x2c, 0xd1, 0xad, 0xd0, 0x8d, 0xae, 0x8d, 0x7a, 0x25, 0x3e, 0xf5, 0xa6, 0x25, 0xdb, + 0x2e, 0x22, 0x1b, 0x71, 0xe5, 0x78, 0x02, 0x20, 0x45, 0xbd, 0xdc, 0x30, 0xde, 0xf4, 0x05, + 0x97, 0x5c, 0xac, 0x72, 0x58, 0x96, 0xa6, 0x00, 0x94, 0x57, 0x3a, 0xa5, 0xe8, 0x1e, 0xf4, + 0xfd, 0x30, 0xd3, 0x88, 0x11, 0x8b, 0x49, 0x97, 0xdf, 0x34, + ]; + + const SAMPLE_ATTESTATION_OBJ_PACKED: [u8; 677] = [ + 0xa3, // map(3) + 0x63, // text(3) + 0x66, 0x6D, 0x74, // "fmt" + 0x66, // text(6) + 0x70, 0x61, 0x63, 0x6b, 0x65, 0x64, // "packed" + 0x67, // text(7) + 0x61, 0x74, 0x74, 0x53, 0x74, 0x6D, 0x74, // "attStmt" + 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, 0x68, // text(8) + 0x61, 0x75, 0x74, 0x68, 0x44, 0x61, 0x74, 0x61, // "authData" + 0x58, 0x94, // bytes(148) + // 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 + // credential public key + 0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20, 0xa5, 0xfd, 0x5c, 0xe1, 0xb1, + 0xc4, 0x58, 0xc5, 0x30, 0xa5, 0x4f, 0xa6, 0x1b, 0x31, 0xbf, 0x6b, 0x04, 0xbe, 0x8b, 0x97, + 0xaf, 0xde, 0x54, 0xdd, 0x8c, 0xbb, 0x69, 0x27, 0x5a, 0x8a, 0x1b, 0xe1, 0x22, 0x58, 0x20, + 0xfa, 0x3a, 0x32, 0x31, 0xdd, 0x9d, 0xee, 0xd9, 0xd1, 0x89, 0x7b, 0xe5, 0xa6, 0x22, 0x8c, + 0x59, 0x50, 0x1e, 0x4b, 0xcd, 0x12, 0x97, 0x5d, 0x3d, 0xff, 0x73, 0x0f, 0x01, 0x27, 0x8e, + 0xa6, 0x1c, + ]; + + 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, + ]; + + const SAMPLE_AUTH_DATA_MAKE_CREDENTIAL: [u8; 164] = [ + 0x58, 0xA2, // bytes(162) + // 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 + 0xC1, // 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 + // credential public key + 0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20, 0xa5, 0xfd, 0x5c, 0xe1, 0xb1, + 0xc4, 0x58, 0xc5, 0x30, 0xa5, 0x4f, 0xa6, 0x1b, 0x31, 0xbf, 0x6b, 0x04, 0xbe, 0x8b, 0x97, + 0xaf, 0xde, 0x54, 0xdd, 0x8c, 0xbb, 0x69, 0x27, 0x5a, 0x8a, 0x1b, 0xe1, 0x22, 0x58, 0x20, + 0xfa, 0x3a, 0x32, 0x31, 0xdd, 0x9d, 0xee, 0xd9, 0xd1, 0x89, 0x7b, 0xe5, 0xa6, 0x22, 0x8c, + 0x59, 0x50, 0x1e, 0x4b, 0xcd, 0x12, 0x97, 0x5d, 0x3d, 0xff, 0x73, 0x0f, 0x01, 0x27, 0x8e, + 0xa6, 0x1c, // pub key end + // Extensions + 0xA1, // map(1) + 0x6B, // text(11) + 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // "hmac-secret" + 0xF5, // true + ]; + + const SAMPLE_AUTH_DATA_GET_ASSERTION: [u8; 229] = [ + 0x58, 0xE3, // bytes(227) + // 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 + 0xC1, // 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 + // credential public key + 0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20, 0xa5, 0xfd, 0x5c, 0xe1, 0xb1, + 0xc4, 0x58, 0xc5, 0x30, 0xa5, 0x4f, 0xa6, 0x1b, 0x31, 0xbf, 0x6b, 0x04, 0xbe, 0x8b, 0x97, + 0xaf, 0xde, 0x54, 0xdd, 0x8c, 0xbb, 0x69, 0x27, 0x5a, 0x8a, 0x1b, 0xe1, 0x22, 0x58, 0x20, + 0xfa, 0x3a, 0x32, 0x31, 0xdd, 0x9d, 0xee, 0xd9, 0xd1, 0x89, 0x7b, 0xe5, 0xa6, 0x22, 0x8c, + 0x59, 0x50, 0x1e, 0x4b, 0xcd, 0x12, 0x97, 0x5d, 0x3d, 0xff, 0x73, 0x0f, 0x01, 0x27, 0x8e, + 0xa6, 0x1c, // pub key end + // Extensions + 0xA1, // map(1) + 0x6B, // text(11) + 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // "hmac-secret" + 0x58, 0x40, // bytes(64) + 0x1F, 0x91, 0x52, 0x6C, 0xAE, 0x45, 0x6E, 0x4C, 0xBB, 0x71, 0xC4, 0xDD, 0xE7, 0xBB, 0x87, + 0x71, 0x57, 0xE6, 0xE5, 0x4D, 0xFE, 0xD3, 0x01, 0x5D, 0x7D, 0x4D, 0xBB, 0x22, 0x69, 0xAF, + 0xCD, 0xE6, 0xA9, 0x1B, 0x8D, 0x26, 0x7E, 0xBB, 0xF8, 0x48, 0xEB, 0x95, 0xA6, 0x8E, 0x79, + 0xC7, 0xAC, 0x70, 0x5E, 0x35, 0x1D, 0x54, 0x3D, 0xB0, 0x16, 0x58, 0x87, 0xD6, 0x29, 0x0F, + 0xD4, 0x7A, 0x40, 0xC4, + ]; + + pub fn create_attestation_obj() -> AttestationObject { + 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, + ], + credential_public_key: COSEKey { + alg: COSEAlgorithm::ES256, + key: COSEKeyType::EC2(COSEEC2Key { + curve: Curve::SECP256R1, + x: vec![ + 0xA5, 0xFD, 0x5C, 0xE1, 0xB1, 0xC4, 0x58, 0xC5, 0x30, 0xA5, 0x4F, + 0xA6, 0x1B, 0x31, 0xBF, 0x6B, 0x04, 0xBE, 0x8B, 0x97, 0xAF, 0xDE, + 0x54, 0xDD, 0x8C, 0xBB, 0x69, 0x27, 0x5A, 0x8A, 0x1B, 0xE1, + ], + y: vec![ + 0xFA, 0x3A, 0x32, 0x31, 0xDD, 0x9D, 0xEE, 0xD9, 0xD1, 0x89, 0x7B, + 0xE5, 0xA6, 0x22, 0x8C, 0x59, 0x50, 0x1E, 0x4B, 0xCD, 0x12, 0x97, + 0x5D, 0x3D, 0xFF, 0x73, 0x0F, 0x01, 0x27, 0x8E, 0xA6, 0x1C, + ], + }), + }, + }), + extensions: Default::default(), + }, + att_stmt: AttestationStatement::Packed(AttestationStatementPacked { + alg: COSEAlgorithm::ES256, + sig: Signature(vec![ + 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, + ])], + }), + } + } + + #[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_statement() { + let actual: AttestationStatement = from_slice(&SAMPLE_ATTESTATION_STMT_NONE).unwrap(); + let expected = AttestationStatement::None; + assert_eq!(expected, actual); + + let actual: AttestationStatement = from_slice(&SAMPLE_ATTESTATION_STMT_FIDO_U2F).unwrap(); + let expected = AttestationStatement::FidoU2F(AttestationStatementFidoU2F { + attestation_cert: vec![AttestationCertificate(vec![ + 0x30, 0x82, 0x02, 0xd9, 0x30, 0x82, 0x01, 0xc1, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, + 0x09, 0x00, 0xdf, 0x92, 0xd9, 0xc4, 0xe2, 0xed, 0x66, 0x0a, 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, 0x6f, 0x31, 0x0b, 0x30, + 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x53, 0x45, 0x31, 0x12, 0x30, 0x10, + 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x09, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, + 0x41, 0x42, 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, 0x31, 0x28, 0x30, 0x26, + 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x1f, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, + 0x55, 0x32, 0x46, 0x20, 0x45, 0x45, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x20, + 0x31, 0x31, 0x35, 0x35, 0x31, 0x30, 0x39, 0x35, 0x39, 0x39, 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, 0x0a, 0x18, 0x6c, 0x6e, 0x4d, + 0x0a, 0x6a, 0x52, 0x8a, 0x44, 0x90, 0x9a, 0x7a, 0x24, 0x23, 0x68, 0x70, 0x28, 0xd4, + 0xc5, 0x7e, 0xcc, 0xb7, 0x17, 0xba, 0x12, 0x80, 0xb8, 0x5c, 0x2f, 0xc1, 0xe4, 0xe0, + 0x61, 0x66, 0x8c, 0x3c, 0x20, 0xae, 0xf3, 0x33, 0x50, 0xd1, 0x96, 0x45, 0x23, 0x8a, + 0x2c, 0x39, 0x0b, 0xf5, 0xdf, 0xfa, 0x34, 0xff, 0x25, 0x50, 0x2f, 0x47, 0x0f, 0x3d, + 0x40, 0xb8, 0x88, 0xa3, 0x81, 0x81, 0x30, 0x7f, 0x30, 0x13, 0x06, 0x0a, 0x2b, 0x06, + 0x01, 0x04, 0x01, 0x82, 0xc4, 0x0a, 0x0d, 0x01, 0x04, 0x05, 0x04, 0x03, 0x05, 0x04, + 0x03, 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, 0x37, 0x30, 0x13, 0x06, 0x0b, 0x2b, + 0x06, 0x01, 0x04, 0x01, 0x82, 0xe5, 0x1c, 0x02, 0x01, 0x01, 0x04, 0x04, 0x03, 0x02, + 0x04, 0x30, 0x30, 0x21, 0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xe5, 0x1c, + 0x01, 0x01, 0x04, 0x04, 0x12, 0x04, 0x10, 0x2f, 0xc0, 0x57, 0x9f, 0x81, 0x13, 0x47, + 0xea, 0xb1, 0x16, 0xbb, 0x5a, 0x8d, 0xb9, 0x20, 0x2a, 0x30, 0x0c, 0x06, 0x03, 0x55, + 0x1d, 0x13, 0x01, 0x01, 0xff, 0x04, 0x02, 0x30, 0x00, 0x30, 0x0d, 0x06, 0x09, 0x2a, + 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, + 0x00, 0x82, 0xac, 0xaf, 0x11, 0x30, 0xa9, 0x9b, 0xd1, 0x43, 0x27, 0xd2, 0xf8, 0xf9, + 0xb0, 0x41, 0xa2, 0xa0, 0x4a, 0x66, 0x85, 0x27, 0x24, 0x22, 0xe5, 0x7b, 0x14, 0xb0, + 0xb8, 0xf8, 0x3b, 0x6f, 0x15, 0x45, 0x66, 0x4b, 0xbf, 0x55, 0x68, 0x1e, 0xaf, 0x01, + 0x58, 0x72, 0x2a, 0xbf, 0xce, 0xd2, 0xe4, 0xac, 0x63, 0x3c, 0xec, 0x09, 0x59, 0x56, + 0x45, 0x24, 0xb0, 0xf2, 0xe5, 0x17, 0xdd, 0x97, 0x10, 0x98, 0xb9, 0x89, 0x15, 0x17, + 0xec, 0xd0, 0xc5, 0x53, 0xa2, 0xe4, 0x73, 0x9f, 0x9d, 0xe1, 0x3d, 0xaf, 0xd0, 0xd5, + 0xd7, 0xb8, 0xac, 0x4a, 0x37, 0xf4, 0xf2, 0xcc, 0x30, 0xef, 0x25, 0xcb, 0x00, 0x65, + 0x2d, 0x19, 0xdb, 0x69, 0xd7, 0xda, 0x57, 0xbd, 0x1a, 0x9c, 0x1d, 0x8e, 0xd8, 0x7d, + 0x46, 0xd8, 0x0d, 0x2b, 0x3b, 0xdf, 0xd1, 0xd9, 0xef, 0x9d, 0x2b, 0x68, 0x32, 0xd4, + 0xad, 0x5b, 0xcd, 0x74, 0x21, 0x4c, 0xe6, 0xa6, 0x14, 0x1d, 0x16, 0xb2, 0xe9, 0x3a, + 0xcb, 0x2c, 0x88, 0xf6, 0x0a, 0x3e, 0xb6, 0xd5, 0xf6, 0x14, 0x71, 0x97, 0x59, 0x09, + 0x37, 0x3b, 0xc6, 0x77, 0x90, 0x23, 0x24, 0x57, 0x1a, 0x57, 0x3f, 0x60, 0xf0, 0x7b, + 0xbe, 0xd1, 0x7b, 0x92, 0xc8, 0xb5, 0x9f, 0xa2, 0x82, 0x10, 0xbf, 0xa8, 0xc6, 0x01, + 0x22, 0x93, 0x00, 0x1b, 0x39, 0xef, 0xe5, 0x7b, 0xf9, 0xcb, 0x1e, 0x3a, 0xca, 0x8a, + 0x41, 0x30, 0xf8, 0x3a, 0xf8, 0x66, 0x8f, 0x73, 0xde, 0xf2, 0x71, 0x1b, 0x20, 0xdc, + 0x99, 0xe8, 0xa8, 0x04, 0xee, 0xa3, 0xf7, 0x42, 0x71, 0x97, 0xb6, 0xb4, 0x51, 0xb3, + 0x73, 0x5c, 0x23, 0xbc, 0x9b, 0x1b, 0xe2, 0x74, 0xc2, 0x6d, 0x3b, 0xf9, 0x19, 0x6f, + 0x8c, 0x4a, 0x4b, 0x71, 0x5f, 0x4b, 0x95, 0xc4, 0xdb, 0x7b, 0x97, 0xe7, 0x59, 0x4e, + 0xb4, 0x65, 0x64, 0x8c, 0x1c, + ])], + sig: Signature(vec![ + 0x30, 0x44, 0x02, 0x20, 0x48, 0x5a, 0x72, 0x40, 0xdf, 0x2c, 0x1e, 0x31, 0xa5, 0xb3, + 0x0b, 0x3b, 0x2c, 0xd1, 0xad, 0xd0, 0x8d, 0xae, 0x8d, 0x7a, 0x25, 0x3e, 0xf5, 0xa6, + 0x25, 0xdb, 0x2e, 0x22, 0x1b, 0x71, 0xe5, 0x78, 0x02, 0x20, 0x45, 0xbd, 0xdc, 0x30, + 0xde, 0xf4, 0x05, 0x97, 0x5c, 0xac, 0x72, 0x58, 0x96, 0xa6, 0x00, 0x94, 0x57, 0x3a, + 0xa5, 0xe8, 0x1e, 0xf4, 0xfd, 0x30, 0xd3, 0x88, 0x11, 0x8b, 0x49, 0x97, 0xdf, 0x34, + ]), + }); + assert_eq!(expected, actual); + } + + #[test] + fn parse_attestation_object() { + let actual: AttestationObject = from_slice(&SAMPLE_ATTESTATION_OBJ_PACKED).unwrap(); + let expected = create_attestation_obj(); + + assert_eq!(expected, actual); + } + + #[test] + fn test_anonymize_att_obj() { + // Anonymize should prevent identifying data in the attestation statement from being + // serialized. + let mut att_obj = create_attestation_obj(); + + // This test assumes that the sample attestation object contains identifying information + assert_ne!(att_obj.att_stmt, AttestationStatement::None); + assert_ne!( + att_obj + .auth_data + .credential_data + .as_ref() + .expect("credential_data should be Some") + .aaguid, + AAGuid::default() + ); + + att_obj.anonymize(); + + // Write the attestation object out to bytes and read it back. The result should not + // have an attestation statement, and it should have the default AAGUID. + let encoded_att_obj = to_vec(&att_obj).expect("could not serialize anonymized att_obj"); + let att_obj: AttestationObject = + from_slice(&encoded_att_obj).expect("could not deserialize anonymized att_obj"); + + assert_eq!(att_obj.att_stmt, AttestationStatement::None); + assert_eq!( + att_obj + .auth_data + .credential_data + .as_ref() + .expect("credential_data should be Some") + .aaguid, + AAGuid::default() + ); + } + + #[test] + fn parse_reader() { + let v: Vec = vec![ + 0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72, 0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72, + ]; + let mut data = Cursor::new(v); + let value: String = from_slice_stream::<_, _, serde_cbor::Error>(&mut data).unwrap(); + assert_eq!(value, "foobar"); + let mut remaining = Vec::new(); + data.read_to_end(&mut remaining).unwrap(); + assert_eq!( + remaining.as_slice(), + &[0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72] + ); + let mut data = Cursor::new(remaining); + let value: String = from_slice_stream::<_, _, serde_cbor::Error>(&mut data).unwrap(); + assert_eq!(value, "foobar"); + let mut remaining = Vec::new(); + data.read_to_end(&mut remaining).unwrap(); + assert!(remaining.is_empty()); + } + + #[test] + fn parse_extensions() { + let auth_make: AuthenticatorData = from_slice(&SAMPLE_AUTH_DATA_MAKE_CREDENTIAL).unwrap(); + assert_eq!( + auth_make.extensions.hmac_secret, + Some(HmacSecretResponse::Confirmed(true)) + ); + let auth_get: AuthenticatorData = from_slice(&SAMPLE_AUTH_DATA_GET_ASSERTION).unwrap(); + assert_eq!( + auth_get.extensions.hmac_secret, + Some(HmacSecretResponse::Secret(vec![ + 0x1F, 0x91, 0x52, 0x6C, 0xAE, 0x45, 0x6E, 0x4C, 0xBB, 0x71, 0xC4, 0xDD, 0xE7, 0xBB, + 0x87, 0x71, 0x57, 0xE6, 0xE5, 0x4D, 0xFE, 0xD3, 0x01, 0x5D, 0x7D, 0x4D, 0xBB, 0x22, + 0x69, 0xAF, 0xCD, 0xE6, 0xA9, 0x1B, 0x8D, 0x26, 0x7E, 0xBB, 0xF8, 0x48, 0xEB, 0x95, + 0xA6, 0x8E, 0x79, 0xC7, 0xAC, 0x70, 0x5E, 0x35, 0x1D, 0x54, 0x3D, 0xB0, 0x16, 0x58, + 0x87, 0xD6, 0x29, 0x0F, 0xD4, 0x7A, 0x40, 0xC4, + ])) + ); + } + + #[test] + fn test_empty_extension_data() { + let mut parsed_auth_data: AuthenticatorData = + from_slice(&SAMPLE_AUTH_DATA_MAKE_CREDENTIAL).unwrap(); + assert!(parsed_auth_data + .flags + .contains(AuthenticatorDataFlags::EXTENSION_DATA)); + + // Remove the extension data but keep the extension data flag set. + parsed_auth_data.extensions = Default::default(); + let with_flag = to_vec(&parsed_auth_data).expect("could not serialize auth data"); + // The serialized auth data should end with an empty map (CBOR 0xA0). + assert_eq!(with_flag[with_flag.len() - 1], 0xA0); + + // Remove the extension data flag. + parsed_auth_data + .flags + .remove(AuthenticatorDataFlags::EXTENSION_DATA); + let without_flag = to_vec(&parsed_auth_data).expect("could not serialize auth data"); + // The serialized auth data should be one byte shorter. + assert!(with_flag.len() == without_flag.len() + 1); + } + + /// See: https://github.com/mozilla/authenticator-rs/issues/187 + #[test] + fn test_aaguid_output() { + let input = [ + 0xcb, 0x69, 0x48, 0x1e, 0x8f, 0xf0, 0x00, 0x39, 0x93, 0xec, 0x0a, 0x27, 0x29, 0xa1, + 0x54, 0xa8, + ]; + let expected = "AAGuid(cb69481e-8ff0-0039-93ec-0a2729a154a8)"; + let result = AAGuid::from(&input).expect("Failed to parse AAGuid"); + let res_str = format!("{result:?}"); + assert_eq!(expected, &res_str); + } + + #[test] + fn test_ad_flags_from_bits() { + // Check that AuthenticatorDataFlags is defined on the entire u8 range and that + // `from_bits_truncate` is lossless + for x in 0..=u8::MAX { + assert_eq!( + AuthenticatorDataFlags::from_bits(x), + Some(AuthenticatorDataFlags::from_bits_truncate(x)) + ); + } + } +} diff --git a/third_party/rust/authenticator/src/ctap2/client_data.rs b/third_party/rust/authenticator/src/ctap2/client_data.rs new file mode 100644 index 0000000000..872a119e0c --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/client_data.rs @@ -0,0 +1,339 @@ +use super::commands::CommandError; +use crate::transport::errors::HIDError; +use base64::Engine; +use serde::de::{self, Deserializer, Error as SerdeError, MapAccess, Visitor}; +use serde::ser::SerializeMap; +use serde::{Deserialize, 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(String), + 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) => { + map.serialize_entry(&"status", "present")?; + // Verify here, that `v` is valid base64 encoded? + // base64::decode_config(&v, base64::URL_SAFE_NO_PAD); + // For now: Let the token do that. + map.serialize_entry(&"id", &v)?; + } + } + map.end() + } +} + +impl<'de> Deserialize<'de> for TokenBinding { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct TokenBindingVisitor; + + impl<'de> Visitor<'de> for TokenBindingVisitor { + type Value = TokenBinding; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a byte string") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'de>, + { + let mut id = None; + let mut status = None; + + while let Some(key) = map.next_key()? { + match key { + "status" => { + status = Some(map.next_value()?); + } + "id" => { + id = Some(map.next_value()?); + } + k => { + return Err(M::Error::custom(format!("unexpected key: {k:?}"))); + } + } + } + + if let Some(stat) = status { + match stat { + "present" => { + if let Some(id) = id { + Ok(TokenBinding::Present(id)) + } else { + Err(SerdeError::missing_field("id")) + } + } + "supported" => Ok(TokenBinding::Supported), + k => Err(M::Error::custom(format!("unexpected status key: {k:?}"))), + } + } else { + Err(SerdeError::missing_field("status")) + } + } + } + + deserializer.deserialize_map(TokenBindingVisitor) + } +} + +/// 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"), + } + } +} + +impl<'de> Deserialize<'de> for WebauthnType { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct WebauthnTypeVisitor; + + impl<'de> Visitor<'de> for WebauthnTypeVisitor { + type Value = WebauthnType; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + match v { + "webauthn.create" => Ok(WebauthnType::Create), + "webauthn.get" => Ok(WebauthnType::Get), + _ => Err(E::custom("unexpected webauthn_type")), + } + } + } + + deserializer.deserialize_str(WebauthnTypeVisitor) + } +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] +pub struct Challenge(pub String); + +impl Challenge { + pub fn new(input: Vec) -> Self { + let value = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(input); + Challenge(value) + } +} + +impl From> for Challenge { + fn from(v: Vec) -> Challenge { + Challenge::new(v) + } +} + +impl AsRef<[u8]> for Challenge { + fn as_ref(&self) -> &[u8] { + self.0.as_bytes() + } +} + +pub type Origin = String; + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct CollectedClientData { + #[serde(rename = "type")] + pub webauthn_type: WebauthnType, + pub challenge: Challenge, + pub origin: Origin, + // It is optional, according to https://www.w3.org/TR/webauthn/#collectedclientdata-hash-of-the-serialized-client-data + // But we are serializing it, so we *have to* set crossOrigin (if not given, we have to set it to false) + // Thus, on our side, it is not optional. For deserializing, we provide a default (bool's default == False) + #[serde(rename = "crossOrigin", default)] + pub cross_origin: bool, + #[serde(rename = "tokenBinding", skip_serializing_if = "Option::is_none")] + pub token_binding: Option, +} + +impl CollectedClientData { + pub fn hash(&self) -> 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 json = json::to_vec(&self).map_err(CommandError::Json)?; + let digest = Sha256::digest(json); + Ok(ClientDataHash(digest.into())) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ClientDataHash(pub [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)] +mod test { + use super::{Challenge, ClientDataHash, CollectedClientData, TokenBinding, WebauthnType}; + use serde_json as json; + + #[test] + fn test_token_binding_status() { + let tok = TokenBinding::Present("AAECAw".to_string()); + + 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_parsing() { + let original_str = "{\"type\":\"webauthn.create\",\"challenge\":\"AAECAw\",\"origin\":\"example.com\",\"crossOrigin\":false,\"tokenBinding\":{\"status\":\"present\",\"id\":\"AAECAw\"}}"; + let parsed: CollectedClientData = serde_json::from_str(original_str).unwrap(); + let expected = CollectedClientData { + webauthn_type: WebauthnType::Create, + challenge: Challenge::new(vec![0x00, 0x01, 0x02, 0x03]), + origin: String::from("example.com"), + cross_origin: false, + token_binding: Some(TokenBinding::Present("AAECAw".to_string())), + }; + assert_eq!(parsed, expected); + + let back_again = serde_json::to_string(&expected).unwrap(); + assert_eq!(back_again, original_str); + } + + #[test] + fn test_collected_client_data_defaults() { + let cross_origin_str = "{\"type\":\"webauthn.create\",\"challenge\":\"AAECAw\",\"origin\":\"example.com\",\"crossOrigin\":false,\"tokenBinding\":{\"status\":\"present\",\"id\":\"AAECAw\"}}"; + let no_cross_origin_str = "{\"type\":\"webauthn.create\",\"challenge\":\"AAECAw\",\"origin\":\"example.com\",\"tokenBinding\":{\"status\":\"present\",\"id\":\"AAECAw\"}}"; + let parsed: CollectedClientData = serde_json::from_str(no_cross_origin_str).unwrap(); + let expected = CollectedClientData { + webauthn_type: WebauthnType::Create, + challenge: Challenge::new(vec![0x00, 0x01, 0x02, 0x03]), + origin: String::from("example.com"), + cross_origin: false, + token_binding: Some(TokenBinding::Present("AAECAw".to_string())), + }; + assert_eq!(parsed, expected); + + let back_again = serde_json::to_string(&expected).unwrap(); + assert_eq!(back_again, cross_origin_str); + } + + #[test] + fn test_collected_client_data() { + let client_data = CollectedClientData { + webauthn_type: WebauthnType::Create, + challenge: Challenge::new(vec![0x00, 0x01, 0x02, 0x03]), + origin: String::from("example.com"), + cross_origin: false, + token_binding: Some(TokenBinding::Present("AAECAw".to_string())), + }; + assert_eq!( + client_data.hash().expect("failed to serialize client data"), + // echo -n '{"type":"webauthn.create","challenge":"AAECAw","origin":"example.com","crossOrigin":false,"tokenBinding":{"status":"present","id":"AAECAw"}}' | sha256sum -t + ClientDataHash([ + 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, 0x32, + 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, 0x10, 0x87, + 0x54, 0xc3, 0x2d, 0x80 + ]) + ); + } +} diff --git a/third_party/rust/authenticator/src/ctap2/commands/authenticator_config.rs b/third_party/rust/authenticator/src/ctap2/commands/authenticator_config.rs new file mode 100644 index 0000000000..f72640d701 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/authenticator_config.rs @@ -0,0 +1,225 @@ +use super::{Command, CommandError, PinUvAuthCommand, RequestCtap2, StatusCode}; +use crate::{ + crypto::{PinUvAuthParam, PinUvAuthToken}, + ctap2::server::UserVerificationRequirement, + errors::AuthenticatorError, + transport::errors::HIDError, + AuthenticatorInfo, FidoDevice, +}; +use serde::{ser::SerializeMap, Deserialize, Serialize, Serializer}; +use serde_cbor::{de::from_slice, to_vec, Value}; + +#[derive(Debug, Clone, Deserialize)] +pub struct SetMinPINLength { + /// Minimum PIN length in code points + pub new_min_pin_length: Option, + /// RP IDs which are allowed to get this information via the minPinLength extension. + /// This parameter MUST NOT be used unless the minPinLength extension is supported. + pub min_pin_length_rpids: Option>, + /// The authenticator returns CTAP2_ERR_PIN_POLICY_VIOLATION until changePIN is successful. + pub force_change_pin: Option, +} + +impl Serialize for SetMinPINLength { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map_len = 0; + if self.new_min_pin_length.is_some() { + map_len += 1; + } + if self.min_pin_length_rpids.is_some() { + map_len += 1; + } + if self.force_change_pin.is_some() { + map_len += 1; + } + + let mut map = serializer.serialize_map(Some(map_len))?; + if let Some(new_min_pin_length) = self.new_min_pin_length { + map.serialize_entry(&0x01, &new_min_pin_length)?; + } + if let Some(min_pin_length_rpids) = &self.min_pin_length_rpids { + map.serialize_entry(&0x02, &min_pin_length_rpids)?; + } + if let Some(force_change_pin) = self.force_change_pin { + map.serialize_entry(&0x03, &force_change_pin)?; + } + map.end() + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub enum AuthConfigCommand { + EnableEnterpriseAttestation, + ToggleAlwaysUv, + SetMinPINLength(SetMinPINLength), +} + +impl AuthConfigCommand { + fn has_params(&self) -> bool { + match self { + AuthConfigCommand::EnableEnterpriseAttestation => false, + AuthConfigCommand::ToggleAlwaysUv => false, + AuthConfigCommand::SetMinPINLength(..) => true, + } + } +} + +#[derive(Debug, Serialize)] +pub enum AuthConfigResult { + Success(AuthenticatorInfo), +} + +#[derive(Debug)] +pub struct AuthenticatorConfig { + subcommand: AuthConfigCommand, // subCommand currently being requested + pin_uv_auth_param: Option, // First 16 bytes of HMAC-SHA-256 of contents using pinUvAuthToken. +} + +impl AuthenticatorConfig { + pub(crate) fn new(subcommand: AuthConfigCommand) -> Self { + Self { + subcommand, + pin_uv_auth_param: None, + } + } +} + +impl Serialize for AuthenticatorConfig { + 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 = 1; + if self.pin_uv_auth_param.is_some() { + map_len += 2; + } + if self.subcommand.has_params() { + map_len += 1; + } + + let mut map = serializer.serialize_map(Some(map_len))?; + + match &self.subcommand { + AuthConfigCommand::EnableEnterpriseAttestation => { + map.serialize_entry(&0x01, &0x01)?; + } + AuthConfigCommand::ToggleAlwaysUv => { + map.serialize_entry(&0x01, &0x02)?; + } + AuthConfigCommand::SetMinPINLength(params) => { + map.serialize_entry(&0x01, &0x03)?; + map.serialize_entry(&0x02, ¶ms)?; + } + } + + if let Some(ref pin_uv_auth_param) = self.pin_uv_auth_param { + map.serialize_entry(&0x03, &pin_uv_auth_param.pin_protocol.id())?; + map.serialize_entry(&0x04, pin_uv_auth_param)?; + } + + map.end() + } +} + +impl RequestCtap2 for AuthenticatorConfig { + type Output = (); + + fn command(&self) -> Command { + Command::AuthenticatorConfig + } + + fn wire_format(&self) -> Result, HIDError> { + let output = to_vec(&self).map_err(CommandError::Serializing)?; + trace!("client subcommmand: {:04X?}", &output); + Ok(output) + } + + fn handle_response_ctap2( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result + where + Dev: FidoDevice, + { + if input.is_empty() { + return Err(CommandError::InputTooSmall.into()); + } + + let status: StatusCode = input[0].into(); + + if status.is_ok() { + Ok(()) + } else { + let msg = if input.len() > 1 { + let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + Some(data) + } else { + None + }; + Err(CommandError::StatusCode(status, msg).into()) + } + } + + fn send_to_virtual_device( + &self, + _dev: &mut Dev, + ) -> Result { + unimplemented!() + } +} + +impl PinUvAuthCommand for AuthenticatorConfig { + fn set_pin_uv_auth_param( + &mut self, + pin_uv_auth_token: Option, + ) -> Result<(), AuthenticatorError> { + let mut param = None; + if let Some(token) = pin_uv_auth_token { + // pinUvAuthParam (0x04): the result of calling + // authenticate(pinUvAuthToken, 32Ă—0xff || 0x0d || uint8(subCommand) || subCommandParams). + let mut data = vec![0xff; 32]; + data.push(0x0D); + match &self.subcommand { + AuthConfigCommand::EnableEnterpriseAttestation => { + data.push(0x01); + } + AuthConfigCommand::ToggleAlwaysUv => { + data.push(0x02); + } + AuthConfigCommand::SetMinPINLength(params) => { + data.push(0x03); + data.extend(to_vec(params).map_err(CommandError::Serializing)?); + } + } + param = Some(token.derive(&data).map_err(CommandError::Crypto)?); + } + self.pin_uv_auth_param = param; + Ok(()) + } + + fn can_skip_user_verification( + &mut self, + authinfo: &AuthenticatorInfo, + _uv_req: UserVerificationRequirement, + ) -> bool { + !authinfo.device_is_protected() + } + + fn set_uv_option(&mut self, _uv: Option) { + /* No-op */ + } + + fn get_pin_uv_auth_param(&self) -> Option<&PinUvAuthParam> { + self.pin_uv_auth_param.as_ref() + } + + fn get_rp_id(&self) -> Option<&String> { + None + } +} diff --git a/third_party/rust/authenticator/src/ctap2/commands/bio_enrollment.rs b/third_party/rust/authenticator/src/ctap2/commands/bio_enrollment.rs new file mode 100644 index 0000000000..817bb5ac51 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/bio_enrollment.rs @@ -0,0 +1,660 @@ +use crate::{ + crypto::{PinUvAuthParam, PinUvAuthToken}, + ctap2::server::UserVerificationRequirement, + errors::AuthenticatorError, + transport::errors::HIDError, + AuthenticatorInfo, FidoDevice, +}; +use serde::{ + de::{Error as SerdeError, IgnoredAny, MapAccess, Visitor}, + ser::SerializeMap, + Deserialize, Deserializer, Serialize, Serializer, +}; +use serde_bytes::ByteBuf; +use serde_cbor::{from_slice, to_vec, Value}; +use std::fmt; + +use super::{Command, CommandError, CtapResponse, PinUvAuthCommand, RequestCtap2, StatusCode}; + +#[derive(Debug, Clone, Copy)] +pub enum BioEnrollmentModality { + Fingerprint, + Other(u8), +} + +impl From for BioEnrollmentModality { + fn from(value: u8) -> Self { + match value { + 0x01 => BioEnrollmentModality::Fingerprint, + x => BioEnrollmentModality::Other(x), + } + } +} + +impl From for u8 { + fn from(value: BioEnrollmentModality) -> Self { + match value { + BioEnrollmentModality::Fingerprint => 0x01, + BioEnrollmentModality::Other(x) => x, + } + } +} + +impl Serialize for BioEnrollmentModality { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_u8((*self).into()) + } +} + +impl<'de> Deserialize<'de> for BioEnrollmentModality { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct BioEnrollmentModalityVisitor; + + impl<'de> Visitor<'de> for BioEnrollmentModalityVisitor { + type Value = BioEnrollmentModality; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an integer") + } + + fn visit_u8(self, v: u8) -> Result + where + E: SerdeError, + { + Ok(BioEnrollmentModality::from(v)) + } + } + + deserializer.deserialize_u8(BioEnrollmentModalityVisitor) + } +} + +pub type BioTemplateId = Vec; +#[derive(Debug, Clone, Deserialize, Default)] +struct BioEnrollmentParams { + template_id: Option, // Template Identifier. + template_friendly_name: Option, // Template Friendly Name. + timeout_milliseconds: Option, // Timeout in milliSeconds. +} + +impl BioEnrollmentParams { + fn has_some(&self) -> bool { + self.template_id.is_some() + || self.template_friendly_name.is_some() + || self.timeout_milliseconds.is_some() + } +} + +impl Serialize for BioEnrollmentParams { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map_len = 0; + if self.template_id.is_some() { + map_len += 1; + } + if self.template_friendly_name.is_some() { + map_len += 1; + } + if self.timeout_milliseconds.is_some() { + map_len += 1; + } + + let mut map = serializer.serialize_map(Some(map_len))?; + if let Some(template_id) = &self.template_id { + map.serialize_entry(&0x01, &ByteBuf::from(template_id.as_slice()))?; + } + if let Some(template_friendly_name) = &self.template_friendly_name { + map.serialize_entry(&0x02, template_friendly_name)?; + } + if let Some(timeout_milliseconds) = &self.timeout_milliseconds { + map.serialize_entry(&0x03, timeout_milliseconds)?; + } + map.end() + } +} + +#[derive(Debug)] +pub enum BioEnrollmentCommand { + EnrollBegin(Option), + EnrollCaptureNextSample((BioTemplateId, Option)), + CancelCurrentEnrollment, + EnumerateEnrollments, + SetFriendlyName((BioTemplateId, String)), + RemoveEnrollment(BioTemplateId), + GetFingerprintSensorInfo, +} + +impl BioEnrollmentCommand { + fn to_id_and_param(&self) -> (u8, BioEnrollmentParams) { + let mut params = BioEnrollmentParams::default(); + match &self { + BioEnrollmentCommand::EnrollBegin(timeout) => { + params.timeout_milliseconds = *timeout; + (0x01, params) + } + BioEnrollmentCommand::EnrollCaptureNextSample((id, timeout)) => { + params.template_id = Some(id.clone()); + params.timeout_milliseconds = *timeout; + (0x02, params) + } + BioEnrollmentCommand::CancelCurrentEnrollment => (0x03, params), + BioEnrollmentCommand::EnumerateEnrollments => (0x04, params), + BioEnrollmentCommand::SetFriendlyName((id, name)) => { + params.template_id = Some(id.clone()); + params.template_friendly_name = Some(name.clone()); + (0x05, params) + } + BioEnrollmentCommand::RemoveEnrollment(id) => { + params.template_id = Some(id.clone()); + (0x06, params) + } + BioEnrollmentCommand::GetFingerprintSensorInfo => (0x07, params), + } + } +} + +#[derive(Debug)] +pub struct BioEnrollment { + /// The user verification modality being requested + modality: BioEnrollmentModality, + /// The authenticator user verification sub command currently being requested + pub(crate) subcommand: BioEnrollmentCommand, + /// First 16 bytes of HMAC-SHA-256 of contents using pinUvAuthToken. + pin_uv_auth_param: Option, + use_legacy_preview: bool, +} + +impl BioEnrollment { + pub(crate) fn new(subcommand: BioEnrollmentCommand, use_legacy_preview: bool) -> Self { + Self { + modality: BioEnrollmentModality::Fingerprint, // As per spec: Currently always "Fingerprint" + subcommand, + pin_uv_auth_param: None, + use_legacy_preview, + } + } +} + +impl Serialize for BioEnrollment { + 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; + let (id, params) = self.subcommand.to_id_and_param(); + if params.has_some() { + map_len += 1; + } + if self.pin_uv_auth_param.is_some() { + map_len += 2; + } + + let mut map = serializer.serialize_map(Some(map_len))?; + + map.serialize_entry(&0x01, &self.modality)?; // Per spec currently always Fingerprint + map.serialize_entry(&0x02, &id)?; + if params.has_some() { + map.serialize_entry(&0x03, ¶ms)?; + } + + if let Some(ref pin_uv_auth_param) = self.pin_uv_auth_param { + map.serialize_entry(&0x04, &pin_uv_auth_param.pin_protocol.id())?; + map.serialize_entry(&0x05, pin_uv_auth_param)?; + } + + map.end() + } +} + +impl PinUvAuthCommand for BioEnrollment { + fn get_rp_id(&self) -> Option<&String> { + None + } + + fn set_pin_uv_auth_param( + &mut self, + pin_uv_auth_token: Option, + ) -> Result<(), AuthenticatorError> { + let mut param = None; + if let Some(token) = pin_uv_auth_token { + // pinUvAuthParam (0x04): the result of calling + // authenticate(pinUvAuthToken, fingerprint (0x01) || uint8(subCommand) || subCommandParams). + let (id, params) = self.subcommand.to_id_and_param(); + let modality = self.modality.into(); + let mut data = vec![modality, id]; + if params.has_some() { + data.extend(to_vec(¶ms).map_err(CommandError::Serializing)?); + } + param = Some(token.derive(&data).map_err(CommandError::Crypto)?); + } + self.pin_uv_auth_param = param; + Ok(()) + } + + fn can_skip_user_verification( + &mut self, + _info: &crate::AuthenticatorInfo, + _uv: UserVerificationRequirement, + ) -> bool { + // "discouraged" does not exist for BioEnrollment + false + } + + fn set_uv_option(&mut self, _uv: Option) { + /* No-op */ + } + + fn get_pin_uv_auth_param(&self) -> Option<&PinUvAuthParam> { + self.pin_uv_auth_param.as_ref() + } +} + +impl RequestCtap2 for BioEnrollment { + type Output = BioEnrollmentResponse; + + fn command(&self) -> Command { + if self.use_legacy_preview { + Command::BioEnrollmentPreview + } else { + Command::BioEnrollment + } + } + + fn wire_format(&self) -> Result, HIDError> { + let output = to_vec(&self).map_err(CommandError::Serializing)?; + trace!("client subcommmand: {:04X?}", &output); + Ok(output) + } + + fn handle_response_ctap2( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result + where + Dev: FidoDevice, + { + if input.is_empty() { + return Err(CommandError::InputTooSmall.into()); + } + + let status: StatusCode = input[0].into(); + if status.is_ok() { + if input.len() > 1 { + trace!("parsing bio enrollment response data: {:#04X?}", &input); + let bio_enrollment = + from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + Ok(bio_enrollment) + } else { + // Some subcommands return only an OK-status without any data + Ok(BioEnrollmentResponse::default()) + } + } else { + let data: Option = if input.len() > 1 { + Some(from_slice(&input[1..]).map_err(CommandError::Deserializing)?) + } else { + None + }; + Err(CommandError::StatusCode(status, data).into()) + } + } + + fn send_to_virtual_device( + &self, + _dev: &mut Dev, + ) -> Result { + unimplemented!() + } +} + +#[derive(Debug, Copy, Clone, Serialize)] +pub enum LastEnrollmentSampleStatus { + /// Good fingerprint capture. + Ctap2EnrollFeedbackFpGood, + /// Fingerprint was too high. + Ctap2EnrollFeedbackFpTooHigh, + /// Fingerprint was too low. + Ctap2EnrollFeedbackFpTooLow, + /// Fingerprint was too left. + Ctap2EnrollFeedbackFpTooLeft, + /// Fingerprint was too right. + Ctap2EnrollFeedbackFpTooRight, + /// Fingerprint was too fast. + Ctap2EnrollFeedbackFpTooFast, + /// Fingerprint was too slow. + Ctap2EnrollFeedbackFpTooSlow, + /// Fingerprint was of poor quality. + Ctap2EnrollFeedbackFpPoorQuality, + /// Fingerprint was too skewed. + Ctap2EnrollFeedbackFpTooSkewed, + /// Fingerprint was too short. + Ctap2EnrollFeedbackFpTooShort, + /// Merge failure of the capture. + Ctap2EnrollFeedbackFpMergeFailure, + /// Fingerprint already exists. + Ctap2EnrollFeedbackFpExists, + /// (this error number is available) + Unused, + /// User did not touch/swipe the authenticator. + Ctap2EnrollFeedbackNoUserActivity, + /// User did not lift the finger off the sensor. + Ctap2EnrollFeedbackNoUserPresenceTransition, + /// Other possible failure cases that are not (yet) defined by the spec + Ctap2EnrollFeedbackOther(u8), +} + +impl<'de> Deserialize<'de> for LastEnrollmentSampleStatus { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct LastEnrollmentSampleStatusVisitor; + + impl<'de> Visitor<'de> for LastEnrollmentSampleStatusVisitor { + type Value = LastEnrollmentSampleStatus; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an integer") + } + + fn visit_u8(self, v: u8) -> Result + where + E: SerdeError, + { + match v { + 0x00 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpGood), + 0x01 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooHigh), + 0x02 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooLow), + 0x03 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooLeft), + 0x04 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooRight), + 0x05 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooFast), + 0x06 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooSlow), + 0x07 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpPoorQuality), + 0x08 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooSkewed), + 0x09 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooShort), + 0x0A => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpMergeFailure), + 0x0B => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpExists), + 0x0C => Ok(LastEnrollmentSampleStatus::Unused), + 0x0D => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackNoUserActivity), + 0x0E => { + Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackNoUserPresenceTransition) + } + x => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackOther(x)), + } + } + } + + deserializer.deserialize_u8(LastEnrollmentSampleStatusVisitor) + } +} + +#[derive(Debug, Copy, Clone, Serialize)] +pub enum FingerprintKind { + TouchSensor, + SwipeSensor, + // Not (yet) defined by the spec + Other(u8), +} + +impl<'de> Deserialize<'de> for FingerprintKind { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct FingerprintKindVisitor; + + impl<'de> Visitor<'de> for FingerprintKindVisitor { + type Value = FingerprintKind; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an integer") + } + + fn visit_u8(self, v: u8) -> Result + where + E: SerdeError, + { + match v { + 0x01 => Ok(FingerprintKind::TouchSensor), + 0x02 => Ok(FingerprintKind::SwipeSensor), + x => Ok(FingerprintKind::Other(x)), + } + } + } + + deserializer.deserialize_u8(FingerprintKindVisitor) + } +} + +#[derive(Debug, Serialize)] +pub(crate) struct BioTemplateInfo { + template_id: BioTemplateId, + template_friendly_name: Option, +} + +impl<'de> Deserialize<'de> for BioTemplateInfo { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct BioTemplateInfoResponseVisitor; + + impl<'de> Visitor<'de> for BioTemplateInfoResponseVisitor { + type Value = BioTemplateInfo; + + 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 template_id = None; // (0x01) + let mut template_friendly_name = None; // (0x02) + while let Some(key) = map.next_key()? { + match key { + 0x01 => { + if template_id.is_some() { + return Err(SerdeError::duplicate_field("template_id")); + } + template_id = Some(map.next_value::()?.into_vec()); + } + 0x02 => { + if template_friendly_name.is_some() { + return Err(SerdeError::duplicate_field("template_friendly_name")); + } + template_friendly_name = Some(map.next_value()?); + } + k => { + warn!("BioTemplateInfo: unexpected key: {:?}", k); + let _ = map.next_value::()?; + continue; + } + } + } + + if let Some(template_id) = template_id { + Ok(BioTemplateInfo { + template_id, + template_friendly_name, + }) + } else { + Err(SerdeError::missing_field("template_id")) + } + } + } + deserializer.deserialize_bytes(BioTemplateInfoResponseVisitor) + } +} + +#[derive(Default, Debug)] +pub struct BioEnrollmentResponse { + /// The user verification modality. + pub(crate) modality: Option, + /// Indicates the type of fingerprint sensor. For touch type sensor, its value is 1. For swipe type sensor its value is 2. + pub(crate) fingerprint_kind: Option, + /// Indicates the maximum good samples required for enrollment. + pub(crate) max_capture_samples_required_for_enroll: Option, + /// Template Identifier. + pub(crate) template_id: Option, + /// Last enrollment sample status. + pub(crate) last_enroll_sample_status: Option, + /// Number of more sample required for enrollment to complete + pub(crate) remaining_samples: Option, + /// Array of templateInfo’s + pub(crate) template_infos: Vec, + /// Indicates the maximum number of bytes the authenticator will accept as a templateFriendlyName. + pub(crate) max_template_friendly_name: Option, +} + +impl CtapResponse for BioEnrollmentResponse {} + +impl<'de> Deserialize<'de> for BioEnrollmentResponse { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct BioEnrollmentResponseVisitor; + + impl<'de> Visitor<'de> for BioEnrollmentResponseVisitor { + type Value = BioEnrollmentResponse; + + 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 modality = None; // (0x01) + let mut fingerprint_kind = None; // (0x02) + let mut max_capture_samples_required_for_enroll = None; // (0x03) + let mut template_id = None; // (0x04) + let mut last_enroll_sample_status = None; // (0x05) + let mut remaining_samples = None; // (0x06) + let mut template_infos = None; // (0x07) + let mut max_template_friendly_name = None; // (0x08) + + while let Some(key) = map.next_key()? { + match key { + 0x01 => { + if modality.is_some() { + return Err(SerdeError::duplicate_field("modality")); + } + modality = Some(map.next_value()?); + } + 0x02 => { + if fingerprint_kind.is_some() { + return Err(SerdeError::duplicate_field("fingerprint_kind")); + } + fingerprint_kind = Some(map.next_value()?); + } + 0x03 => { + if max_capture_samples_required_for_enroll.is_some() { + return Err(SerdeError::duplicate_field( + "max_capture_samples_required_for_enroll", + )); + } + max_capture_samples_required_for_enroll = Some(map.next_value()?); + } + 0x04 => { + if template_id.is_some() { + return Err(SerdeError::duplicate_field("template_id")); + } + template_id = Some(map.next_value::()?.into_vec()); + } + 0x05 => { + if last_enroll_sample_status.is_some() { + return Err(SerdeError::duplicate_field( + "last_enroll_sample_status", + )); + } + last_enroll_sample_status = Some(map.next_value()?); + } + 0x06 => { + if remaining_samples.is_some() { + return Err(SerdeError::duplicate_field("remaining_samples")); + } + remaining_samples = Some(map.next_value()?); + } + 0x07 => { + if template_infos.is_some() { + return Err(SerdeError::duplicate_field("template_infos")); + } + template_infos = Some(map.next_value()?); + } + 0x08 => { + if max_template_friendly_name.is_some() { + return Err(SerdeError::duplicate_field( + "max_template_friendly_name", + )); + } + max_template_friendly_name = Some(map.next_value()?); + } + k => { + warn!("BioEnrollmentResponse: unexpected key: {:?}", k); + let _ = map.next_value::()?; + continue; + } + } + } + + Ok(BioEnrollmentResponse { + modality, + fingerprint_kind, + max_capture_samples_required_for_enroll, + template_id, + last_enroll_sample_status, + remaining_samples, + template_infos: template_infos.unwrap_or_default(), + max_template_friendly_name, + }) + } + } + deserializer.deserialize_bytes(BioEnrollmentResponseVisitor) + } +} + +#[derive(Debug, Serialize)] +pub struct EnrollmentInfo { + pub template_id: Vec, + pub template_friendly_name: Option, +} + +impl From<&BioTemplateInfo> for EnrollmentInfo { + fn from(value: &BioTemplateInfo) -> Self { + Self { + template_id: value.template_id.to_vec(), + template_friendly_name: value.template_friendly_name.clone(), + } + } +} + +#[derive(Debug, Serialize)] +pub struct FingerprintSensorInfo { + pub fingerprint_kind: FingerprintKind, + pub max_capture_samples_required_for_enroll: u64, + pub max_template_friendly_name: Option, +} + +#[derive(Debug, Serialize)] +pub enum BioEnrollmentResult { + EnrollmentList(Vec), + DeleteSuccess(AuthenticatorInfo), + UpdateSuccess, + AddSuccess(AuthenticatorInfo), + FingerprintSensorInfo(FingerprintSensorInfo), + SampleStatus(LastEnrollmentSampleStatus, u64), +} diff --git a/third_party/rust/authenticator/src/ctap2/commands/client_pin.rs b/third_party/rust/authenticator/src/ctap2/commands/client_pin.rs new file mode 100644 index 0000000000..eb3b713655 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/client_pin.rs @@ -0,0 +1,851 @@ +#![allow(non_upper_case_globals)] +use super::CtapResponse; +// Note: Needed for PinUvAuthTokenPermission +// The current version of `bitflags` doesn't seem to allow +// to set this for an individual bitflag-struct. +use super::{get_info::AuthenticatorInfo, Command, CommandError, RequestCtap2, StatusCode}; +use crate::crypto::{COSEKey, CryptoError, PinUvAuthProtocol, SharedSecret}; +use crate::transport::errors::HIDError; +use crate::transport::{FidoDevice, VirtualFidoDevice}; +use serde::{ + de::{Error as SerdeError, IgnoredAny, MapAccess, Visitor}, + ser::SerializeMap, + Deserialize, Deserializer, Serialize, Serializer, +}; +use serde_bytes::{ByteBuf, Bytes}; +use serde_cbor::de::from_slice; +use serde_cbor::ser::to_vec; +use serde_cbor::Value; +use sha2::{Digest, Sha256}; +use std::convert::TryFrom; +use std::error::Error as StdErrorT; +use std::fmt; + +#[derive(Debug, Copy, Clone)] +#[repr(u8)] +pub enum PINSubcommand { + GetPinRetries = 0x01, + GetKeyAgreement = 0x02, + SetPIN = 0x03, + ChangePIN = 0x04, + GetPINToken = 0x05, // superseded by GetPinUvAuth* + GetPinUvAuthTokenUsingUvWithPermissions = 0x06, + GetUvRetries = 0x07, + GetPinUvAuthTokenUsingPinWithPermissions = 0x09, // Yes, 0x08 is missing +} + +bitflags! { + pub struct PinUvAuthTokenPermission: u8 { + const MakeCredential = 0x01; // rp_id required + const GetAssertion = 0x02; // rp_id required + const CredentialManagement = 0x04; // rp_id optional + const BioEnrollment = 0x08; // rp_id ignored + const LargeBlobWrite = 0x10; // rp_id ignored + const AuthenticatorConfiguration = 0x20; // rp_id ignored + } +} + +impl Default for PinUvAuthTokenPermission { + fn default() -> Self { + // CTAP 2.1 spec: + // If authenticatorClientPIN's getPinToken subcommand is invoked, default permissions + // of `mc` and `ga` (value 0x03) are granted for the returned pinUvAuthToken. + PinUvAuthTokenPermission::MakeCredential | PinUvAuthTokenPermission::GetAssertion + } +} + +#[derive(Debug)] +pub struct ClientPIN { + pub pin_protocol: Option, + pub subcommand: PINSubcommand, + pub key_agreement: Option, + pub pin_auth: Option>, + pub new_pin_enc: Option>, + pub pin_hash_enc: Option>, + pub permissions: Option, + pub rp_id: Option, +} + +impl Default for ClientPIN { + fn default() -> Self { + ClientPIN { + pin_protocol: None, + subcommand: PINSubcommand::GetPinRetries, + key_agreement: None, + pin_auth: None, + new_pin_enc: None, + pin_hash_enc: None, + permissions: None, + rp_id: 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 = 1; + if self.pin_protocol.is_some() { + map_len += 1; + } + 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; + } + if self.permissions.is_some() { + map_len += 1; + } + if self.rp_id.is_some() { + map_len += 1; + } + + let mut map = serializer.serialize_map(Some(map_len))?; + if let Some(ref pin_protocol) = self.pin_protocol { + map.serialize_entry(&1, &pin_protocol.id())?; + } + 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, Bytes::new(pin_auth))?; + } + if let Some(ref new_pin_enc) = self.new_pin_enc { + map.serialize_entry(&5, Bytes::new(new_pin_enc))?; + } + if let Some(ref pin_hash_enc) = self.pin_hash_enc { + map.serialize_entry(&6, Bytes::new(pin_hash_enc))?; + } + if let Some(ref permissions) = self.permissions { + map.serialize_entry(&9, permissions)?; + } + if let Some(ref rp_id) = self.rp_id { + map.serialize_entry(&0x0A, rp_id)?; + } + + map.end() + } +} + +pub trait ClientPINSubCommand { + type Output; + fn as_client_pin(&self) -> Result; + fn parse_response_payload(&self, input: &[u8]) -> Result; +} + +#[derive(Default, Debug, PartialEq, Eq)] +pub struct ClientPinResponse { + pub key_agreement: Option, + pub pin_token: Option>, + /// Number of PIN attempts remaining before lockout. + pub pin_retries: Option, + pub power_cycle_state: Option, + pub uv_retries: Option, +} + +impl CtapResponse for ClientPinResponse {} + +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 pin_retries = None; + let mut power_cycle_state = None; + let mut uv_retries = None; + while let Some(key) = map.next_key()? { + match key { + 0x01 => { + if key_agreement.is_some() { + return Err(SerdeError::duplicate_field("key_agreement")); + } + key_agreement = map.next_value()?; + } + 0x02 => { + if pin_token.is_some() { + return Err(SerdeError::duplicate_field("pin_token")); + } + let value: ByteBuf = map.next_value()?; + pin_token = Some(value.into_vec()); + } + 0x03 => { + if pin_retries.is_some() { + return Err(SerdeError::duplicate_field("pin_retries")); + } + pin_retries = Some(map.next_value()?); + } + 0x04 => { + if power_cycle_state.is_some() { + return Err(SerdeError::duplicate_field("power_cycle_state")); + } + power_cycle_state = Some(map.next_value()?); + } + 0x05 => { + if uv_retries.is_some() { + return Err(SerdeError::duplicate_field("uv_retries")); + } + uv_retries = Some(map.next_value()?); + } + k => { + warn!("ClientPinResponse: unexpected key: {:?}", k); + let _ = map.next_value::()?; + continue; + } + } + } + Ok(ClientPinResponse { + key_agreement, + pin_token, + pin_retries, + power_cycle_state, + uv_retries, + }) + } + } + deserializer.deserialize_bytes(ClientPinResponseVisitor) + } +} + +#[derive(Debug)] +pub struct GetKeyAgreement { + pin_protocol: PinUvAuthProtocol, +} + +impl GetKeyAgreement { + pub fn new(pin_protocol: PinUvAuthProtocol) -> Self { + GetKeyAgreement { pin_protocol } + } +} + +impl ClientPINSubCommand for GetKeyAgreement { + type Output = ClientPinResponse; + + fn as_client_pin(&self) -> Result { + Ok(ClientPIN { + pin_protocol: Some(self.pin_protocol.clone()), + subcommand: PINSubcommand::GetKeyAgreement, + ..ClientPIN::default() + }) + } + + fn parse_response_payload(&self, input: &[u8]) -> Result { + let get_pin_response: ClientPinResponse = + from_slice(input).map_err(CommandError::Deserializing)?; + if get_pin_response.key_agreement.is_none() { + return Err(CommandError::MissingRequiredField("key_agreement")); + } + Ok(get_pin_response) + } +} + +#[derive(Debug)] +/// Superseded by GetPinUvAuthTokenUsingUvWithPermissions or +/// GetPinUvAuthTokenUsingPinWithPermissions, thus for backwards compatibility only +pub struct GetPinToken<'sc, 'pin> { + shared_secret: &'sc SharedSecret, + pin: &'pin Pin, +} + +impl<'sc, 'pin> GetPinToken<'sc, 'pin> { + pub fn new(shared_secret: &'sc SharedSecret, pin: &'pin Pin) -> Self { + GetPinToken { shared_secret, pin } + } +} + +impl<'sc, 'pin> ClientPINSubCommand for GetPinToken<'sc, 'pin> { + type Output = ClientPinResponse; + + fn as_client_pin(&self) -> Result { + let input = self.pin.for_pin_token(); + trace!("pin_hash = {:#04X?}", &input); + let pin_hash_enc = self.shared_secret.encrypt(&input)?; + trace!("pin_hash_enc = {:#04X?}", &pin_hash_enc); + + Ok(ClientPIN { + pin_protocol: Some(self.shared_secret.pin_protocol.clone()), + subcommand: PINSubcommand::GetPINToken, + key_agreement: Some(self.shared_secret.client_input().clone()), + pin_hash_enc: Some(pin_hash_enc), + ..ClientPIN::default() + }) + } + + fn parse_response_payload(&self, input: &[u8]) -> Result { + let get_pin_response: ClientPinResponse = + from_slice(input).map_err(CommandError::Deserializing)?; + if get_pin_response.pin_token.is_none() { + return Err(CommandError::MissingRequiredField("pin_token")); + } + Ok(get_pin_response) + } +} + +#[derive(Debug)] +pub struct GetPinUvAuthTokenUsingPinWithPermissions<'sc, 'pin> { + shared_secret: &'sc SharedSecret, + pin: &'pin Pin, + permissions: PinUvAuthTokenPermission, + rp_id: Option, +} + +impl<'sc, 'pin> GetPinUvAuthTokenUsingPinWithPermissions<'sc, 'pin> { + pub fn new( + shared_secret: &'sc SharedSecret, + pin: &'pin Pin, + permissions: PinUvAuthTokenPermission, + rp_id: Option, + ) -> Self { + GetPinUvAuthTokenUsingPinWithPermissions { + shared_secret, + pin, + permissions, + rp_id, + } + } +} + +impl<'sc, 'pin> ClientPINSubCommand for GetPinUvAuthTokenUsingPinWithPermissions<'sc, 'pin> { + type Output = ClientPinResponse; + + fn as_client_pin(&self) -> Result { + let input = self.pin.for_pin_token(); + let pin_hash_enc = self.shared_secret.encrypt(&input)?; + + Ok(ClientPIN { + pin_protocol: Some(self.shared_secret.pin_protocol.clone()), + subcommand: PINSubcommand::GetPinUvAuthTokenUsingPinWithPermissions, + key_agreement: Some(self.shared_secret.client_input().clone()), + pin_hash_enc: Some(pin_hash_enc), + permissions: Some(self.permissions.bits()), + rp_id: self.rp_id.clone(), /* TODO: This could probably be done less wasteful with + * &str all the way */ + ..ClientPIN::default() + }) + } + + fn parse_response_payload(&self, input: &[u8]) -> Result { + let get_pin_response: ClientPinResponse = + from_slice(input).map_err(CommandError::Deserializing)?; + if get_pin_response.pin_token.is_none() { + return Err(CommandError::MissingRequiredField("pin_token")); + } + Ok(get_pin_response) + } +} + +macro_rules! implementRetries { + ($name:ident, $getter:ident) => { + #[derive(Debug, Default)] + pub struct $name {} + + impl $name { + pub fn new() -> Self { + Self {} + } + } + + impl ClientPINSubCommand for $name { + type Output = ClientPinResponse; + + fn as_client_pin(&self) -> Result { + Ok(ClientPIN { + subcommand: PINSubcommand::$name, + ..ClientPIN::default() + }) + } + + fn parse_response_payload(&self, input: &[u8]) -> Result { + let get_pin_response: ClientPinResponse = + from_slice(input).map_err(CommandError::Deserializing)?; + if get_pin_response.$getter.is_none() { + return Err(CommandError::MissingRequiredField(stringify!($getter))); + } + Ok(get_pin_response) + } + } + }; +} + +implementRetries!(GetPinRetries, pin_retries); +implementRetries!(GetUvRetries, uv_retries); + +#[derive(Debug)] +pub struct GetPinUvAuthTokenUsingUvWithPermissions<'sc> { + shared_secret: &'sc SharedSecret, + permissions: PinUvAuthTokenPermission, + rp_id: Option, +} + +impl<'sc> GetPinUvAuthTokenUsingUvWithPermissions<'sc> { + pub fn new( + shared_secret: &'sc SharedSecret, + permissions: PinUvAuthTokenPermission, + rp_id: Option, + ) -> Self { + GetPinUvAuthTokenUsingUvWithPermissions { + shared_secret, + permissions, + rp_id, + } + } +} + +impl<'sc> ClientPINSubCommand for GetPinUvAuthTokenUsingUvWithPermissions<'sc> { + type Output = ClientPinResponse; + + fn as_client_pin(&self) -> Result { + Ok(ClientPIN { + pin_protocol: Some(self.shared_secret.pin_protocol.clone()), + subcommand: PINSubcommand::GetPinUvAuthTokenUsingUvWithPermissions, + key_agreement: Some(self.shared_secret.client_input().clone()), + permissions: Some(self.permissions.bits()), + rp_id: self.rp_id.clone(), /* TODO: This could probably be done less wasteful with + * &str all the way */ + ..ClientPIN::default() + }) + } + + fn parse_response_payload(&self, input: &[u8]) -> Result { + let get_pin_response: ClientPinResponse = + from_slice(input).map_err(CommandError::Deserializing)?; + if get_pin_response.pin_token.is_none() { + return Err(CommandError::MissingRequiredField("pin_token")); + } + Ok(get_pin_response) + } +} + +#[derive(Debug)] +pub struct SetNewPin<'sc, 'pin> { + shared_secret: &'sc SharedSecret, + new_pin: &'pin Pin, +} + +impl<'sc, 'pin> SetNewPin<'sc, 'pin> { + pub fn new(shared_secret: &'sc SharedSecret, new_pin: &'pin Pin) -> Self { + SetNewPin { + shared_secret, + new_pin, + } + } +} + +impl<'sc, 'pin> ClientPINSubCommand for SetNewPin<'sc, 'pin> { + type Output = ClientPinResponse; + + fn as_client_pin(&self) -> Result { + if self.new_pin.as_bytes().len() > 63 { + return Err(CommandError::StatusCode( + StatusCode::PinPolicyViolation, + None, + )); + } + + // newPinEnc: the result of calling encrypt(shared secret, paddedPin) where paddedPin is + // newPin padded on the right with 0x00 bytes to make it 64 bytes long. (Since the maximum + // length of newPin is 63 bytes, there is always at least one byte of padding.) + let new_pin_padded = self.new_pin.padded(); + let new_pin_enc = self.shared_secret.encrypt(&new_pin_padded)?; + + // pinUvAuthParam: the result of calling authenticate(shared secret, newPinEnc). + let pin_auth = self.shared_secret.authenticate(&new_pin_enc)?; + + Ok(ClientPIN { + pin_protocol: Some(self.shared_secret.pin_protocol.clone()), + subcommand: PINSubcommand::SetPIN, + key_agreement: Some(self.shared_secret.client_input().clone()), + new_pin_enc: Some(new_pin_enc), + pin_auth: Some(pin_auth), + ..ClientPIN::default() + }) + } + + fn parse_response_payload(&self, input: &[u8]) -> Result { + // Should be an empty response or a valid cbor-value (which we ignore) + if !input.is_empty() { + let _: Value = from_slice(input).map_err(CommandError::Deserializing)?; + } + Ok(ClientPinResponse::default()) + } +} + +#[derive(Debug)] +pub struct ChangeExistingPin<'sc, 'pin> { + pin_protocol: PinUvAuthProtocol, + shared_secret: &'sc SharedSecret, + current_pin: &'pin Pin, + new_pin: &'pin Pin, +} + +impl<'sc, 'pin> ChangeExistingPin<'sc, 'pin> { + pub fn new( + info: &AuthenticatorInfo, + shared_secret: &'sc SharedSecret, + current_pin: &'pin Pin, + new_pin: &'pin Pin, + ) -> Result { + Ok(ChangeExistingPin { + pin_protocol: PinUvAuthProtocol::try_from(info)?, + shared_secret, + current_pin, + new_pin, + }) + } +} + +impl<'sc, 'pin> ClientPINSubCommand for ChangeExistingPin<'sc, 'pin> { + type Output = ClientPinResponse; + + fn as_client_pin(&self) -> Result { + if self.new_pin.as_bytes().len() > 63 { + return Err(CommandError::StatusCode( + StatusCode::PinPolicyViolation, + None, + )); + } + + // newPinEnc: the result of calling encrypt(shared secret, paddedPin) where paddedPin is + // newPin padded on the right with 0x00 bytes to make it 64 bytes long. (Since the maximum + // length of newPin is 63 bytes, there is always at least one byte of padding.) + let new_pin_padded = self.new_pin.padded(); + let new_pin_enc = self.shared_secret.encrypt(&new_pin_padded)?; + + let current_pin_hash = self.current_pin.for_pin_token(); + let pin_hash_enc = self.shared_secret.encrypt(current_pin_hash.as_ref())?; + + let pin_auth = self + .shared_secret + .authenticate(&[new_pin_enc.as_slice(), pin_hash_enc.as_slice()].concat())?; + + Ok(ClientPIN { + pin_protocol: Some(self.shared_secret.pin_protocol.clone()), + subcommand: PINSubcommand::ChangePIN, + key_agreement: Some(self.shared_secret.client_input().clone()), + new_pin_enc: Some(new_pin_enc), + pin_hash_enc: Some(pin_hash_enc), + pin_auth: Some(pin_auth), + permissions: None, + rp_id: None, + }) + } + + fn parse_response_payload(&self, input: &[u8]) -> Result { + // Should be an empty response or a valid cbor-value (which we ignore) + if !input.is_empty() { + let _: Value = from_slice(input).map_err(CommandError::Deserializing)?; + } + Ok(ClientPinResponse::default()) + } +} + +impl RequestCtap2 for T +where + T: ClientPINSubCommand, + T: fmt::Debug, +{ + type Output = ::Output; + + fn command(&self) -> Command { + Command::ClientPin + } + + fn wire_format(&self) -> Result, HIDError> { + let client_pin = self.as_client_pin()?; + let output = to_vec(&client_pin).map_err(CommandError::Serializing)?; + trace!("client subcommmand: {:04X?}", &output); + + Ok(output) + } + + fn handle_response_ctap2( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result { + trace!("Client pin subcomand response:{:04X?}", &input); + if input.is_empty() { + return Err(CommandError::InputTooSmall.into()); + } + + let status: StatusCode = input[0].into(); + debug!("response status code: {:?}", status); + if status.is_ok() { + ::parse_response_payload(self, &input[1..]) + .map_err(HIDError::Command) + } else { + let add_data = if input.len() > 1 { + let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + Some(data) + } else { + None + }; + Err(CommandError::StatusCode(status, add_data).into()) + } + } + + fn send_to_virtual_device( + &self, + dev: &mut Dev, + ) -> Result { + dev.client_pin(&self.as_client_pin()?) + } +} + +#[derive(Clone, Serialize, Deserialize)] +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) -> Vec { + let mut hasher = Sha256::new(); + hasher.update(self.0.as_bytes()); + + let mut output = [0u8; 16]; + let len = output.len(); + output.copy_from_slice(&hasher.finalize().as_slice()[..len]); + + output.to_vec() + } + + pub fn padded(&self) -> Vec { + let mut out = self.0.as_bytes().to_vec(); + out.resize(64, 0x00); + out + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[derive(Clone, Debug, Serialize)] +pub enum PinError { + PinRequired, + PinIsTooShort, + PinIsTooLong(usize), + InvalidPin(Option), + InvalidUv(Option), + PinAuthBlocked, + PinBlocked, + PinNotSet, + UvBlocked, + /// Used for CTAP2.0 UV (fingerprints) + PinAuthInvalid, + Crypto(CryptoError), +} + +impl fmt::Display for PinError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + PinError::PinRequired => write!(f, "Pin required."), + PinError::PinIsTooShort => write!(f, "pin is too short"), + PinError::PinIsTooLong(len) => write!(f, "pin is too long ({len})"), + PinError::InvalidPin(ref e) => { + let mut res = write!(f, "Invalid Pin."); + if let Some(pin_retries) = e { + res = write!(f, " Retries left: {pin_retries:?}") + } + res + } + PinError::InvalidUv(ref e) => { + let mut res = write!(f, "Invalid Uv."); + if let Some(uv_retries) = e { + res = write!(f, " Retries left: {uv_retries:?}") + } + res + } + PinError::PinAuthBlocked => { + write!(f, "Pin authentication blocked. Device needs power cycle.") + } + PinError::PinBlocked => write!(f, "No retries left. Pin blocked. Device needs reset."), + PinError::PinNotSet => write!(f, "Pin needed but not set on device."), + PinError::UvBlocked => write!(f, "No retries left. Uv blocked. Device needs reset."), + PinError::PinAuthInvalid => write!(f, "PinAuth invalid."), + PinError::Crypto(ref e) => write!(f, "Crypto backend error: {e:?}"), + } + } +} + +impl StdErrorT for PinError {} + +impl From for PinError { + fn from(e: CryptoError) -> Self { + PinError::Crypto(e) + } +} + +#[cfg(test)] +mod test { + use super::ClientPinResponse; + use crate::crypto::{COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve}; + use serde_cbor::de::from_slice; + + #[test] + fn test_get_key_agreement() { + let reference = [ + 161, 1, 165, 1, 2, 3, 56, 24, 32, 1, 33, 88, 32, 115, 222, 167, 5, 88, 238, 119, 202, + 121, 23, 241, 150, 9, 48, 197, 136, 174, 0, 17, 90, 190, 83, 65, 103, 237, 97, 41, 213, + 128, 111, 7, 106, 34, 88, 32, 248, 204, 9, 26, 82, 96, 25, 72, 5, 82, 251, 185, 22, 39, + 246, 149, 54, 246, 255, 225, 52, 102, 67, 221, 113, 194, 236, 213, 199, 147, 180, 81, + ]; + let expected = ClientPinResponse { + key_agreement: Some(COSEKey { + alg: COSEAlgorithm::ECDH_ES_HKDF256, + key: COSEKeyType::EC2(COSEEC2Key { + curve: Curve::SECP256R1, + x: vec![ + 115, 222, 167, 5, 88, 238, 119, 202, 121, 23, 241, 150, 9, 48, 197, 136, + 174, 0, 17, 90, 190, 83, 65, 103, 237, 97, 41, 213, 128, 111, 7, 106, + ], + y: vec![ + 248, 204, 9, 26, 82, 96, 25, 72, 5, 82, 251, 185, 22, 39, 246, 149, 54, + 246, 255, 225, 52, 102, 67, 221, 113, 194, 236, 213, 199, 147, 180, 81, + ], + }), + }), + pin_token: None, + pin_retries: None, + power_cycle_state: None, + uv_retries: None, + }; + let result: ClientPinResponse = + from_slice(&reference).expect("could not deserialize reference"); + assert_eq!(expected, result); + } + + #[test] + fn test_get_pin_retries() { + let reference = [161, 3, 7]; + let expected = ClientPinResponse { + key_agreement: None, + pin_token: None, + pin_retries: Some(7), + power_cycle_state: None, + uv_retries: None, + }; + let result: ClientPinResponse = + from_slice(&reference).expect("could not deserialize reference"); + assert_eq!(expected, result); + } + + #[test] + fn test_get_uv_retries() { + let reference = [161, 5, 2]; + let expected = ClientPinResponse { + key_agreement: None, + pin_token: None, + pin_retries: None, + power_cycle_state: None, + uv_retries: Some(2), + }; + let result: ClientPinResponse = + from_slice(&reference).expect("could not deserialize reference"); + assert_eq!(expected, result); + } + + #[test] + fn test_get_pin_token() { + let reference = [ + 161, 2, 88, 48, 173, 244, 214, 87, 128, 57, 25, 99, 142, 140, 41, 25, 94, 60, 75, 163, + 240, 187, 211, 138, 11, 208, 74, 117, 180, 181, 97, 31, 79, 252, 191, 244, 49, 13, 201, + 217, 204, 219, 122, 3, 101, 4, 70, 26, 14, 41, 150, 148, + ]; + let expected = ClientPinResponse { + key_agreement: None, + pin_token: Some(vec![ + 173, 244, 214, 87, 128, 57, 25, 99, 142, 140, 41, 25, 94, 60, 75, 163, 240, 187, + 211, 138, 11, 208, 74, 117, 180, 181, 97, 31, 79, 252, 191, 244, 49, 13, 201, 217, + 204, 219, 122, 3, 101, 4, 70, 26, 14, 41, 150, 148, + ]), + pin_retries: None, + power_cycle_state: None, + uv_retries: None, + }; + let result: ClientPinResponse = + from_slice(&reference).expect("could not deserialize reference"); + assert_eq!(expected, result); + } + + #[test] + fn test_get_puat_using_uv() { + let reference = [ + 161, 2, 88, 48, 94, 109, 192, 236, 90, 161, 77, 153, 23, 146, 179, 189, 133, 106, 76, + 150, 17, 238, 155, 102, 107, 201, 98, 232, 184, 33, 153, 224, 203, 87, 147, 10, 21, 20, + 85, 184, 109, 61, 240, 58, 236, 198, 171, 48, 242, 165, 221, 214, + ]; + let expected = ClientPinResponse { + key_agreement: None, + pin_token: Some(vec![ + 94, 109, 192, 236, 90, 161, 77, 153, 23, 146, 179, 189, 133, 106, 76, 150, 17, 238, + 155, 102, 107, 201, 98, 232, 184, 33, 153, 224, 203, 87, 147, 10, 21, 20, 85, 184, + 109, 61, 240, 58, 236, 198, 171, 48, 242, 165, 221, 214, + ]), + pin_retries: None, + power_cycle_state: None, + uv_retries: None, + }; + let result: ClientPinResponse = + from_slice(&reference).expect("could not deserialize reference"); + assert_eq!(expected, result); + } + + #[test] + fn test_get_puat_using_pin() { + let reference = [ + 161, 2, 88, 48, 143, 174, 68, 241, 186, 39, 106, 238, 129, 15, 181, 102, 112, 130, 239, + 96, 106, 235, 3, 10, 61, 173, 106, 252, 38, 236, 44, 112, 91, 34, 218, 136, 139, 118, + 162, 178, 172, 227, 82, 103, 136, 91, 136, 178, 170, 233, 156, 62, + ]; + let expected = ClientPinResponse { + key_agreement: None, + pin_token: Some(vec![ + 143, 174, 68, 241, 186, 39, 106, 238, 129, 15, 181, 102, 112, 130, 239, 96, 106, + 235, 3, 10, 61, 173, 106, 252, 38, 236, 44, 112, 91, 34, 218, 136, 139, 118, 162, + 178, 172, 227, 82, 103, 136, 91, 136, 178, 170, 233, 156, 62, + ]), + pin_retries: None, + power_cycle_state: None, + uv_retries: None, + }; + let result: ClientPinResponse = + from_slice(&reference).expect("could not deserialize reference"); + assert_eq!(expected, result); + } +} diff --git a/third_party/rust/authenticator/src/ctap2/commands/credential_management.rs b/third_party/rust/authenticator/src/ctap2/commands/credential_management.rs new file mode 100644 index 0000000000..2a58bb1791 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/credential_management.rs @@ -0,0 +1,457 @@ +use super::{Command, CommandError, CtapResponse, PinUvAuthCommand, RequestCtap2, StatusCode}; +use crate::{ + crypto::{COSEKey, PinUvAuthParam, PinUvAuthToken}, + ctap2::server::{ + PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity, RelyingParty, RpIdHash, + UserVerificationRequirement, + }, + errors::AuthenticatorError, + transport::errors::HIDError, + FidoDevice, +}; +use serde::{ + de::{Error as SerdeError, IgnoredAny, MapAccess, Visitor}, + ser::SerializeMap, + Deserialize, Deserializer, Serialize, Serializer, +}; +use serde_bytes::ByteBuf; +use serde_cbor::{de::from_slice, to_vec, Value}; +use std::fmt; + +#[derive(Debug, Clone, Deserialize, Default)] +struct CredManagementParams { + rp_id_hash: Option, // RP ID SHA-256 hash + credential_id: Option, // Credential Identifier + user: Option, // User Entity +} + +impl CredManagementParams { + fn has_some(&self) -> bool { + self.rp_id_hash.is_some() || self.credential_id.is_some() || self.user.is_some() + } +} + +impl Serialize for CredManagementParams { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map_len = 0; + if self.rp_id_hash.is_some() { + map_len += 1; + } + if self.credential_id.is_some() { + map_len += 1; + } + if self.user.is_some() { + map_len += 1; + } + + let mut map = serializer.serialize_map(Some(map_len))?; + if let Some(rp_id_hash) = &self.rp_id_hash { + map.serialize_entry(&0x01, &ByteBuf::from(rp_id_hash.as_ref()))?; + } + if let Some(credential_id) = &self.credential_id { + map.serialize_entry(&0x02, credential_id)?; + } + if let Some(user) = &self.user { + map.serialize_entry(&0x03, user)?; + } + map.end() + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub(crate) enum CredManagementCommand { + GetCredsMetadata, + EnumerateRPsBegin, + EnumerateRPsGetNextRP, + EnumerateCredentialsBegin(RpIdHash), + EnumerateCredentialsGetNextCredential, + DeleteCredential(PublicKeyCredentialDescriptor), + UpdateUserInformation((PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity)), +} + +impl CredManagementCommand { + fn to_id_and_param(&self) -> (u8, CredManagementParams) { + let mut params = CredManagementParams::default(); + match &self { + CredManagementCommand::GetCredsMetadata => (0x01, params), + CredManagementCommand::EnumerateRPsBegin => (0x02, params), + CredManagementCommand::EnumerateRPsGetNextRP => (0x03, params), + CredManagementCommand::EnumerateCredentialsBegin(rp_id_hash) => { + params.rp_id_hash = Some(rp_id_hash.clone()); + (0x04, params) + } + CredManagementCommand::EnumerateCredentialsGetNextCredential => (0x05, params), + CredManagementCommand::DeleteCredential(cred_id) => { + params.credential_id = Some(cred_id.clone()); + (0x06, params) + } + CredManagementCommand::UpdateUserInformation((cred_id, user)) => { + params.credential_id = Some(cred_id.clone()); + params.user = Some(user.clone()); + (0x07, params) + } + } + } +} +#[derive(Debug)] +pub struct CredentialManagement { + pub(crate) subcommand: CredManagementCommand, // subCommand currently being requested + pin_uv_auth_param: Option, // First 16 bytes of HMAC-SHA-256 of contents using pinUvAuthToken. + use_legacy_preview: bool, +} + +impl CredentialManagement { + pub(crate) fn new(subcommand: CredManagementCommand, use_legacy_preview: bool) -> Self { + Self { + subcommand, + pin_uv_auth_param: None, + use_legacy_preview, + } + } +} + +impl Serialize for CredentialManagement { + 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 = 1; + let (id, params) = self.subcommand.to_id_and_param(); + if params.has_some() { + map_len += 1; + } + if self.pin_uv_auth_param.is_some() { + map_len += 2; + } + + let mut map = serializer.serialize_map(Some(map_len))?; + + map.serialize_entry(&0x01, &id)?; + if params.has_some() { + map.serialize_entry(&0x02, ¶ms)?; + } + + if let Some(ref pin_uv_auth_param) = self.pin_uv_auth_param { + map.serialize_entry(&0x03, &pin_uv_auth_param.pin_protocol.id())?; + map.serialize_entry(&0x04, pin_uv_auth_param)?; + } + + map.end() + } +} + +#[derive(Debug, Default)] +pub struct CredentialManagementResponse { + /// Number of existing discoverable credentials present on the authenticator. + pub existing_resident_credentials_count: Option, + /// Number of maximum possible remaining discoverable credentials which can be created on the authenticator. + pub max_possible_remaining_resident_credentials_count: Option, + /// RP Information + pub rp: Option, + /// RP ID SHA-256 hash + pub rp_id_hash: Option, + /// Total number of RPs present on the authenticator + pub total_rps: Option, + /// User Information + pub user: Option, + /// Credential ID + pub credential_id: Option, + /// Public key of the credential. + pub public_key: Option, + /// Total number of credentials present on the authenticator for the RP in question + pub total_credentials: Option, + /// Credential protection policy. + pub cred_protect: Option, + /// Large blob encryption key. + pub large_blob_key: Option>, +} + +impl CtapResponse for CredentialManagementResponse {} + +#[derive(Debug, PartialEq, Eq, Serialize)] +pub struct CredentialRpListEntry { + /// RP Information + pub rp: RelyingParty, + /// RP ID SHA-256 hash + pub rp_id_hash: RpIdHash, + pub credentials: Vec, +} + +#[derive(Debug, PartialEq, Eq, Serialize)] +pub struct CredentialListEntry { + /// User Information + pub user: PublicKeyCredentialUserEntity, + /// Credential ID + pub credential_id: PublicKeyCredentialDescriptor, + /// Public key of the credential. + pub public_key: COSEKey, + /// Credential protection policy. + pub cred_protect: u64, + /// Large blob encryption key. + pub large_blob_key: Option>, +} + +#[derive(Debug, Serialize)] +pub enum CredentialManagementResult { + CredentialList(CredentialList), + DeleteSucess, + UpdateSuccess, +} + +#[derive(Debug, Default, Serialize)] +pub struct CredentialList { + /// Number of existing discoverable credentials present on the authenticator. + pub existing_resident_credentials_count: u64, + /// Number of maximum possible remaining discoverable credentials which can be created on the authenticator. + pub max_possible_remaining_resident_credentials_count: u64, + /// The found credentials + pub credential_list: Vec, +} + +impl CredentialList { + pub fn new() -> Self { + Default::default() + } +} + +impl<'de> Deserialize<'de> for CredentialManagementResponse { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct CredentialManagementResponseVisitor; + + impl<'de> Visitor<'de> for CredentialManagementResponseVisitor { + type Value = CredentialManagementResponse; + + 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 existing_resident_credentials_count = None; // (0x01) Unsigned Integer Number of existing discoverable credentials present on the authenticator. + let mut max_possible_remaining_resident_credentials_count = None; // (0x02) Unsigned Integer Number of maximum possible remaining discoverable credentials which can be created on the authenticator. + let mut rp = None; // (0x03) PublicKeyCredentialRpEntity RP Information + let mut rp_id_hash = None; // (0x04) Byte String RP ID SHA-256 hash + let mut total_rps = None; // (0x05) Unsigned Integer total number of RPs present on the authenticator + let mut user = None; // (0x06) PublicKeyCredentialUserEntity User Information + let mut credential_id = None; // (0x07) PublicKeyCredentialDescriptor PublicKeyCredentialDescriptor + let mut public_key = None; // (0x08) COSE_Key Public key of the credential. + let mut total_credentials = None; // (0x09) Unsigned Integer Total number of credentials present on the authenticator for the RP in question + let mut cred_protect = None; // (0x0A) Unsigned Integer Credential protection policy. + let mut large_blob_key = None; // (0x0B) Byte string Large blob encryption key. + + while let Some(key) = map.next_key()? { + match key { + 0x01 => { + if existing_resident_credentials_count.is_some() { + return Err(SerdeError::duplicate_field( + "existing_resident_credentials_count", + )); + } + existing_resident_credentials_count = Some(map.next_value()?); + } + 0x02 => { + if max_possible_remaining_resident_credentials_count.is_some() { + return Err(SerdeError::duplicate_field( + "max_possible_remaining_resident_credentials_count", + )); + } + max_possible_remaining_resident_credentials_count = + Some(map.next_value()?); + } + 0x03 => { + if rp.is_some() { + return Err(SerdeError::duplicate_field("rp")); + } + rp = Some(map.next_value()?); + } + 0x04 => { + if rp_id_hash.is_some() { + return Err(SerdeError::duplicate_field("rp_id_hash")); + } + let rp_raw = map.next_value::()?; + rp_id_hash = + Some(RpIdHash::from(rp_raw.as_slice()).map_err(|_| { + SerdeError::invalid_length(rp_raw.len(), &"32") + })?); + } + 0x05 => { + if total_rps.is_some() { + return Err(SerdeError::duplicate_field("total_rps")); + } + total_rps = Some(map.next_value()?); + } + 0x06 => { + if user.is_some() { + return Err(SerdeError::duplicate_field("user")); + } + user = Some(map.next_value()?); + } + 0x07 => { + if credential_id.is_some() { + return Err(SerdeError::duplicate_field("credential_id")); + } + credential_id = Some(map.next_value()?); + } + 0x08 => { + if public_key.is_some() { + return Err(SerdeError::duplicate_field("public_key")); + } + public_key = Some(map.next_value()?); + } + 0x09 => { + if total_credentials.is_some() { + return Err(SerdeError::duplicate_field("total_credentials")); + } + total_credentials = Some(map.next_value()?); + } + 0x0A => { + if cred_protect.is_some() { + return Err(SerdeError::duplicate_field("cred_protect")); + } + cred_protect = Some(map.next_value()?); + } + 0x0B => { + if large_blob_key.is_some() { + return Err(SerdeError::duplicate_field("large_blob_key")); + } + // Using into_vec, to avoid any copy of large_blob_key + large_blob_key = Some(map.next_value::()?.into_vec()); + } + + k => { + warn!("ClientPinResponse: unexpected key: {:?}", k); + let _ = map.next_value::()?; + continue; + } + } + } + + Ok(CredentialManagementResponse { + existing_resident_credentials_count, + max_possible_remaining_resident_credentials_count, + rp, + rp_id_hash, + total_rps, + user, + credential_id, + public_key, + total_credentials, + cred_protect, + large_blob_key, + }) + } + } + deserializer.deserialize_bytes(CredentialManagementResponseVisitor) + } +} + +impl RequestCtap2 for CredentialManagement { + type Output = CredentialManagementResponse; + + fn command(&self) -> Command { + if self.use_legacy_preview { + Command::CredentialManagementPreview + } else { + Command::CredentialManagement + } + } + + fn wire_format(&self) -> Result, HIDError> { + let output = to_vec(&self).map_err(CommandError::Serializing)?; + trace!("client subcommmand: {:04X?}", &output); + Ok(output) + } + + fn handle_response_ctap2( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result + where + Dev: FidoDevice, + { + if input.is_empty() { + return Err(CommandError::InputTooSmall.into()); + } + + let status: StatusCode = input[0].into(); + + if status.is_ok() { + if input.len() > 1 { + trace!("parsing credential management data: {:#04X?}", &input); + let credential_management = + from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + Ok(credential_management) + } else { + // Some subcommands return only an OK-status without any data + Ok(CredentialManagementResponse::default()) + } + } else { + let data: Option = if input.len() > 1 { + Some(from_slice(&input[1..]).map_err(CommandError::Deserializing)?) + } else { + None + }; + Err(CommandError::StatusCode(status, data).into()) + } + } + + fn send_to_virtual_device( + &self, + _dev: &mut Dev, + ) -> Result { + unimplemented!() + } +} + +impl PinUvAuthCommand for CredentialManagement { + fn get_rp_id(&self) -> Option<&String> { + None + } + + fn set_pin_uv_auth_param( + &mut self, + pin_uv_auth_token: Option, + ) -> Result<(), AuthenticatorError> { + let mut param = None; + if let Some(token) = pin_uv_auth_token { + // pinUvAuthParam (0x04): the result of calling + // authenticate(pinUvAuthToken, uint8(subCommand) || subCommandParams). + let (id, params) = self.subcommand.to_id_and_param(); + let mut data = vec![id]; + if params.has_some() { + data.extend(to_vec(¶ms).map_err(CommandError::Serializing)?); + } + param = Some(token.derive(&data).map_err(CommandError::Crypto)?); + } + self.pin_uv_auth_param = param; + Ok(()) + } + + fn can_skip_user_verification( + &mut self, + _info: &crate::AuthenticatorInfo, + _uv: UserVerificationRequirement, + ) -> bool { + // "discouraged" does not exist for AuthenticatorConfig + false + } + + fn set_uv_option(&mut self, _uv: Option) { + /* No-op */ + } + + fn get_pin_uv_auth_param(&self) -> Option<&PinUvAuthParam> { + self.pin_uv_auth_param.as_ref() + } +} diff --git a/third_party/rust/authenticator/src/ctap2/commands/get_assertion.rs b/third_party/rust/authenticator/src/ctap2/commands/get_assertion.rs new file mode 100644 index 0000000000..54d7d4919f --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/get_assertion.rs @@ -0,0 +1,1494 @@ +use super::get_info::AuthenticatorInfo; +use super::{ + Command, CommandError, CtapResponse, PinUvAuthCommand, RequestCtap1, RequestCtap2, Retryable, + StatusCode, +}; +use crate::consts::{ + PARAMETER_SIZE, U2F_AUTHENTICATE, U2F_DONT_ENFORCE_USER_PRESENCE_AND_SIGN, + U2F_REQUEST_USER_PRESENCE, +}; +use crate::crypto::{COSEKey, CryptoError, PinUvAuthParam, PinUvAuthToken, SharedSecret}; +use crate::ctap2::attestation::{AuthenticatorData, AuthenticatorDataFlags}; +use crate::ctap2::client_data::ClientDataHash; +use crate::ctap2::commands::get_next_assertion::GetNextAssertion; +use crate::ctap2::commands::make_credentials::UserVerification; +use crate::ctap2::server::{ + AuthenticationExtensionsClientInputs, AuthenticationExtensionsClientOutputs, + AuthenticatorAttachment, PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity, + RelyingParty, RpIdHash, UserVerificationRequirement, +}; +use crate::ctap2::utils::{read_be_u32, read_byte}; +use crate::errors::AuthenticatorError; +use crate::transport::errors::{ApduErrorStatus, HIDError}; +use crate::transport::{FidoDevice, VirtualFidoDevice}; +use crate::u2ftypes::CTAP1RequestAPDU; +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 std::fmt; +use std::io::Cursor; + +#[derive(Clone, Copy, Debug, Serialize)] +#[cfg_attr(test, derive(Deserialize))] +pub struct GetAssertionOptions { + #[serde(rename = "uv", skip_serializing_if = "Option::is_none")] + pub user_verification: Option, + #[serde(rename = "up", skip_serializing_if = "Option::is_none")] + pub user_presence: Option, +} + +impl Default for GetAssertionOptions { + fn default() -> Self { + Self { + user_presence: Some(true), + user_verification: None, + } + } +} + +impl GetAssertionOptions { + pub(crate) fn has_some(&self) -> bool { + self.user_presence.is_some() || self.user_verification.is_some() + } +} + +impl UserVerification for GetAssertionOptions { + fn ask_user_verification(&self) -> bool { + if let Some(e) = self.user_verification { + e + } else { + false + } + } +} + +#[derive(Debug, Clone)] +pub struct CalculatedHmacSecretExtension { + pub public_key: COSEKey, + pub salt_enc: Vec, + pub salt_auth: Vec, +} + +#[derive(Debug, Clone, Default)] +pub struct HmacSecretExtension { + pub salt1: Vec, + pub salt2: Option>, + calculated_hmac: Option, +} + +impl HmacSecretExtension { + pub fn new(salt1: Vec, salt2: Option>) -> Self { + HmacSecretExtension { + salt1, + salt2, + calculated_hmac: None, + } + } + + pub fn calculate(&mut self, secret: &SharedSecret) -> Result<(), AuthenticatorError> { + if self.salt1.len() < 32 { + return Err(CryptoError::WrongSaltLength.into()); + } + let salt_enc = match &self.salt2 { + Some(salt2) => { + if salt2.len() < 32 { + return Err(CryptoError::WrongSaltLength.into()); + } + let salts = [&self.salt1[..32], &salt2[..32]].concat(); // salt1 || salt2 + secret.encrypt(&salts) + } + None => secret.encrypt(&self.salt1[..32]), + }?; + let salt_auth = secret.authenticate(&salt_enc)?; + let public_key = secret.client_input().clone(); + self.calculated_hmac = Some(CalculatedHmacSecretExtension { + public_key, + salt_enc, + salt_auth, + }); + + Ok(()) + } +} + +impl Serialize for HmacSecretExtension { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if let Some(calc) = &self.calculated_hmac { + let mut map = serializer.serialize_map(Some(3))?; + map.serialize_entry(&1, &calc.public_key)?; + map.serialize_entry(&2, serde_bytes::Bytes::new(&calc.salt_enc))?; + map.serialize_entry(&3, serde_bytes::Bytes::new(&calc.salt_auth))?; + map.end() + } else { + Err(SerError::custom( + "hmac secret has not been calculated before being serialized", + )) + } + } +} + +#[derive(Debug, Default, Clone, Serialize)] +pub struct GetAssertionExtensions { + #[serde(skip_serializing)] + pub app_id: Option, + #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")] + pub hmac_secret: Option, +} + +impl From for GetAssertionExtensions { + fn from(input: AuthenticationExtensionsClientInputs) -> Self { + Self { + app_id: input.app_id, + ..Default::default() + } + } +} + +impl GetAssertionExtensions { + fn has_content(&self) -> bool { + self.hmac_secret.is_some() + } +} + +#[derive(Debug, Clone)] +pub struct GetAssertion { + pub client_data_hash: ClientDataHash, + pub rp: RelyingParty, + pub 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. + pub extensions: GetAssertionExtensions, + pub options: GetAssertionOptions, + pub pin_uv_auth_param: Option, +} + +impl GetAssertion { + pub fn new( + client_data_hash: ClientDataHash, + rp: RelyingParty, + allow_list: Vec, + options: GetAssertionOptions, + extensions: GetAssertionExtensions, + ) -> Self { + Self { + client_data_hash, + rp, + allow_list, + extensions, + options, + pin_uv_auth_param: None, + } + } + + pub fn finalize_result(&self, dev: &Dev, result: &mut GetAssertionResult) { + result.attachment = match dev.get_authenticator_info() { + Some(info) if info.options.platform_device => AuthenticatorAttachment::Platform, + Some(_) => AuthenticatorAttachment::CrossPlatform, + None => AuthenticatorAttachment::Unknown, + }; + + // Handle extensions whose outputs are not encoded in the authenticator data. + // 1. appId + if let Some(app_id) = &self.extensions.app_id { + result.extensions.app_id = + Some(result.assertion.auth_data.rp_id_hash == RelyingParty::from(app_id).hash()); + } + } +} + +impl PinUvAuthCommand for GetAssertion { + fn set_pin_uv_auth_param( + &mut self, + pin_uv_auth_token: Option, + ) -> Result<(), AuthenticatorError> { + let mut param = None; + if let Some(token) = pin_uv_auth_token { + param = Some( + token + .derive(self.client_data_hash.as_ref()) + .map_err(CommandError::Crypto)?, + ); + } + self.pin_uv_auth_param = param; + Ok(()) + } + + fn set_uv_option(&mut self, uv: Option) { + self.options.user_verification = uv; + } + + fn get_rp_id(&self) -> Option<&String> { + Some(&self.rp.id) + } + + fn can_skip_user_verification( + &mut self, + info: &AuthenticatorInfo, + uv_req: UserVerificationRequirement, + ) -> bool { + let supports_uv = info.options.user_verification == Some(true); + let pin_configured = info.options.client_pin == Some(true); + let device_protected = supports_uv || pin_configured; + let uv_discouraged = uv_req == UserVerificationRequirement::Discouraged; + let always_uv = info.options.always_uv == Some(true); + + !always_uv && (!device_protected || uv_discouraged) + } + + fn get_pin_uv_auth_param(&self) -> Option<&PinUvAuthParam> { + self.pin_uv_auth_param.as_ref() + } +} + +impl Serialize for GetAssertion { + 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.allow_list.is_empty() { + map_len += 1; + } + if self.extensions.has_content() { + map_len += 1; + } + if self.options.has_some() { + map_len += 1; + } + if self.pin_uv_auth_param.is_some() { + map_len += 2; + } + + let mut map = serializer.serialize_map(Some(map_len))?; + map.serialize_entry(&1, &self.rp.id)?; + map.serialize_entry(&2, &self.client_data_hash)?; + if !self.allow_list.is_empty() { + map.serialize_entry(&3, &self.allow_list)?; + } + if self.extensions.has_content() { + map.serialize_entry(&4, &self.extensions)?; + } + if self.options.has_some() { + map.serialize_entry(&5, &self.options)?; + } + if let Some(pin_uv_auth_param) = &self.pin_uv_auth_param { + map.serialize_entry(&6, &pin_uv_auth_param)?; + map.serialize_entry(&7, &pin_uv_auth_param.pin_protocol.id())?; + } + map.end() + } +} + +type GetAssertionOutput = Vec; +impl CtapResponse for GetAssertionOutput {} + +impl RequestCtap1 for GetAssertion { + type Output = Vec; + type AdditionalInfo = PublicKeyCredentialDescriptor; + + fn ctap1_format(&self) -> Result<(Vec, Self::AdditionalInfo), HIDError> { + // Pre-flighting should reduce the list to exactly one entry + let key_handle = match &self.allow_list[..] { + [key_handle] => key_handle, + [] => { + return Err(HIDError::Command(CommandError::StatusCode( + StatusCode::NoCredentials, + None, + ))); + } + _ => { + return Err(HIDError::UnsupportedCommand); + } + }; + + debug!("sending key_handle = {:?}", key_handle); + + let flags = if self.options.user_presence.unwrap_or(true) { + U2F_REQUEST_USER_PRESENCE + } else { + U2F_DONT_ENFORCE_USER_PRESENCE_AND_SIGN + }; + let mut auth_data = + Vec::with_capacity(2 * PARAMETER_SIZE + 1 /* key_handle_len */ + key_handle.id.len()); + + auth_data.extend_from_slice(self.client_data_hash.as_ref()); + auth_data.extend_from_slice(self.rp.hash().as_ref()); + auth_data.extend_from_slice(&[key_handle.id.len() as u8]); + auth_data.extend_from_slice(key_handle.id.as_ref()); + + let cmd = U2F_AUTHENTICATE; + let apdu = CTAP1RequestAPDU::serialize(cmd, flags, &auth_data)?; + Ok((apdu, key_handle.clone())) + } + + fn handle_response_ctap1( + &self, + dev: &mut Dev, + status: Result<(), ApduErrorStatus>, + input: &[u8], + add_info: &PublicKeyCredentialDescriptor, + ) -> Result> { + if Err(ApduErrorStatus::ConditionsNotSatisfied) == status { + return Err(Retryable::Retry); + } + if let Err(err) = status { + return Err(Retryable::Error(HIDError::ApduStatus(err))); + } + + let mut result = GetAssertionResult::from_ctap1(input, &self.rp.hash(), add_info) + .map_err(|e| Retryable::Error(HIDError::Command(e)))?; + self.finalize_result(dev, &mut result); + // Although there's only one result, we return a vector for consistency with CTAP2. + Ok(vec![result]) + } + + fn send_to_virtual_device( + &self, + dev: &mut Dev, + ) -> Result { + let mut results = dev.get_assertion(self)?; + for result in results.iter_mut() { + self.finalize_result(dev, result); + } + Ok(results) + } +} + +impl RequestCtap2 for GetAssertion { + type Output = Vec; + + fn command(&self) -> Command { + Command::GetAssertion + } + + fn wire_format(&self) -> Result, HIDError> { + Ok(ser::to_vec(&self).map_err(CommandError::Serializing)?) + } + + fn handle_response_ctap2( + &self, + dev: &mut Dev, + input: &[u8], + ) -> Result { + if input.is_empty() { + return Err(CommandError::InputTooSmall.into()); + } + + let status: StatusCode = input[0].into(); + debug!( + "response status code: {:?}, rest: {:?}", + status, + &input[1..] + ); + if input.len() == 1 { + if status.is_ok() { + return Err(CommandError::InputTooSmall.into()); + } + return Err(CommandError::StatusCode(status, None).into()); + } + + if status.is_ok() { + let assertion: GetAssertionResponse = + from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + let number_of_credentials = assertion.number_of_credentials.unwrap_or(1); + + let mut results = Vec::with_capacity(number_of_credentials); + results.push(GetAssertionResult { + assertion: assertion.into(), + attachment: AuthenticatorAttachment::Unknown, + extensions: Default::default(), + }); + + let msg = GetNextAssertion; + // We already have one, so skipping 0 + for _ in 1..number_of_credentials { + let assertion = dev.send_cbor(&msg)?; + results.push(GetAssertionResult { + assertion: assertion.into(), + attachment: AuthenticatorAttachment::Unknown, + extensions: Default::default(), + }); + } + + for result in results.iter_mut() { + self.finalize_result(dev, result); + } + Ok(results) + } else { + let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + Err(CommandError::StatusCode(status, Some(data)).into()) + } + } + + fn send_to_virtual_device( + &self, + dev: &mut Dev, + ) -> Result { + let mut results = dev.get_assertion(self)?; + for result in results.iter_mut() { + self.finalize_result(dev, result); + } + Ok(results) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Assertion { + pub credentials: Option, /* Was optional in CTAP2.0, is + * mandatory in CTAP2.1 */ + pub auth_data: AuthenticatorData, + pub signature: Vec, + pub user: Option, +} + +impl From for Assertion { + fn from(r: GetAssertionResponse) -> Self { + Assertion { + credentials: r.credentials, + auth_data: r.auth_data, + signature: r.signature, + user: r.user, + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct GetAssertionResult { + pub assertion: Assertion, + pub attachment: AuthenticatorAttachment, + pub extensions: AuthenticationExtensionsClientOutputs, +} + +impl GetAssertionResult { + pub fn from_ctap1( + input: &[u8], + rp_id_hash: &RpIdHash, + key_handle: &PublicKeyCredentialDescriptor, + ) -> Result { + let mut data = Cursor::new(input); + let user_presence = read_byte(&mut data).map_err(CommandError::Deserializing)?; + let counter = read_be_u32(&mut data).map_err(CommandError::Deserializing)?; + // Remaining data is signature (Note: `data.remaining_slice()` is not yet stabilized) + let signature = Vec::from(&data.get_ref()[data.position() as usize..]); + + // Step 5 of Section 10.3 of CTAP2.1: "Copy bits 0 (the UP bit) and bit 1 from the + // CTAP2/U2F response user presence byte to bits 0 and 1 of the CTAP2 flags, respectively. + // Set all other bits of flags to zero." + let flag_mask = AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::RESERVED_1; + let flags = flag_mask & AuthenticatorDataFlags::from_bits_truncate(user_presence); + let auth_data = AuthenticatorData { + rp_id_hash: rp_id_hash.clone(), + flags, + counter, + credential_data: None, + extensions: Default::default(), + }; + let assertion = Assertion { + credentials: Some(key_handle.clone()), + signature, + user: None, + auth_data, + }; + + Ok(GetAssertionResult { + assertion, + attachment: AuthenticatorAttachment::Unknown, + extensions: Default::default(), + }) + } +} + +#[derive(Debug, PartialEq)] +pub struct GetAssertionResponse { + pub credentials: Option, + pub auth_data: AuthenticatorData, + pub signature: Vec, + pub user: Option, + pub number_of_credentials: Option, +} + +impl CtapResponse for GetAssertionResponse {} + +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 user = 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 user.is_some() { + return Err(M::Error::duplicate_field("user")); + } + user = 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, + user, + number_of_credentials, + }) + } + } + + deserializer.deserialize_bytes(GetAssertionResponseVisitor) + } +} + +#[cfg(test)] +pub mod test { + use super::{ + Assertion, CommandError, GetAssertion, GetAssertionOptions, GetAssertionResult, HIDError, + StatusCode, + }; + use crate::consts::{ + Capability, HIDCmd, SW_CONDITIONS_NOT_SATISFIED, SW_NO_ERROR, U2F_CHECK_IS_REGISTERED, + U2F_REQUEST_USER_PRESENCE, + }; + use crate::ctap2::attestation::{AAGuid, AuthenticatorData, AuthenticatorDataFlags}; + use crate::ctap2::client_data::{Challenge, CollectedClientData, TokenBinding, WebauthnType}; + use crate::ctap2::commands::get_info::tests::AAGUID_RAW; + use crate::ctap2::commands::get_info::{ + AuthenticatorInfo, AuthenticatorOptions, AuthenticatorVersion, + }; + use crate::ctap2::commands::RequestCtap1; + use crate::ctap2::preflight::{ + do_credential_list_filtering_ctap1, do_credential_list_filtering_ctap2, + }; + use crate::ctap2::server::{ + AuthenticatorAttachment, PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity, + RelyingParty, RpIdHash, Transport, + }; + use crate::transport::device_selector::Device; + use crate::transport::hid::HIDDevice; + use crate::transport::{FidoDevice, FidoDeviceIO, FidoProtocol}; + use crate::u2ftypes::U2FDeviceInfo; + use rand::{thread_rng, RngCore}; + + #[test] + fn test_get_assertion_ctap2() { + let client_data = CollectedClientData { + webauthn_type: WebauthnType::Create, + challenge: Challenge::from(vec![0x00, 0x01, 0x02, 0x03]), + origin: String::from("example.com"), + cross_origin: false, + token_binding: Some(TokenBinding::Present(String::from("AAECAw"))), + }; + let assertion = GetAssertion::new( + client_data.hash().expect("failed to serialize client data"), + RelyingParty::from("example.com"), + 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_verification: None, + }, + Default::default(), + ); + let mut device = Device::new("commands/get_assertion").unwrap(); + assert_eq!(device.get_protocol(), FidoProtocol::CTAP2); + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + device.set_cid(cid); + + let mut msg = cid.to_vec(); + msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x90]); + msg.extend(vec![0x2]); // u2f command + msg.extend(vec![ + 0xa4, // map(4) + 0x1, // rpid + 0x6b, // text(11) + 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, // example.com + 0x2, // clientDataHash + 0x58, 0x20, //bytes(32) + 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, 0x32, + 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, 0x10, 0x87, + 0x54, 0xc3, 0x2d, 0x80, // hash + 0x3, //allowList + 0x81, // array(1) + 0xa2, // map(2) + 0x62, // text(2) + 0x69, 0x64, // id + 0x58, // bytes( + ]); + device.add_write(&msg, 0); + + msg = cid.to_vec(); + msg.extend([0x0]); //SEQ + msg.extend([0x40]); // 64) + msg.extend(&assertion.allow_list[0].id[..58]); + device.add_write(&msg, 0); + + msg = cid.to_vec(); + msg.extend([0x1]); //SEQ + msg.extend(&assertion.allow_list[0].id[58..64]); + msg.extend(vec![ + 0x64, // text(4), + 0x74, 0x79, 0x70, 0x65, // type + 0x6a, // text(10) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // public-key + 0x5, // options + 0xa1, // map(1) + 0x62, // text(2) + 0x75, 0x70, // up + 0xf5, // true + ]); + device.add_write(&msg, 0); + + // fido response + let mut msg = cid.to_vec(); + msg.extend([HIDCmd::Cbor.into(), 0x1, 0x2a]); // cmd + bcnt + msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[..57]); + device.add_read(&msg, 0); + + let mut msg = cid.to_vec(); + msg.extend([0x0]); // SEQ + msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[57..116]); + device.add_read(&msg, 0); + + let mut msg = cid.to_vec(); + msg.extend([0x1]); // SEQ + msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[116..175]); + device.add_read(&msg, 0); + + let mut msg = cid.to_vec(); + msg.extend([0x2]); // SEQ + msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[175..234]); + device.add_read(&msg, 0); + + let mut msg = cid.to_vec(); + msg.extend([0x3]); // SEQ + msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[234..293]); + device.add_read(&msg, 0); + let mut msg = cid.to_vec(); + msg.extend([0x4]); // SEQ + msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[293..]); + device.add_read(&msg, 0); + + // Check if response is correct + let expected_auth_data = AuthenticatorData { + rp_id_hash: RpIdHash([ + 0x62, 0x5d, 0xda, 0xdf, 0x74, 0x3f, 0x57, 0x27, 0xe6, 0x6b, 0xba, 0x8c, 0x2e, 0x38, + 0x79, 0x22, 0xd1, 0xaf, 0x43, 0xc5, 0x03, 0xd9, 0x11, 0x4a, 0x8f, 0xba, 0x10, 0x4d, + 0x84, 0xd0, 0x2b, 0xfa, + ]), + flags: AuthenticatorDataFlags::USER_PRESENT, + counter: 0x11, + credential_data: None, + extensions: Default::default(), + }; + + let expected_assertion = Assertion { + credentials: Some(PublicKeyCredentialDescriptor { + id: vec![ + 242, 32, 6, 222, 79, 144, 90, 246, 138, 67, 148, 47, 2, 79, 42, 94, 206, 96, + 61, 156, 109, 75, 61, 248, 190, 8, 237, 1, 252, 68, 38, 70, 208, 52, 133, 138, + 199, 91, 237, 63, 213, 128, 191, 152, 8, 217, 79, 203, 238, 130, 185, 178, 239, + 102, 119, 175, 10, 220, 195, 88, 82, 234, 107, 158, + ], + transports: vec![], + }), + signature: vec![ + 0x30, 0x45, 0x02, 0x20, 0x4a, 0x5a, 0x9d, 0xd3, 0x92, 0x98, 0x14, 0x9d, 0x90, 0x47, + 0x69, 0xb5, 0x1a, 0x45, 0x14, 0x33, 0x00, 0x6f, 0x18, 0x2a, 0x34, 0xfb, 0xdf, 0x66, + 0xde, 0x5f, 0xc7, 0x17, 0xd7, 0x5f, 0xb3, 0x50, 0x02, 0x21, 0x00, 0xa4, 0x6b, 0x8e, + 0xa3, 0xc3, 0xb9, 0x33, 0x82, 0x1c, 0x6e, 0x7f, 0x5e, 0xf9, 0xda, 0xae, 0x94, 0xab, + 0x47, 0xf1, 0x8d, 0xb4, 0x74, 0xc7, 0x47, 0x90, 0xea, 0xab, 0xb1, 0x44, 0x11, 0xe7, + 0xa0, + ], + user: Some(PublicKeyCredentialUserEntity { + 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, + ], + name: Some("johnpsmith@example.com".to_string()), + display_name: Some("John P. Smith".to_string()), + }), + auth_data: expected_auth_data, + }; + + let expected = vec![GetAssertionResult { + assertion: expected_assertion, + attachment: AuthenticatorAttachment::Unknown, + extensions: Default::default(), + }]; + let response = device.send_cbor(&assertion).unwrap(); + assert_eq!(response, expected); + } + + fn fill_device_ctap1(device: &mut Device, 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 client_data = CollectedClientData { + webauthn_type: WebauthnType::Create, + challenge: Challenge::from(vec![0x00, 0x01, 0x02, 0x03]), + origin: String::from("example.com"), + cross_origin: false, + token_binding: Some(TokenBinding::Present(String::from("AAECAw"))), + }; + let allowed_key = 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], + }; + let mut assertion = GetAssertion::new( + client_data.hash().expect("failed to serialize client data"), + RelyingParty::from("example.com"), + vec![allowed_key.clone()], + GetAssertionOptions { + user_presence: Some(true), + user_verification: None, + }, + Default::default(), + ); + let mut device = Device::new("commands/get_assertion").unwrap(); // not really used (all functions ignore it) + // channel id + device.downgrade_to_ctap1(); + assert_eq!(device.get_protocol(), FidoProtocol::CTAP1); + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + + device.set_cid(cid); + + // ctap1 request + fill_device_ctap1( + &mut device, + cid, + U2F_CHECK_IS_REGISTERED, + SW_CONDITIONS_NOT_SATISFIED, + ); + let key_handle = do_credential_list_filtering_ctap1( + &mut device, + &assertion.allow_list, + &assertion.rp, + &assertion.client_data_hash, + ) + .expect("Did not find a key_handle, even though it should have"); + assertion.allow_list = vec![key_handle]; + let (ctap1_request, key_handle) = assertion.ctap1_format().unwrap(); + assert_eq!(key_handle, allowed_key); + // 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 + // Pre-flighting is not done automatically + fill_device_ctap1(&mut device, cid, U2F_REQUEST_USER_PRESENCE, SW_NO_ERROR); + + let response = device.send_ctap1(&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: Default::default(), + }; + + let expected_assertion = Assertion { + credentials: Some(allowed_key), + 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, + ], + user: None, + auth_data: expected_auth_data, + }; + + let expected = vec![GetAssertionResult { + assertion: expected_assertion, + attachment: AuthenticatorAttachment::Unknown, + extensions: Default::default(), + }]; + assert_eq!(response, expected); + } + + #[test] + fn test_get_assertion_ctap1_long_keys() { + let client_data = CollectedClientData { + webauthn_type: WebauthnType::Create, + challenge: Challenge::from(vec![0x00, 0x01, 0x02, 0x03]), + origin: String::from("example.com"), + cross_origin: false, + token_binding: Some(TokenBinding::Present(String::from("AAECAw"))), + }; + + let too_long_key_handle = PublicKeyCredentialDescriptor { + id: vec![0; 1000], + transports: vec![Transport::USB], + }; + let mut assertion = GetAssertion::new( + client_data.hash().expect("failed to serialize client data"), + RelyingParty::from("example.com"), + vec![too_long_key_handle.clone()], + GetAssertionOptions { + user_presence: Some(true), + user_verification: None, + }, + Default::default(), + ); + + let mut device = Device::new("commands/get_assertion").unwrap(); // not really used (all functions ignore it) + // channel id + device.downgrade_to_ctap1(); + assert_eq!(device.get_protocol(), FidoProtocol::CTAP1); + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + + device.set_cid(cid); + + assert_matches!( + do_credential_list_filtering_ctap1( + &mut device, + &assertion.allow_list, + &assertion.rp, + &assertion.client_data_hash, + ), + None + ); + assertion.allow_list = vec![]; + // It should also fail when trying to format + assert_matches!( + assertion.ctap1_format(), + Err(HIDError::Command(CommandError::StatusCode( + StatusCode::NoCredentials, + .. + ))) + ); + + // Test also multiple too long keys and an empty allow list + for allow_list in [vec![], vec![too_long_key_handle.clone(); 5]] { + assertion.allow_list = allow_list; + + assert_matches!( + do_credential_list_filtering_ctap1( + &mut device, + &assertion.allow_list, + &assertion.rp, + &assertion.client_data_hash, + ), + None + ); + } + + let ok_key_handle = 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], + }; + assertion.allow_list = vec![ + too_long_key_handle.clone(), + too_long_key_handle.clone(), + too_long_key_handle.clone(), + ok_key_handle.clone(), + too_long_key_handle, + ]; + + // ctap1 request + fill_device_ctap1( + &mut device, + cid, + U2F_CHECK_IS_REGISTERED, + SW_CONDITIONS_NOT_SATISFIED, + ); + let key_handle = do_credential_list_filtering_ctap1( + &mut device, + &assertion.allow_list, + &assertion.rp, + &assertion.client_data_hash, + ) + .expect("Did not find a key_handle, even though it should have"); + assertion.allow_list = vec![key_handle]; + let (ctap1_request, key_handle) = assertion.ctap1_format().unwrap(); + assert_eq!(key_handle, ok_key_handle); + // 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 + // Pre-flighting is not done automatically + fill_device_ctap1(&mut device, cid, U2F_REQUEST_USER_PRESENCE, SW_NO_ERROR); + + let response = device.send_ctap1(&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: Default::default(), + }; + + let expected_assertion = Assertion { + credentials: Some(ok_key_handle), + 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, + ], + user: None, + auth_data: expected_auth_data, + }; + + let expected = vec![GetAssertionResult { + assertion: expected_assertion, + attachment: AuthenticatorAttachment::Unknown, + extensions: Default::default(), + }]; + assert_eq!(response, expected); + } + + #[test] + fn test_get_assertion_ctap2_pre_flight() { + let client_data = CollectedClientData { + webauthn_type: WebauthnType::Create, + challenge: Challenge::from(vec![0x00, 0x01, 0x02, 0x03]), + origin: String::from("example.com"), + cross_origin: false, + token_binding: Some(TokenBinding::Present(String::from("AAECAw"))), + }; + let assertion = GetAssertion::new( + client_data.hash().expect("failed to serialize client data"), + RelyingParty::from("example.com"), + vec![ + // This should never be tested, because it gets pre-filtered, since it is too long + // (see max_credential_id_length) + PublicKeyCredentialDescriptor { + id: vec![0x10; 100], + transports: vec![Transport::USB], + }, + // One we test and skip + PublicKeyCredentialDescriptor { + id: vec![ + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, + ], + transports: vec![Transport::USB], + }, + // This one is the 'right' one + 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], + }, + // We should never test this one + PublicKeyCredentialDescriptor { + id: vec![ + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, + ], + transports: vec![Transport::USB], + }, + ], + GetAssertionOptions { + user_presence: Some(true), + user_verification: None, + }, + Default::default(), + ); + let mut device = Device::new("commands/get_assertion").unwrap(); + assert_eq!(device.get_protocol(), FidoProtocol::CTAP2); + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + device.set_cid(cid); + device.set_device_info(U2FDeviceInfo { + vendor_name: Vec::new(), + device_name: Vec::new(), + version_interface: 0x02, + version_major: 0x04, + version_minor: 0x01, + version_build: 0x08, + cap_flags: Capability::WINK | Capability::CBOR, + }); + device.set_authenticator_info(AuthenticatorInfo { + versions: vec![AuthenticatorVersion::U2F_V2, AuthenticatorVersion::FIDO_2_0], + extensions: vec!["uvm".to_string(), "hmac-secret".to_string()], + aaguid: AAGuid(AAGUID_RAW), + options: AuthenticatorOptions { + platform_device: false, + resident_key: true, + client_pin: Some(false), + user_presence: true, + user_verification: None, + ..Default::default() + }, + max_msg_size: Some(1200), + pin_protocols: Some(vec![1]), + max_credential_count_in_list: None, + max_credential_id_length: Some(80), + transports: None, + algorithms: None, + max_ser_large_blob_array: None, + force_pin_change: None, + min_pin_length: None, + firmware_version: None, + max_cred_blob_length: None, + max_rpids_for_set_min_pin_length: None, + preferred_platform_uv_attempts: None, + uv_modality: None, + certifications: None, + remaining_discoverable_credentials: None, + vendor_prototype_config_commands: None, + }); + + // Sending first GetAssertion with first allow_list-entry, that will return an error + let mut msg = cid.to_vec(); + msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x90]); + msg.extend(vec![0x2]); // u2f command + msg.extend(vec![ + 0xa4, // map(4) + 0x1, // rpid + 0x6b, // text(11) + 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, // example.com + 0x2, // clientDataHash + 0x58, 0x20, //bytes(32) + 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, + 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, + 0x78, 0x52, 0xb8, 0x55, // empty hash + 0x3, //allowList + 0x81, // array(1) + 0xa2, // map(2) + 0x62, // text(2) + 0x69, 0x64, // id + 0x58, // bytes( + ]); + device.add_write(&msg, 0); + + msg = cid.to_vec(); + msg.extend([0x0]); //SEQ + msg.extend([0x40]); // 64) + msg.extend(&assertion.allow_list[1].id[..58]); + device.add_write(&msg, 0); + + msg = cid.to_vec(); + msg.extend([0x1]); //SEQ + msg.extend(&assertion.allow_list[1].id[58..64]); + msg.extend(vec![ + 0x64, // text(4), + 0x74, 0x79, 0x70, 0x65, // type + 0x6a, // text(10) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // public-key + 0x5, // options + 0xa1, // map(1) + 0x62, // text(2) + 0x75, 0x70, // up + 0xf4, // false + ]); + device.add_write(&msg, 0); + + // fido response + let len = 0x1; + let mut msg = cid.to_vec(); + msg.extend(vec![HIDCmd::Cbor.into(), 0x00, len]); // cmd + bcnt + msg.push(0x2e); // Status code: NoCredentials + device.add_read(&msg, 0); + + // Sending second GetAssertion with first allow_list-entry, that will return a success + let mut msg = cid.to_vec(); + msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x90]); + msg.extend(vec![0x2]); // u2f command + msg.extend(vec![ + 0xa4, // map(4) + 0x1, // rpid + 0x6b, // text(11) + 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, // example.com + 0x2, // clientDataHash + 0x58, 0x20, //bytes(32) + 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, + 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, + 0x78, 0x52, 0xb8, 0x55, // empty hash + 0x3, //allowList + 0x81, // array(1) + 0xa2, // map(2) + 0x62, // text(2) + 0x69, 0x64, // id + 0x58, // bytes( + ]); + device.add_write(&msg, 0); + + msg = cid.to_vec(); + msg.extend([0x0]); //SEQ + msg.extend([0x40]); // 64) + msg.extend(&assertion.allow_list[2].id[..58]); + device.add_write(&msg, 0); + + msg = cid.to_vec(); + msg.extend([0x1]); //SEQ + msg.extend(&assertion.allow_list[2].id[58..64]); + msg.extend(vec![ + 0x64, // text(4), + 0x74, 0x79, 0x70, 0x65, // type + 0x6a, // text(10) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // public-key + 0x5, // options + 0xa1, // map(1) + 0x62, // text(2) + 0x75, 0x70, // up + 0xf4, // false + ]); + device.add_write(&msg, 0); + + let mut msg = cid.to_vec(); + msg.extend([HIDCmd::Cbor.into(), 0x1, 0x2a]); // cmd + bcnt + msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[..57]); + device.add_read(&msg, 0); + + let mut msg = cid.to_vec(); + msg.extend([0x0]); // SEQ + msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[57..116]); + device.add_read(&msg, 0); + + let mut msg = cid.to_vec(); + msg.extend([0x1]); // SEQ + msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[116..175]); + device.add_read(&msg, 0); + + let mut msg = cid.to_vec(); + msg.extend([0x2]); // SEQ + msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[175..234]); + device.add_read(&msg, 0); + + let mut msg = cid.to_vec(); + msg.extend([0x3]); // SEQ + msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[234..293]); + device.add_read(&msg, 0); + let mut msg = cid.to_vec(); + msg.extend([0x4]); // SEQ + msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[293..]); + device.add_read(&msg, 0); + + assert_matches!( + do_credential_list_filtering_ctap2( + &mut device, + &assertion.allow_list, + &assertion.rp, + None, + ), + Ok(..) + ); + } + + #[test] + fn test_get_assertion_ctap1_flags() { + // Ensure that only the two low bits of flags are preserved when repackaging a + // CTAP1 response. + let mut sample = GET_ASSERTION_SAMPLE_RESPONSE_CTAP1.to_vec(); + sample[0] = 0xff; // Set all 8 flag bits before repackaging + let add_info = PublicKeyCredentialDescriptor { + id: vec![], + transports: vec![], + }; + let rp_hash = RpIdHash([0u8; 32]); + let resp = GetAssertionResult::from_ctap1(&sample, &rp_hash, &add_info) + .expect("could not handle response"); + assert_eq!( + resp.assertion.auth_data.flags, + AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::RESERVED_1 + ); + } + + // Manually assembled according to https://www.w3.org/TR/webauthn-2/#clientdatajson-serialization + const CLIENT_DATA_VEC: [u8; 140] = [ + 0x7b, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, // {"type": + 0x22, 0x77, 0x65, 0x62, 0x61, 0x75, 0x74, 0x68, 0x6e, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x22, // "webauthn.create" + 0x2c, 0x22, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x22, + 0x3a, // (,"challenge": + 0x22, 0x41, 0x41, 0x45, 0x43, 0x41, 0x77, 0x22, // challenge in base64 + 0x2c, 0x22, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x22, 0x3a, // ,"origin": + 0x22, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, + 0x22, // "example.com" + 0x2c, 0x22, 0x63, 0x72, 0x6f, 0x73, 0x73, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x22, + 0x3a, // ,"crossOrigin": + 0x66, 0x61, 0x6c, 0x73, 0x65, // false + 0x2c, 0x22, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x22, + 0x3a, // ,"tokenBinding": + 0x7b, 0x22, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x3a, // {"status": + 0x22, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x22, // "present" + 0x2c, 0x22, 0x69, 0x64, 0x22, 0x3a, // ,"id": + 0x22, 0x41, 0x41, 0x45, 0x43, 0x41, 0x77, 0x22, // "AAECAw" + 0x7d, // } + 0x7d, // } + ]; + + const CLIENT_DATA_HASH: [u8; 32] = [ + 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, // hash + 0x32, 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, // hash + 0x10, 0x87, 0x54, 0xc3, 0x2d, 0x80, // hash + ]; + + 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, // CLA + 0x2, // INS U2F_Authenticate + 0x3, // P1 Flags (user presence) + 0x0, // P2 + 0x0, 0x0, 0x81, // Lc + // 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: + 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, // hash + 0x32, 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, // hash + 0x10, 0x87, 0x54, 0xc3, 0x2d, 0x80, // 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, // .. + // Le (Ne=65536): + 0x0, 0x0, + ]; + + const GET_ASSERTION_SAMPLE_REQUEST_CTAP2: [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: + 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, 0x32, 0x64, + 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, 0x10, 0x87, 0x54, 0xc3, + 0x2d, 0x80, // 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, + ]; + + const GET_ASSERTION_SAMPLE_RESPONSE_CTAP2: [u8; 298] = [ + 0x00, // status == success + 0xA5, // map(5) + 0x01, // unsigned(1) + 0xA2, // map(2) + 0x62, // text(2) + 0x69, 0x64, // "id" + 0x58, 0x40, // bytes(0x64, ) credential_id + 0xF2, 0x20, 0x06, 0xDE, 0x4F, 0x90, 0x5A, 0xF6, 0x8A, 0x43, 0x94, 0x2F, 0x02, 0x4F, 0x2A, + 0x5E, 0xCE, 0x60, 0x3D, 0x9C, 0x6D, 0x4B, 0x3D, 0xF8, 0xBE, 0x08, 0xED, 0x01, 0xFC, 0x44, + 0x26, 0x46, 0xD0, 0x34, 0x85, 0x8A, 0xC7, 0x5B, 0xED, 0x3F, 0xD5, 0x80, 0xBF, 0x98, 0x08, + 0xD9, 0x4F, 0xCB, 0xEE, 0x82, 0xB9, 0xB2, 0xEF, 0x66, 0x77, 0xAF, 0x0A, 0xDC, 0xC3, 0x58, + 0x52, 0xEA, 0x6B, 0x9E, // end: credential_id + 0x64, // text(4) + 0x74, 0x79, 0x70, 0x65, // "type" + 0x6A, // text(0x10, ) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // "public-key" + 0x02, // unsigned(2) + 0x58, 0x25, // bytes(0x37, ) auth_data + 0x62, 0x5D, 0xDA, 0xDF, 0x74, 0x3F, 0x57, 0x27, 0xE6, 0x6B, 0xBA, 0x8C, 0x2E, 0x38, 0x79, + 0x22, 0xD1, 0xAF, 0x43, 0xC5, 0x03, 0xD9, 0x11, 0x4A, 0x8F, 0xBA, 0x10, 0x4D, 0x84, 0xD0, + 0x2B, 0xFA, 0x01, 0x00, 0x00, 0x00, 0x11, // end: auth_data + 0x03, // unsigned(3) + 0x58, 0x47, // bytes(0x71, ) signature + 0x30, 0x45, 0x02, 0x20, 0x4A, 0x5A, 0x9D, 0xD3, 0x92, 0x98, 0x14, 0x9D, 0x90, 0x47, 0x69, + 0xB5, 0x1A, 0x45, 0x14, 0x33, 0x00, 0x6F, 0x18, 0x2A, 0x34, 0xFB, 0xDF, 0x66, 0xDE, 0x5F, + 0xC7, 0x17, 0xD7, 0x5F, 0xB3, 0x50, 0x02, 0x21, 0x00, 0xA4, 0x6B, 0x8E, 0xA3, 0xC3, 0xB9, + 0x33, 0x82, 0x1C, 0x6E, 0x7F, 0x5E, 0xF9, 0xDA, 0xAE, 0x94, 0xAB, 0x47, 0xF1, 0x8D, 0xB4, + 0x74, 0xC7, 0x47, 0x90, 0xEA, 0xAB, 0xB1, 0x44, 0x11, 0xE7, 0xA0, // end: signature + 0x04, // unsigned(4) + 0xA3, // map(3) + 0x62, // text(2) + 0x69, 0x64, // "id" + 0x58, 0x20, // bytes(0x32, ) user_id + 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, // end: user_id + 0x64, // text(4) + 0x6E, 0x61, 0x6D, 0x65, // "name" + 0x76, // text(0x22, ) + 0x6A, 0x6F, 0x68, 0x6E, 0x70, 0x73, 0x6D, 0x69, 0x74, 0x68, 0x40, 0x65, 0x78, 0x61, 0x6D, + 0x70, 0x6C, 0x65, 0x2E, 0x63, 0x6F, 0x6D, // "johnpsmith@example.com" + 0x6B, // text(0x11, ) + 0x64, 0x69, 0x73, 0x70, 0x6C, 0x61, 0x79, 0x4E, 0x61, 0x6D, 0x65, // "displayName" + 0x6D, // text(0x13, ) + 0x4A, 0x6F, 0x68, 0x6E, 0x20, 0x50, 0x2E, 0x20, 0x53, 0x6D, 0x69, 0x74, + 0x68, // "John P. Smith" + 0x05, // unsigned(5) + 0x01, // unsigned(1) + ]; +} diff --git a/third_party/rust/authenticator/src/ctap2/commands/get_info.rs b/third_party/rust/authenticator/src/ctap2/commands/get_info.rs new file mode 100644 index 0000000000..f676605a0b --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/get_info.rs @@ -0,0 +1,1054 @@ +use super::{Command, CommandError, CtapResponse, RequestCtap2, StatusCode}; +use crate::ctap2::attestation::AAGuid; +use crate::ctap2::server::PublicKeyCredentialParameters; +use crate::transport::errors::HIDError; +use crate::transport::{FidoDevice, VirtualFidoDevice}; +use serde::{ + de::{Error as SError, IgnoredAny, MapAccess, Visitor}, + Deserialize, Deserializer, Serialize, +}; +use serde_cbor::{de::from_slice, Value}; +use std::collections::BTreeMap; +use std::fmt; + +#[derive(Debug, Default)] +pub struct GetInfo {} + +impl RequestCtap2 for GetInfo { + type Output = AuthenticatorInfo; + + fn command(&self) -> Command { + Command::GetInfo + } + + fn wire_format(&self) -> Result, HIDError> { + Ok(Vec::new()) + } + + fn handle_response_ctap2( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result { + if input.is_empty() { + return Err(CommandError::InputTooSmall.into()); + } + + let status: StatusCode = input[0].into(); + + if input.len() > 1 { + 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::Deserializing)?; + Err(CommandError::StatusCode(status, Some(data)).into()) + } + } else { + Err(CommandError::InputTooSmall.into()) + } + } + + fn send_to_virtual_device( + &self, + dev: &mut Dev, + ) -> Result { + dev.get_info() + } +} + +fn true_val() -> bool { + true +} + +#[derive(Debug, Deserialize, Clone, Eq, PartialEq, Serialize)] +pub struct AuthenticatorOptions { + /// Indicates that the device is attached to the client and therefore can’t + /// be removed and used on another client. + #[serde(rename = "plat", default)] + pub platform_device: bool, + /// Indicates that the device is capable of storing keys on the device + /// itself and therefore can satisfy the authenticatorGetAssertion request + /// with allowList parameter not specified or empty. + #[serde(rename = "rk", default)] + pub resident_key: bool, + + /// Client PIN: + /// If present and set to true, it indicates that the device is capable of + /// accepting a PIN from the client and PIN has been set. + /// If present and set to false, it indicates that the device is capable of + /// accepting a PIN from the client and PIN has not been set yet. + /// If absent, it indicates that the device is not capable of accepting a + /// PIN from the client. + /// Client PIN is one of the ways to do user verification. + #[serde(rename = "clientPin")] + pub client_pin: Option, + + /// Indicates that the device is capable of testing user presence. + #[serde(rename = "up", default = "true_val")] + pub user_presence: bool, + + /// Indicates that the device is capable of verifying the user within + /// itself. For example, devices with UI, biometrics fall into this + /// category. + /// If present and set to true, it indicates that the device is capable of + /// user verification within itself and has been configured. + /// If present and set to false, it indicates that the device is capable of + /// user verification within itself and has not been yet configured. For + /// example, a biometric device that has not yet been configured will + /// return this parameter set to false. + /// If absent, it indicates that the device is not capable of user + /// verification within itself. + /// A device that can only do Client PIN will not return the "uv" parameter. + /// If a device is capable of verifying the user within itself as well as + /// able to do Client PIN, it will return both "uv" and the Client PIN + /// option. + // TODO(MS): My Token (key-ID FIDO2) does return Some(false) here, even though + // it has no built-in verification method. Not to be trusted... + #[serde(rename = "uv")] + pub user_verification: Option, + + // ---------------------------------------------------- + // CTAP 2.1 options + // ---------------------------------------------------- + /// If pinUvAuthToken is: + /// present and set to true + /// if the clientPin option id is present and set to true, then the + /// authenticator supports authenticatorClientPIN's getPinUvAuthTokenUsingPinWithPermissions + /// subcommand. If the uv option id is present and set to true, then + /// the authenticator supports authenticatorClientPIN's getPinUvAuthTokenUsingUvWithPermissions + /// subcommand. + /// present and set to false, or absent. + /// the authenticator does not support authenticatorClientPIN's + /// getPinUvAuthTokenUsingPinWithPermissions and getPinUvAuthTokenUsingUvWithPermissions + /// subcommands. + #[serde(rename = "pinUvAuthToken")] + pub pin_uv_auth_token: Option, + + /// If this noMcGaPermissionsWithClientPin is: + /// present and set to true + /// A pinUvAuthToken obtained via getPinUvAuthTokenUsingPinWithPermissions + /// (or getPinToken) cannot be used for authenticatorMakeCredential or + /// authenticatorGetAssertion commands, because it will lack the necessary + /// mc and ga permissions. In this situation, platforms SHOULD NOT attempt + /// to use getPinUvAuthTokenUsingPinWithPermissions if using + /// getPinUvAuthTokenUsingUvWithPermissions fails. + /// present and set to false, or absent. + /// A pinUvAuthToken obtained via getPinUvAuthTokenUsingPinWithPermissions + /// (or getPinToken) can be used for authenticatorMakeCredential or + /// authenticatorGetAssertion commands. + /// Note: noMcGaPermissionsWithClientPin MUST only be present if the + /// clientPin option ID is present. + #[serde(rename = "noMcGaPermissionsWithClientPin")] + pub no_mc_ga_permissions_with_client_pin: Option, + + /// If largeBlobs is: + /// present and set to true + /// the authenticator supports the authenticatorLargeBlobs command. + /// present and set to false, or absent. + /// The authenticatorLargeBlobs command is NOT supported. + #[serde(rename = "largeBlobs")] + pub large_blobs: Option, + + /// Enterprise Attestation feature support: + /// If ep is: + /// Present and set to true + /// The authenticator is enterprise attestation capable, and enterprise + /// attestation is enabled. + /// Present and set to false + /// The authenticator is enterprise attestation capable, and enterprise + /// attestation is disabled. + /// Absent + /// The Enterprise Attestation feature is NOT supported. + #[serde(rename = "ep")] + pub ep: Option, + + /// If bioEnroll is: + /// present and set to true + /// the authenticator supports the authenticatorBioEnrollment commands, + /// and has at least one bio enrollment presently provisioned. + /// present and set to false + /// the authenticator supports the authenticatorBioEnrollment commands, + /// and does not yet have any bio enrollments provisioned. + /// absent + /// the authenticatorBioEnrollment commands are NOT supported. + #[serde(rename = "bioEnroll")] + pub bio_enroll: Option, + + /// "FIDO_2_1_PRE" Prototype Credential management support: + /// If userVerificationMgmtPreview is: + /// present and set to true + /// the authenticator supports the Prototype authenticatorBioEnrollment (0x41) + /// commands, and has at least one bio enrollment presently provisioned. + /// present and set to false + /// the authenticator supports the Prototype authenticatorBioEnrollment (0x41) + /// commands, and does not yet have any bio enrollments provisioned. + /// absent + /// the Prototype authenticatorBioEnrollment (0x41) commands are not supported. + #[serde(rename = "userVerificationMgmtPreview")] + pub user_verification_mgmt_preview: Option, + + /// getPinUvAuthTokenUsingUvWithPermissions support for requesting the be permission: + /// This option ID MUST only be present if bioEnroll is also present. + /// If uvBioEnroll is: + /// present and set to true + /// requesting the be permission when invoking getPinUvAuthTokenUsingUvWithPermissions + /// is supported. + /// present and set to false, or absent. + /// requesting the be permission when invoking getPinUvAuthTokenUsingUvWithPermissions + /// is NOT supported. + #[serde(rename = "uvBioEnroll")] + pub uv_bio_enroll: Option, + + /// authenticatorConfig command support: + /// If authnrCfg is: + /// present and set to true + /// the authenticatorConfig command is supported. + /// present and set to false, or absent. + /// the authenticatorConfig command is NOT supported. + #[serde(rename = "authnrCfg")] + pub authnr_cfg: Option, + + /// getPinUvAuthTokenUsingUvWithPermissions support for requesting the acfg permission: + /// This option ID MUST only be present if authnrCfg is also present. + /// If uvAcfg is: + /// present and set to true + /// requesting the acfg permission when invoking getPinUvAuthTokenUsingUvWithPermissions + /// is supported. + /// present and set to false, or absent. + /// requesting the acfg permission when invoking getPinUvAuthTokenUsingUvWithPermissions + /// is NOT supported. + #[serde(rename = "uvAcfg")] + pub uv_acfg: Option, + + /// Credential management support: + /// If credMgmt is: + /// present and set to true + /// the authenticatorCredentialManagement command is supported. + /// present and set to false, or absent. + /// the authenticatorCredentialManagement command is NOT supported. + #[serde(rename = "credMgmt")] + pub cred_mgmt: Option, + + /// "FIDO_2_1_PRE" Prototype Credential management support: + /// If credentialMgmtPreview is: + /// present and set to true + /// the Prototype authenticatorCredentialManagement (0x41) command is supported. + /// present and set to false, or absent. + /// the Prototype authenticatorCredentialManagement (0x41) command is NOT supported. + #[serde(rename = "credentialMgmtPreview")] + pub credential_mgmt_preview: Option, + + /// Support for the Set Minimum PIN Length feature. + /// If setMinPINLength is: + /// present and set to true + /// the setMinPINLength subcommand is supported. + /// present and set to false, or absent. + /// the setMinPINLength subcommand is NOT supported. + /// Note: setMinPINLength MUST only be present if the clientPin option ID is present. + #[serde(rename = "setMinPINLength")] + pub set_min_pin_length: Option, + + /// Support for making non-discoverable credentials without requiring User Verification. + /// If makeCredUvNotRqd is: + /// present and set to true + /// the authenticator allows creation of non-discoverable credentials without + /// requiring any form of user verification, if the platform requests this behaviour. + /// present and set to false, or absent. + /// the authenticator requires some form of user verification for creating + /// non-discoverable credentials, regardless of the parameters the platform supplies + /// for the authenticatorMakeCredential command. + /// Authenticators SHOULD include this option with the value true. + #[serde(rename = "makeCredUvNotRqd")] + pub make_cred_uv_not_rqd: Option, + + /// Support for the Always Require User Verification feature: + /// If alwaysUv is + /// present and set to true + /// the authenticator supports the Always Require User Verification feature and it is enabled. + /// present and set to false + /// the authenticator supports the Always Require User Verification feature but it is disabled. + /// absent + /// the authenticator does not support the Always Require User Verification feature. + /// Note: If the alwaysUv option ID is present and true the authenticator MUST set the value + /// of makeCredUvNotRqd to false. + #[serde(rename = "alwaysUv")] + pub always_uv: Option, +} + +impl Default for AuthenticatorOptions { + fn default() -> Self { + AuthenticatorOptions { + platform_device: false, + resident_key: false, + client_pin: None, + user_presence: true, + user_verification: None, + pin_uv_auth_token: None, + no_mc_ga_permissions_with_client_pin: None, + large_blobs: None, + ep: None, + bio_enroll: None, + user_verification_mgmt_preview: None, + uv_bio_enroll: None, + authnr_cfg: None, + uv_acfg: None, + cred_mgmt: None, + credential_mgmt_preview: None, + set_min_pin_length: None, + make_cred_uv_not_rqd: None, + always_uv: None, + } + } +} + +#[allow(non_camel_case_types)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub enum AuthenticatorVersion { + U2F_V2, + FIDO_2_0, + FIDO_2_1_PRE, + FIDO_2_1, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)] +pub struct AuthenticatorInfo { + pub versions: Vec, + pub extensions: Vec, + pub aaguid: AAGuid, + pub options: AuthenticatorOptions, + pub max_msg_size: Option, + pub pin_protocols: Option>, + // CTAP 2.1 + pub max_credential_count_in_list: Option, + pub max_credential_id_length: Option, + pub transports: Option>, + pub algorithms: Option>, + pub max_ser_large_blob_array: Option, + pub force_pin_change: Option, + pub min_pin_length: Option, + pub firmware_version: Option, + pub max_cred_blob_length: Option, + pub max_rpids_for_set_min_pin_length: Option, + pub preferred_platform_uv_attempts: Option, + pub uv_modality: Option, + pub certifications: Option>, + pub remaining_discoverable_credentials: Option, + pub vendor_prototype_config_commands: Option>, +} + +impl AuthenticatorInfo { + pub fn supports_cred_protect(&self) -> bool { + self.extensions.contains(&"credProtect".to_string()) + } + + pub fn supports_hmac_secret(&self) -> bool { + self.extensions.contains(&"hmac-secret".to_string()) + } + + pub fn max_supported_version(&self) -> AuthenticatorVersion { + let versions = vec![ + AuthenticatorVersion::FIDO_2_1, + AuthenticatorVersion::FIDO_2_1_PRE, + AuthenticatorVersion::FIDO_2_0, + AuthenticatorVersion::U2F_V2, + ]; + for ver in versions { + if self.versions.contains(&ver) { + return ver; + } + } + AuthenticatorVersion::U2F_V2 + } + + pub fn device_is_protected(&self) -> bool { + self.options.client_pin == Some(true) || self.options.user_verification == Some(true) + } +} + +impl CtapResponse for AuthenticatorInfo {} + +macro_rules! parse_next_optional_value { + ($name:expr, $map:expr) => { + if $name.is_some() { + return Err(serde::de::Error::duplicate_field("$name")); + } + $name = Some($map.next_value()?); + }; +} + +impl<'de> Deserialize<'de> for AuthenticatorInfo { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct AuthenticatorInfoVisitor; + + impl<'de> Visitor<'de> for AuthenticatorInfoVisitor { + type Value = AuthenticatorInfo; + + 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 versions = Vec::new(); + let mut extensions = Vec::new(); + let mut aaguid = None; + let mut options = AuthenticatorOptions::default(); + let mut max_msg_size = None; + let mut pin_protocols: Option> = None; + let mut max_credential_count_in_list = None; + let mut max_credential_id_length = None; + let mut transports = None; + let mut algorithms = None; + let mut max_ser_large_blob_array = None; + let mut force_pin_change = None; + let mut min_pin_length = None; + let mut firmware_version = None; + let mut max_cred_blob_length = None; + let mut max_rpids_for_set_min_pin_length = None; + let mut preferred_platform_uv_attempts = None; + let mut uv_modality = None; + let mut certifications = None; + let mut remaining_discoverable_credentials = None; + let mut vendor_prototype_config_commands = None; + while let Some(key) = map.next_key()? { + match key { + 0x01 => { + if !versions.is_empty() { + return Err(serde::de::Error::duplicate_field("versions")); + } + versions = map.next_value()?; + } + 0x02 => { + if !extensions.is_empty() { + return Err(serde::de::Error::duplicate_field("extensions")); + } + extensions = map.next_value()?; + } + 0x03 => { + parse_next_optional_value!(aaguid, map); + } + 0x04 => { + options = map.next_value()?; + } + 0x05 => { + parse_next_optional_value!(max_msg_size, map); + } + 0x06 => { + parse_next_optional_value!(pin_protocols, map); + } + 0x07 => { + parse_next_optional_value!(max_credential_count_in_list, map); + } + 0x08 => { + parse_next_optional_value!(max_credential_id_length, map); + } + 0x09 => { + parse_next_optional_value!(transports, map); + } + 0x0a => { + parse_next_optional_value!(algorithms, map); + } + 0x0b => { + parse_next_optional_value!(max_ser_large_blob_array, map); + } + 0x0c => { + parse_next_optional_value!(force_pin_change, map); + } + 0x0d => { + parse_next_optional_value!(min_pin_length, map); + } + 0x0e => { + parse_next_optional_value!(firmware_version, map); + } + 0x0f => { + parse_next_optional_value!(max_cred_blob_length, map); + } + 0x10 => { + parse_next_optional_value!(max_rpids_for_set_min_pin_length, map); + } + 0x11 => { + parse_next_optional_value!(preferred_platform_uv_attempts, map); + } + 0x12 => { + parse_next_optional_value!(uv_modality, map); + } + 0x13 => { + parse_next_optional_value!(certifications, map); + } + 0x14 => { + parse_next_optional_value!(remaining_discoverable_credentials, map); + } + 0x15 => { + parse_next_optional_value!(vendor_prototype_config_commands, map); + } + k => { + warn!("GetInfo: unexpected key: {:?}", k); + let _ = map.next_value::()?; + continue; + } + } + } + + if versions.is_empty() { + return Err(M::Error::custom( + "expected at least one version, got none".to_string(), + )); + } + + if let Some(protocols) = &pin_protocols { + if protocols.is_empty() { + return Err(M::Error::custom( + "Token returned empty PIN protocol list, which is not allowed" + .to_string(), + )); + } + } + + if let Some(aaguid) = aaguid { + Ok(AuthenticatorInfo { + versions, + extensions, + aaguid, + options, + max_msg_size, + pin_protocols, + max_credential_count_in_list, + max_credential_id_length, + transports, + algorithms, + max_ser_large_blob_array, + force_pin_change, + min_pin_length, + firmware_version, + max_cred_blob_length, + max_rpids_for_set_min_pin_length, + preferred_platform_uv_attempts, + uv_modality, + certifications, + remaining_discoverable_credentials, + vendor_prototype_config_commands, + }) + } else { + Err(M::Error::custom("No AAGuid specified".to_string())) + } + } + } + + deserializer.deserialize_bytes(AuthenticatorInfoVisitor) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::consts::{Capability, HIDCmd, CID_BROADCAST}; + use crate::crypto::COSEAlgorithm; + use crate::transport::device_selector::Device; + use crate::transport::platform::device::IN_HID_RPT_SIZE; + use crate::transport::{hid::HIDDevice, FidoDevice, FidoProtocol}; + use rand::{thread_rng, RngCore}; + use serde_cbor::de::from_slice; + + // Raw data take from https://github.com/Yubico/python-fido2/blob/master/test/test_ctap2.py + pub const AAGUID_RAW: [u8; 16] = [ + 0xF8, 0xA0, 0x11, 0xF3, 0x8C, 0x0A, 0x4D, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1F, 0x9E, 0xDC, + 0x7D, + ]; + + pub const AUTHENTICATOR_INFO_PAYLOAD: [u8; 89] = [ + 0xa6, // map(6) + 0x01, // unsigned(1) + 0x82, // array(2) + 0x66, // text(6) + 0x55, 0x32, 0x46, 0x5f, 0x56, 0x32, // "U2F_V2" + 0x68, // text(8) + 0x46, 0x49, 0x44, 0x4f, 0x5f, 0x32, 0x5f, 0x30, // "FIDO_2_0" + 0x02, // unsigned(2) + 0x82, // array(2) + 0x63, // text(3) + 0x75, 0x76, 0x6d, // "uvm" + 0x6b, // text(11) + 0x68, 0x6d, 0x61, 0x63, 0x2d, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // "hmac-secret" + 0x03, // unsigned(3) + 0x50, // bytes(16) + 0xf8, 0xa0, 0x11, 0xf3, 0x8c, 0x0a, 0x4d, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1f, 0x9e, 0xdc, + 0x7d, // "\xF8\xA0\u0011\xF3\x8C\nM\u0015\x80\u0006\u0017\u0011\u001F\x9E\xDC}" + 0x04, // unsigned(4) + 0xa4, // map(4) + 0x62, // text(2) + 0x72, 0x6b, // "rk" + 0xf5, // primitive(21) + 0x62, // text(2) + 0x75, 0x70, // "up" + 0xf5, // primitive(21) + 0x64, // text(4) + 0x70, 0x6c, 0x61, 0x74, // "plat" + 0xf4, // primitive(20) + 0x69, // text(9) + 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x50, 0x69, 0x6e, // "clientPin" + 0xf4, // primitive(20) + 0x05, // unsigned(5) + 0x19, 0x04, 0xb0, // unsigned(1200) + 0x06, // unsigned(6) + 0x81, // array(1) + 0x01, // unsigned(1) + ]; + + // Real world example from Yubikey Bio + pub const AUTHENTICATOR_INFO_PAYLOAD_YK_BIO_5C: [u8; 409] = [ + 0xB3, // map(19) + 0x01, // unsigned(1) + 0x84, // array(4) + 0x66, // text(6) + 0x55, 0x32, 0x46, 0x5F, 0x56, 0x32, // "U2F_V2" + 0x68, // text(8) + 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30, // "FIDO_2_0" + 0x6C, // text(12) + 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52, + 0x45, // "FIDO_2_1_PRE" + 0x68, // text(8) + 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x31, // "FIDO_2_1" + 0x02, // unsigned(2) + 0x85, // array(5) + 0x6B, // text(11) + 0x63, 0x72, 0x65, 0x64, 0x50, 0x72, 0x6F, 0x74, 0x65, 0x63, 0x74, // "credProtect" + 0x6B, // text(11) + 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // "hmac-secret" + 0x6C, // text(12) + 0x6C, 0x61, 0x72, 0x67, 0x65, 0x42, 0x6C, 0x6F, 0x62, 0x4B, 0x65, + 0x79, // "largeBlobKey" + 0x68, // text(8) + 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, // "credBlob" + 0x6C, // text(12) + 0x6D, 0x69, 0x6E, 0x50, 0x69, 0x6E, 0x4C, 0x65, 0x6E, 0x67, 0x74, + 0x68, // "minPinLength" + 0x03, // unsigned(3) + 0x50, // bytes(16) + 0xD8, 0x52, 0x2D, 0x9F, 0x57, 0x5B, 0x48, 0x66, 0x88, 0xA9, 0xBA, 0x99, 0xFA, 0x02, 0xF3, + 0x5B, // "\xD8R-\x9FW[Hf\x88\xA9\xBA\x99\xFA\u0002\xF3[" + 0x04, // unsigned(4) + 0xB0, // map(16) + 0x62, // text(2) + 0x72, 0x6B, // "rk" + 0xF5, // primitive(21) + 0x62, // text(2) + 0x75, 0x70, // "up" + 0xF5, // primitive(21) + 0x62, // text(2) + 0x75, 0x76, // "uv" + 0xF5, // primitive(21) + 0x64, // text(4) + 0x70, 0x6C, 0x61, 0x74, // "plat" + 0xF4, // primitive(20) + 0x67, // text(7) + 0x75, 0x76, 0x54, 0x6F, 0x6B, 0x65, 0x6E, // "uvToken" + 0xF5, // primitive(21) + 0x68, // text(8) + 0x61, 0x6C, 0x77, 0x61, 0x79, 0x73, 0x55, 0x76, // "alwaysUv" + 0xF5, // primitive(21) + 0x68, // text(8) + 0x63, 0x72, 0x65, 0x64, 0x4D, 0x67, 0x6D, 0x74, // "credMgmt" + 0xF5, // primitive(21) + 0x69, // text(9) + 0x61, 0x75, 0x74, 0x68, 0x6E, 0x72, 0x43, 0x66, 0x67, // "authnrCfg" + 0xF5, // primitive(21) + 0x69, // text(9) + 0x62, 0x69, 0x6F, 0x45, 0x6E, 0x72, 0x6F, 0x6C, 0x6C, // "bioEnroll" + 0xF5, // primitive(21) + 0x69, // text(9) + 0x63, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x50, 0x69, 0x6E, // "clientPin" + 0xF5, // primitive(21) + 0x6A, // text(10) + 0x6C, 0x61, 0x72, 0x67, 0x65, 0x42, 0x6C, 0x6F, 0x62, 0x73, // "largeBlobs" + 0xF5, // primitive(21) + 0x6E, // text(14) + 0x70, 0x69, 0x6E, 0x55, 0x76, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6F, 0x6B, 0x65, + 0x6E, // "pinUvAuthToken" + 0xF5, // primitive(21) + 0x6F, // text(15) + 0x73, 0x65, 0x74, 0x4D, 0x69, 0x6E, 0x50, 0x49, 0x4E, 0x4C, 0x65, 0x6E, 0x67, 0x74, + 0x68, // "setMinPINLength" + 0xF5, // primitive(21) + 0x70, // text(16) + 0x6D, 0x61, 0x6B, 0x65, 0x43, 0x72, 0x65, 0x64, 0x55, 0x76, 0x4E, 0x6F, 0x74, 0x52, 0x71, + 0x64, // "makeCredUvNotRqd" + 0xF4, // primitive(20) + 0x75, // text(21) + 0x63, 0x72, 0x65, 0x64, 0x65, 0x6E, 0x74, 0x69, 0x61, 0x6C, 0x4D, 0x67, 0x6D, 0x74, 0x50, + 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, // "credentialMgmtPreview" + 0xF5, // primitive(21) + 0x78, 0x1B, // text(27) + 0x75, 0x73, 0x65, 0x72, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, + 0x6E, 0x4D, 0x67, 0x6D, 0x74, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65, + 0x77, // "userVerificationMgmtPreview" + 0xF5, // primitive(21) + 0x05, // unsigned(5) + 0x19, 0x04, 0xB0, // unsigned(1200) + 0x06, // unsigned(6) + 0x82, // array(2) + 0x02, // unsigned(2) + 0x01, // unsigned(1) + 0x07, // unsigned(7) + 0x08, // unsigned(8) + 0x08, // unsigned(8) + 0x18, 0x80, // unsigned(128) + 0x09, // unsigned(9) + 0x81, // array(1) + 0x63, // text(3) + 0x75, 0x73, 0x62, // "usb" + 0x0A, // unsigned(10) + 0x82, // array(2) + 0xA2, // map(2) + 0x63, // text(3) + 0x61, 0x6C, 0x67, // "alg" + 0x26, // negative(6) + 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" + 0x27, // negative(7) + 0x64, // text(4) + 0x74, 0x79, 0x70, 0x65, // "type" + 0x6A, // text(10) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // "public-key" + 0x0B, // unsigned(11) + 0x19, 0x04, 0x00, // unsigned(1024) + 0x0C, // unsigned(12) + 0xF4, // primitive(20) + 0x0D, // unsigned(13) + 0x04, // unsigned(4) + 0x0E, // unsigned(14) + 0x1A, 0x00, 0x05, 0x05, 0x06, // unsigned(328966) + 0x0F, // unsigned(15) + 0x18, 0x20, // unsigned(32) + 0x10, // unsigned(16) + 0x01, // unsigned(1) + 0x11, // unsigned(17) + 0x03, // unsigned(3) + 0x12, // unsigned(18) + 0x02, // unsigned(2) + 0x14, // unsigned(20) + 0x18, 0x18, // unsigned(24) + ]; + + #[test] + fn parse_authenticator_info() { + let authenticator_info: AuthenticatorInfo = + from_slice(&AUTHENTICATOR_INFO_PAYLOAD).unwrap(); + + let expected = AuthenticatorInfo { + versions: vec![AuthenticatorVersion::U2F_V2, AuthenticatorVersion::FIDO_2_0], + extensions: vec!["uvm".to_string(), "hmac-secret".to_string()], + aaguid: AAGuid(AAGUID_RAW), + options: AuthenticatorOptions { + platform_device: false, + resident_key: true, + client_pin: Some(false), + user_presence: true, + user_verification: None, + ..Default::default() + }, + max_msg_size: Some(1200), + pin_protocols: Some(vec![1]), + max_credential_count_in_list: None, + max_credential_id_length: None, + transports: None, + algorithms: None, + max_ser_large_blob_array: None, + force_pin_change: None, + min_pin_length: None, + firmware_version: None, + max_cred_blob_length: None, + max_rpids_for_set_min_pin_length: None, + preferred_platform_uv_attempts: None, + uv_modality: None, + certifications: None, + remaining_discoverable_credentials: None, + vendor_prototype_config_commands: None, + }; + + assert_eq!(authenticator_info, expected); + + // Test broken auth info + let mut broken_payload = AUTHENTICATOR_INFO_PAYLOAD.to_vec(); + // Have one more entry in the map + broken_payload[0] += 1; + // Add the additional entry at the back with an invalid key + broken_payload.extend_from_slice(&[ + 0x17, // unsigned(23) -> invalid key-number. CTAP2.1 goes only to 0x15 + 0x6B, // text(11) + 0x69, 0x6E, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x5F, 0x6B, 0x65, 0x79, // "invalid_key" + ]); + + let authenticator_info: AuthenticatorInfo = from_slice(&broken_payload).unwrap(); + assert_eq!(authenticator_info, expected); + } + + #[test] + fn parse_authenticator_info_yk_bio_5c() { + let authenticator_info: AuthenticatorInfo = + from_slice(&AUTHENTICATOR_INFO_PAYLOAD_YK_BIO_5C).unwrap(); + + let expected = AuthenticatorInfo { + versions: vec![ + AuthenticatorVersion::U2F_V2, + AuthenticatorVersion::FIDO_2_0, + AuthenticatorVersion::FIDO_2_1_PRE, + AuthenticatorVersion::FIDO_2_1, + ], + extensions: vec![ + "credProtect".to_string(), + "hmac-secret".to_string(), + "largeBlobKey".to_string(), + "credBlob".to_string(), + "minPinLength".to_string(), + ], + aaguid: AAGuid([ + 0xd8, 0x52, 0x2d, 0x9f, 0x57, 0x5b, 0x48, 0x66, 0x88, 0xa9, 0xba, 0x99, 0xfa, 0x02, + 0xf3, 0x5b, + ]), + options: AuthenticatorOptions { + platform_device: false, + resident_key: true, + client_pin: Some(true), + user_presence: true, + user_verification: Some(true), + pin_uv_auth_token: Some(true), + no_mc_ga_permissions_with_client_pin: None, + large_blobs: Some(true), + ep: None, + bio_enroll: Some(true), + user_verification_mgmt_preview: Some(true), + uv_bio_enroll: None, + authnr_cfg: Some(true), + uv_acfg: None, + cred_mgmt: Some(true), + credential_mgmt_preview: Some(true), + set_min_pin_length: Some(true), + make_cred_uv_not_rqd: Some(false), + always_uv: Some(true), + }, + max_msg_size: Some(1200), + pin_protocols: Some(vec![2, 1]), + max_credential_count_in_list: Some(8), + max_credential_id_length: Some(128), + transports: Some(vec!["usb".to_string()]), + algorithms: Some(vec![ + PublicKeyCredentialParameters { + alg: COSEAlgorithm::ES256, + }, + PublicKeyCredentialParameters { + alg: COSEAlgorithm::EDDSA, + }, + ]), + max_ser_large_blob_array: Some(1024), + force_pin_change: Some(false), + min_pin_length: Some(4), + firmware_version: Some(328966), + max_cred_blob_length: Some(32), + max_rpids_for_set_min_pin_length: Some(1), + preferred_platform_uv_attempts: Some(3), + uv_modality: Some(2), + certifications: None, + remaining_discoverable_credentials: Some(24), + vendor_prototype_config_commands: None, + }; + + assert_eq!(authenticator_info, expected); + } + + #[test] + fn test_get_info_ctap2_only() { + let mut device = Device::new("commands/get_info").unwrap(); + assert_eq!(device.get_protocol(), FidoProtocol::CTAP2); + let nonce = [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]; + + // channel id + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + + // init packet + let mut msg = CID_BROADCAST.to_vec(); + msg.extend(vec![HIDCmd::Init.into(), 0x00, 0x08]); // cmd + bcnt + msg.extend_from_slice(&nonce); + device.add_write(&msg, 0); + + // init_resp packet + let mut msg = CID_BROADCAST.to_vec(); + msg.extend(vec![ + 0x06, /* HIDCmd::Init without TYPE_INIT */ + 0x00, 0x11, + ]); // cmd + bcnt + msg.extend_from_slice(&nonce); + msg.extend_from_slice(&cid); // new channel id + + // We are setting NMSG, to signal that the device does not support CTAP1 + msg.extend(vec![0x02, 0x04, 0x01, 0x08, 0x01 | 0x04 | 0x08]); // versions + flags (wink+cbor+nmsg) + device.add_read(&msg, 0); + + // ctap2 request + let mut msg = cid.to_vec(); + msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x1]); // cmd + bcnt + msg.extend(vec![0x04]); // authenticatorGetInfo + device.add_write(&msg, 0); + + // ctap2 response + let mut msg = cid.to_vec(); + 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 - 8)..]); + device.add_read(&msg, 0); + device.init().expect("Failed to init device"); + + assert_eq!(device.get_cid(), &cid); + + let dev_info = device.get_device_info(); + assert_eq!( + dev_info.cap_flags, + Capability::WINK | Capability::CBOR | Capability::NMSG + ); + + let result = device + .get_authenticator_info() + .expect("Didn't get any authenticator_info"); + let expected = AuthenticatorInfo { + versions: vec![AuthenticatorVersion::U2F_V2, AuthenticatorVersion::FIDO_2_0], + extensions: vec!["uvm".to_string(), "hmac-secret".to_string()], + aaguid: AAGuid(AAGUID_RAW), + options: AuthenticatorOptions { + platform_device: false, + resident_key: true, + client_pin: Some(false), + user_presence: true, + user_verification: None, + ..Default::default() + }, + max_msg_size: Some(1200), + pin_protocols: Some(vec![1]), + max_credential_count_in_list: None, + max_credential_id_length: None, + transports: None, + algorithms: None, + max_ser_large_blob_array: None, + force_pin_change: None, + min_pin_length: None, + firmware_version: None, + max_cred_blob_length: None, + max_rpids_for_set_min_pin_length: None, + preferred_platform_uv_attempts: None, + uv_modality: None, + certifications: None, + remaining_discoverable_credentials: None, + vendor_prototype_config_commands: None, + }; + + assert_eq!(result, &expected); + } + + #[test] + fn test_authenticator_info_max_version() { + let fido2_0 = AuthenticatorInfo { + versions: vec![AuthenticatorVersion::U2F_V2, AuthenticatorVersion::FIDO_2_0], + ..Default::default() + }; + assert_eq!( + fido2_0.max_supported_version(), + AuthenticatorVersion::FIDO_2_0 + ); + + let fido2_1_pre = AuthenticatorInfo { + versions: vec![ + AuthenticatorVersion::FIDO_2_1_PRE, + AuthenticatorVersion::U2F_V2, + ], + ..Default::default() + }; + assert_eq!( + fido2_1_pre.max_supported_version(), + AuthenticatorVersion::FIDO_2_1_PRE + ); + + let fido2_1 = AuthenticatorInfo { + versions: vec![ + AuthenticatorVersion::FIDO_2_1_PRE, + AuthenticatorVersion::FIDO_2_1, + AuthenticatorVersion::U2F_V2, + AuthenticatorVersion::FIDO_2_0, + ], + ..Default::default() + }; + assert_eq!( + fido2_1.max_supported_version(), + AuthenticatorVersion::FIDO_2_1 + ); + } + + #[test] + fn parse_authenticator_info_protocol_versions() { + let mut expected = AuthenticatorInfo { + versions: vec![AuthenticatorVersion::U2F_V2, AuthenticatorVersion::FIDO_2_0], + extensions: vec!["uvm".to_string(), "hmac-secret".to_string()], + aaguid: AAGuid(AAGUID_RAW), + options: AuthenticatorOptions { + platform_device: false, + resident_key: true, + client_pin: Some(false), + user_presence: true, + ..Default::default() + }, + max_msg_size: Some(1200), + pin_protocols: None, + ..Default::default() + }; + + let raw_data = AUTHENTICATOR_INFO_PAYLOAD.to_vec(); + // pin protocol entry (last 3 bytes in payload): + // 0x06, // unsigned(6) + // 0x81, // array(1) + // 0x01, // unsigned(1) + + let mut raw_empty_list = raw_data.clone(); + raw_empty_list.pop(); + let raw_list_len = raw_empty_list.len(); + raw_empty_list[raw_list_len - 1] = 0x80; // array(0) instead of array(1) + + // Empty protocols-array, that should produce an error + from_slice::(&raw_empty_list).unwrap_err(); + + // No protocols specified + let mut raw_no_list = raw_data.clone(); + raw_no_list.pop(); + raw_no_list.pop(); + raw_no_list.pop(); + raw_no_list[0] = 0xa5; // map(5) instead of map(6) + + let authenticator_info: AuthenticatorInfo = from_slice(&raw_no_list).unwrap(); + assert_eq!(authenticator_info, expected); + + // Both 1 and 2 + let mut raw_list = raw_data; + let raw_list_len = raw_list.len(); + raw_list[raw_list_len - 2] = 0x82; // array(2) instead of array(1) + raw_list.push(0x02); + + expected.pin_protocols = Some(vec![1, 2]); + let authenticator_info: AuthenticatorInfo = from_slice(&raw_list).unwrap(); + assert_eq!(authenticator_info, expected); + } +} diff --git a/third_party/rust/authenticator/src/ctap2/commands/get_next_assertion.rs b/third_party/rust/authenticator/src/ctap2/commands/get_next_assertion.rs new file mode 100644 index 0000000000..b75bb51b08 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/get_next_assertion.rs @@ -0,0 +1,54 @@ +use super::{Command, CommandError, RequestCtap2, StatusCode}; +use crate::ctap2::commands::get_assertion::GetAssertionResponse; +use crate::transport::errors::HIDError; +use crate::transport::{FidoDevice, VirtualFidoDevice}; +use serde_cbor::{de::from_slice, Value}; + +#[derive(Debug)] +pub(crate) struct GetNextAssertion; + +impl RequestCtap2 for GetNextAssertion { + type Output = GetAssertionResponse; + + fn command(&self) -> Command { + Command::GetNextAssertion + } + + fn wire_format(&self) -> Result, HIDError> { + Ok(Vec::new()) + } + + fn handle_response_ctap2( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result { + if input.is_empty() { + return Err(CommandError::InputTooSmall.into()); + } + + 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::Deserializing)?; + // TODO(baloo): check assertion response does not have numberOfCredentials + Ok(assertion) + } else { + let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + Err(CommandError::StatusCode(status, Some(data)).into()) + } + } else if status.is_ok() { + Err(CommandError::InputTooSmall.into()) + } else { + Err(CommandError::StatusCode(status, None).into()) + } + } + + fn send_to_virtual_device( + &self, + _dev: &mut Dev, + ) -> Result { + unimplemented!() + } +} diff --git a/third_party/rust/authenticator/src/ctap2/commands/get_version.rs b/third_party/rust/authenticator/src/ctap2/commands/get_version.rs new file mode 100644 index 0000000000..40019c8f1a --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/get_version.rs @@ -0,0 +1,121 @@ +use super::{CommandError, CtapResponse, RequestCtap1, Retryable}; +use crate::consts::U2F_VERSION; +use crate::transport::errors::{ApduErrorStatus, HIDError}; +use crate::transport::{FidoDevice, VirtualFidoDevice}; +use crate::u2ftypes::CTAP1RequestAPDU; + +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum U2FInfo { + U2F_V2, +} + +impl CtapResponse for U2FInfo {} + +#[derive(Debug, Default)] +// TODO(baloo): if one does not issue U2F_VERSION before makecredentials or getassertion, token +// will return error (ConditionsNotSatified), test this in unit tests +pub struct GetVersion {} + +impl RequestCtap1 for GetVersion { + type Output = U2FInfo; + type AdditionalInfo = (); + + fn handle_response_ctap1( + &self, + _dev: &mut Dev, + _status: Result<(), ApduErrorStatus>, + input: &[u8], + _add_info: &(), + ) -> Result> { + if input.is_empty() { + return Err(Retryable::Error(HIDError::Command( + CommandError::InputTooSmall, + ))); + } + + let expected = String::from("U2F_V2"); + let result = String::from_utf8_lossy(input); + match result { + ref data if data == &expected => Ok(U2FInfo::U2F_V2), + _ => Err(Retryable::Error(HIDError::UnexpectedVersion)), + } + } + + fn ctap1_format(&self) -> Result<(Vec, ()), HIDError> { + let flags = 0; + + let cmd = U2F_VERSION; + let data = CTAP1RequestAPDU::serialize(cmd, flags, &[])?; + Ok((data, ())) + } + + fn send_to_virtual_device( + &self, + dev: &mut Dev, + ) -> Result { + dev.get_version(self) + } +} + +#[cfg(test)] +pub mod tests { + use crate::consts::{Capability, HIDCmd, CID_BROADCAST, SW_NO_ERROR}; + use crate::transport::device_selector::Device; + use crate::transport::{hid::HIDDevice, FidoDevice, FidoProtocol}; + use rand::{thread_rng, RngCore}; + + #[test] + fn test_get_version_ctap1_only() { + let mut device = Device::new("commands/get_version").unwrap(); + device.downgrade_to_ctap1(); + assert_eq!(device.get_protocol(), FidoProtocol::CTAP1); + let nonce = [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]; + + // channel id + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + + // init packet + let mut msg = CID_BROADCAST.to_vec(); + msg.extend([HIDCmd::Init.into(), 0x00, 0x08]); // cmd + bcnt + msg.extend_from_slice(&nonce); + device.add_write(&msg, 0); + + // init_resp packet + let mut msg = CID_BROADCAST.to_vec(); + msg.extend(vec![ + 0x06, /* HIDCmd::Init without !TYPE_INIT */ + 0x00, 0x11, + ]); // cmd + bcnt + msg.extend_from_slice(&nonce); + msg.extend_from_slice(&cid); // new channel id + + // We are not setting CBOR, to signal that the device does not support CTAP1 + msg.extend([0x02, 0x04, 0x01, 0x08, 0x01]); // versions + flags (wink) + device.add_read(&msg, 0); + + // ctap1 U2F_VERSION request + let mut msg = cid.to_vec(); + msg.extend([HIDCmd::Msg.into(), 0x0, 0x7]); // cmd + bcnt + msg.extend([0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0]); + device.add_write(&msg, 0); + + // fido response + let mut msg = cid.to_vec(); + msg.extend([HIDCmd::Msg.into(), 0x0, 0x08]); // cmd + bcnt + msg.extend([0x55, 0x32, 0x46, 0x5f, 0x56, 0x32]); // 'U2F_V2' + msg.extend(SW_NO_ERROR); + device.add_read(&msg, 0); + + device.init().expect("Failed to init device"); + + assert_eq!(device.get_cid(), &cid); + + let dev_info = device.get_device_info(); + assert_eq!(dev_info.cap_flags, Capability::WINK); + + let result = device.get_authenticator_info(); + assert!(result.is_none()); + } +} diff --git a/third_party/rust/authenticator/src/ctap2/commands/make_credentials.rs b/third_party/rust/authenticator/src/ctap2/commands/make_credentials.rs new file mode 100644 index 0000000000..1de1048404 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/make_credentials.rs @@ -0,0 +1,1061 @@ +use super::get_info::{AuthenticatorInfo, AuthenticatorVersion}; +use super::{ + Command, CommandError, CtapResponse, PinUvAuthCommand, RequestCtap1, RequestCtap2, Retryable, + StatusCode, +}; +use crate::consts::{PARAMETER_SIZE, U2F_REGISTER, U2F_REQUEST_USER_PRESENCE}; +use crate::crypto::{ + parse_u2f_der_certificate, COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve, + PinUvAuthParam, PinUvAuthToken, +}; +use crate::ctap2::attestation::{ + AAGuid, AttestationObject, AttestationStatement, AttestationStatementFidoU2F, + AttestedCredentialData, AuthenticatorData, AuthenticatorDataFlags, HmacSecretResponse, +}; +use crate::ctap2::client_data::ClientDataHash; +use crate::ctap2::server::{ + AuthenticationExtensionsClientInputs, AuthenticationExtensionsClientOutputs, + AuthenticatorAttachment, CredentialProtectionPolicy, PublicKeyCredentialDescriptor, + PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, RelyingParty, RpIdHash, + UserVerificationRequirement, +}; +use crate::ctap2::utils::{read_byte, serde_parse_err}; +use crate::errors::AuthenticatorError; +use crate::transport::errors::{ApduErrorStatus, HIDError}; +use crate::transport::{FidoDevice, VirtualFidoDevice}; +use crate::u2ftypes::CTAP1RequestAPDU; +use serde::{ + de::{Error as DesError, MapAccess, Unexpected, Visitor}, + ser::SerializeMap, + Deserialize, Deserializer, Serialize, Serializer, +}; +use serde_cbor::{self, de::from_slice, ser, Value}; +use std::fmt; +use std::io::{Cursor, Read}; + +#[derive(Debug, PartialEq, Eq)] +pub struct MakeCredentialsResult { + pub att_obj: AttestationObject, + pub attachment: AuthenticatorAttachment, + pub extensions: AuthenticationExtensionsClientOutputs, +} + +impl MakeCredentialsResult { + pub fn from_ctap1(input: &[u8], rp_id_hash: &RpIdHash) -> Result { + let mut data = Cursor::new(input); + let magic_num = read_byte(&mut data).map_err(CommandError::Deserializing)?; + if magic_num != 0x05 { + error!("error while parsing registration: magic header not 0x05, but {magic_num}"); + return Err(CommandError::Deserializing(DesError::invalid_value( + serde::de::Unexpected::Unsigned(magic_num as u64), + &"0x05", + ))); + } + let mut public_key = [0u8; 65]; + data.read_exact(&mut public_key) + .map_err(|_| CommandError::Deserializing(serde_parse_err("PublicKey")))?; + + let credential_id_len = read_byte(&mut data).map_err(CommandError::Deserializing)?; + let mut credential_id = vec![0u8; credential_id_len as usize]; + data.read_exact(&mut credential_id) + .map_err(|_| CommandError::Deserializing(serde_parse_err("CredentialId")))?; + + let cert_and_sig = parse_u2f_der_certificate(&data.get_ref()[data.position() as usize..]) + .map_err(|err| { + CommandError::Deserializing(serde_parse_err(&format!( + "Certificate and Signature: {err:?}", + ))) + })?; + + let credential_ec2_key = COSEEC2Key::from_sec1_uncompressed(Curve::SECP256R1, &public_key) + .map_err(|err| { + CommandError::Deserializing(serde_parse_err(&format!("EC2 Key: {err:?}",))) + })?; + + let credential_public_key = COSEKey { + alg: COSEAlgorithm::ES256, + key: COSEKeyType::EC2(credential_ec2_key), + }; + + let auth_data = AuthenticatorData { + rp_id_hash: rp_id_hash.clone(), + // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#u2f-authenticatorMakeCredential-interoperability + // "Let flags be a byte whose zeroth bit (bit 0, UP) is set, and whose sixth bit + // (bit 6, AT) is set, and all other bits are zero (bit zero is the least + // significant bit)" + flags: AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::ATTESTED, + counter: 0, + credential_data: Some(AttestedCredentialData { + aaguid: AAGuid::default(), + credential_id, + credential_public_key, + }), + extensions: Default::default(), + }; + + let att_stmt = AttestationStatement::FidoU2F(AttestationStatementFidoU2F::new( + cert_and_sig.certificate, + cert_and_sig.signature, + )); + + let att_obj = AttestationObject { + auth_data, + att_stmt, + }; + + Ok(Self { + att_obj, + attachment: AuthenticatorAttachment::Unknown, + extensions: Default::default(), + }) + } +} + +impl<'de> Deserialize<'de> for MakeCredentialsResult { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct MakeCredentialsResultVisitor; + + impl<'de> Visitor<'de> for MakeCredentialsResultVisitor { + type Value = MakeCredentialsResult; + + 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<&str> = None; + let mut auth_data: Option = None; + let mut att_stmt: Option = None; + + while let Some(key) = map.next_key()? { + match key { + 1 => { + if format.is_some() { + return Err(DesError::duplicate_field("fmt (0x01)")); + } + format = Some(map.next_value()?); + } + 2 => { + if auth_data.is_some() { + return Err(DesError::duplicate_field("authData (0x02)")); + } + auth_data = Some(map.next_value()?); + } + 3 => { + let format = + format.ok_or_else(|| DesError::missing_field("fmt (0x01)"))?; + if att_stmt.is_some() { + return Err(DesError::duplicate_field("attStmt (0x03)")); + } + att_stmt = match format { + "none" => { + let map: std::collections::BTreeMap<(), ()> = + map.next_value()?; + if !map.is_empty() { + return Err(DesError::invalid_value( + Unexpected::Map, + &"the empty map", + )); + } + Some(AttestationStatement::None) + } + "packed" => Some(AttestationStatement::Packed(map.next_value()?)), + "fido-u2f" => { + Some(AttestationStatement::FidoU2F(map.next_value()?)) + } + _ => { + return Err(DesError::custom( + "unknown attestation statement format", + )) + } + } + } + _ => continue, + } + } + + let auth_data = auth_data + .ok_or_else(|| M::Error::custom("found no authData (0x02)".to_string()))?; + let att_stmt = att_stmt + .ok_or_else(|| M::Error::custom("found no attStmt (0x03)".to_string()))?; + + Ok(MakeCredentialsResult { + att_obj: AttestationObject { + auth_data, + att_stmt, + }, + attachment: AuthenticatorAttachment::Unknown, + extensions: Default::default(), + }) + } + } + + deserializer.deserialize_bytes(MakeCredentialsResultVisitor) + } +} + +impl CtapResponse for MakeCredentialsResult {} + +#[derive(Copy, Clone, Debug, Default, Serialize)] +#[cfg_attr(test, derive(Deserialize))] +pub struct MakeCredentialsOptions { + #[serde(rename = "rk", skip_serializing_if = "Option::is_none")] + pub resident_key: Option, + #[serde(rename = "uv", skip_serializing_if = "Option::is_none")] + pub user_verification: Option, + // TODO(MS): ctap2.1 supports user_presence, but ctap2.0 does not and tokens will error out + // Commands need a version-flag to know what to de/serialize and what to ignore. +} + +impl MakeCredentialsOptions { + pub(crate) fn has_some(&self) -> bool { + self.resident_key.is_some() || self.user_verification.is_some() + } +} + +pub(crate) trait UserVerification { + fn ask_user_verification(&self) -> bool; +} + +impl UserVerification for MakeCredentialsOptions { + fn ask_user_verification(&self) -> bool { + if let Some(e) = self.user_verification { + e + } else { + false + } + } +} + +#[derive(Debug, Default, Clone, Serialize)] +pub struct MakeCredentialsExtensions { + #[serde(skip_serializing)] + pub cred_props: Option, + #[serde(rename = "credProtect", skip_serializing_if = "Option::is_none")] + pub cred_protect: Option, + #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")] + pub hmac_secret: Option, + #[serde(rename = "minPinLength", skip_serializing_if = "Option::is_none")] + pub min_pin_length: Option, +} + +impl MakeCredentialsExtensions { + fn has_content(&self) -> bool { + self.cred_protect.is_some() || self.hmac_secret.is_some() || self.min_pin_length.is_some() + } +} + +impl From for MakeCredentialsExtensions { + fn from(input: AuthenticationExtensionsClientInputs) -> Self { + Self { + cred_props: input.cred_props, + cred_protect: input.credential_protection_policy, + hmac_secret: input.hmac_create_secret, + min_pin_length: input.min_pin_length, + } + } +} + +#[derive(Debug, Clone)] +pub struct MakeCredentials { + pub client_data_hash: ClientDataHash, + pub rp: RelyingParty, + // Note(baloo): If none -> ctap1 + pub user: Option, + pub pub_cred_params: Vec, + pub 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. + pub extensions: MakeCredentialsExtensions, + pub options: MakeCredentialsOptions, + pub pin_uv_auth_param: Option, + pub enterprise_attestation: Option, +} + +impl MakeCredentials { + #[allow(clippy::too_many_arguments)] + pub fn new( + client_data_hash: ClientDataHash, + rp: RelyingParty, + user: Option, + pub_cred_params: Vec, + exclude_list: Vec, + options: MakeCredentialsOptions, + extensions: MakeCredentialsExtensions, + ) -> Self { + Self { + client_data_hash, + rp, + user, + pub_cred_params, + exclude_list, + extensions, + options, + pin_uv_auth_param: None, + enterprise_attestation: None, + } + } + + pub fn finalize_result(&self, dev: &Dev, result: &mut MakeCredentialsResult) { + let maybe_info = dev.get_authenticator_info(); + + result.attachment = match maybe_info { + Some(info) if info.options.platform_device => AuthenticatorAttachment::Platform, + Some(_) => AuthenticatorAttachment::CrossPlatform, + None => AuthenticatorAttachment::Unknown, + }; + + // Handle extensions whose outputs are not encoded in the authenticator data. + // 1. credProps + // "set clientExtensionResults["credProps"]["rk"] to the value of the + // requireResidentKey parameter that was used in the invocation of the + // authenticatorMakeCredential operation." + // Note: a CTAP 2.0 authenticator is allowed to create a discoverable credential even + // if one was not requested, so there is a case in which we cannot confidently + // return `rk=false` here. We omit the response entirely in this case. + let dev_supports_rk = maybe_info.map_or(false, |info| info.options.resident_key); + let requested_rk = self.options.resident_key.unwrap_or(false); + let max_supported_version = maybe_info.map_or(AuthenticatorVersion::U2F_V2, |info| { + info.max_supported_version() + }); + let rk_uncertain = max_supported_version == AuthenticatorVersion::FIDO_2_0 + && dev_supports_rk + && !requested_rk; + if self.extensions.cred_props == Some(true) && !rk_uncertain { + result + .extensions + .cred_props + .get_or_insert(Default::default()) + .rk = requested_rk; + } + + // 2. hmac-secret + // The extension returns a flag in the authenticator data which we need to mirror as a + // client output. + if self.extensions.hmac_secret == Some(true) { + if let Some(HmacSecretResponse::Confirmed(flag)) = + result.att_obj.auth_data.extensions.hmac_secret + { + result.extensions.hmac_create_secret = Some(flag); + } + } + } +} + +impl PinUvAuthCommand for MakeCredentials { + fn set_pin_uv_auth_param( + &mut self, + pin_uv_auth_token: Option, + ) -> Result<(), AuthenticatorError> { + let mut param = None; + if let Some(token) = pin_uv_auth_token { + param = Some( + token + .derive(self.client_data_hash.as_ref()) + .map_err(CommandError::Crypto)?, + ); + } + self.pin_uv_auth_param = param; + Ok(()) + } + + fn set_uv_option(&mut self, uv: Option) { + self.options.user_verification = uv; + } + + fn get_rp_id(&self) -> Option<&String> { + Some(&self.rp.id) + } + + fn can_skip_user_verification( + &mut self, + info: &AuthenticatorInfo, + uv_req: UserVerificationRequirement, + ) -> bool { + // TODO(MS): Handle here the case where we NEED a UV, the device supports PINs, but hasn't set a PIN. + // For this, the user has to be prompted to set a PIN first (see https://github.com/mozilla/authenticator-rs/issues/223) + + let supports_uv = info.options.user_verification == Some(true); + let pin_configured = info.options.client_pin == Some(true); + + // CTAP 2.0 authenticators require user verification if the device is protected + let device_protected = supports_uv || pin_configured; + + // CTAP 2.1 authenticators may allow the creation of non-discoverable credentials without + // user verification. This is only relevant if the relying party has not requested user + // verification. + let make_cred_uv_not_required = info.options.make_cred_uv_not_rqd == Some(true) + && self.options.resident_key != Some(true) + && uv_req == UserVerificationRequirement::Discouraged; + + // Alternatively, CTAP 2.1 authenticators may require user verification regardless of the + // RP's requirement. + let always_uv = info.options.always_uv == Some(true); + + !always_uv && (!device_protected || make_cred_uv_not_required) + } + + fn get_pin_uv_auth_param(&self) -> Option<&PinUvAuthParam> { + self.pin_uv_auth_param.as_ref() + } +} + +impl Serialize for MakeCredentials { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + debug!("Serialize MakeCredentials"); + // 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.has_content() { + map_len += 1; + } + if self.options.has_some() { + map_len += 1; + } + if self.pin_uv_auth_param.is_some() { + map_len += 2; + } + if self.enterprise_attestation.is_some() { + map_len += 1; + } + + let mut map = serializer.serialize_map(Some(map_len))?; + map.serialize_entry(&0x01, &self.client_data_hash)?; + map.serialize_entry(&0x02, &self.rp)?; + map.serialize_entry(&0x03, &self.user)?; + map.serialize_entry(&0x04, &self.pub_cred_params)?; + if !self.exclude_list.is_empty() { + map.serialize_entry(&0x05, &self.exclude_list)?; + } + if self.extensions.has_content() { + map.serialize_entry(&0x06, &self.extensions)?; + } + if self.options.has_some() { + map.serialize_entry(&0x07, &self.options)?; + } + if let Some(pin_uv_auth_param) = &self.pin_uv_auth_param { + map.serialize_entry(&0x08, &pin_uv_auth_param)?; + map.serialize_entry(&0x09, &pin_uv_auth_param.pin_protocol.id())?; + } + if let Some(enterprise_attestation) = self.enterprise_attestation { + map.serialize_entry(&0x0a, &enterprise_attestation)?; + } + map.end() + } +} + +impl RequestCtap1 for MakeCredentials { + type Output = MakeCredentialsResult; + type AdditionalInfo = (); + + fn ctap1_format(&self) -> Result<(Vec, ()), HIDError> { + let flags = U2F_REQUEST_USER_PRESENCE; + + let mut register_data = Vec::with_capacity(2 * PARAMETER_SIZE); + register_data.extend_from_slice(self.client_data_hash.as_ref()); + register_data.extend_from_slice(self.rp.hash().as_ref()); + let cmd = U2F_REGISTER; + let apdu = CTAP1RequestAPDU::serialize(cmd, flags, ®ister_data)?; + + Ok((apdu, ())) + } + + fn handle_response_ctap1( + &self, + dev: &mut Dev, + status: Result<(), ApduErrorStatus>, + input: &[u8], + _add_info: &(), + ) -> Result> { + if Err(ApduErrorStatus::ConditionsNotSatisfied) == status { + return Err(Retryable::Retry); + } + if let Err(err) = status { + return Err(Retryable::Error(HIDError::ApduStatus(err))); + } + + let mut output = MakeCredentialsResult::from_ctap1(input, &self.rp.hash()) + .map_err(|e| Retryable::Error(HIDError::Command(e)))?; + self.finalize_result(dev, &mut output); + Ok(output) + } + + fn send_to_virtual_device( + &self, + dev: &mut Dev, + ) -> Result { + let mut output = dev.make_credentials(self)?; + self.finalize_result(dev, &mut output); + Ok(output) + } +} + +impl RequestCtap2 for MakeCredentials { + type Output = MakeCredentialsResult; + + fn command(&self) -> Command { + Command::MakeCredentials + } + + fn wire_format(&self) -> Result, HIDError> { + Ok(ser::to_vec(&self).map_err(CommandError::Serializing)?) + } + + fn handle_response_ctap2( + &self, + dev: &mut Dev, + input: &[u8], + ) -> Result { + if input.is_empty() { + return Err(HIDError::Command(CommandError::InputTooSmall)); + } + + let status: StatusCode = input[0].into(); + debug!("response status code: {:?}", status); + if input.len() == 1 { + if status.is_ok() { + return Err(HIDError::Command(CommandError::InputTooSmall)); + } + return Err(HIDError::Command(CommandError::StatusCode(status, None))); + } + + if status.is_ok() { + let mut output: MakeCredentialsResult = + from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + self.finalize_result(dev, &mut output); + Ok(output) + } else { + let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + Err(HIDError::Command(CommandError::StatusCode( + status, + Some(data), + ))) + } + } + + fn send_to_virtual_device( + &self, + dev: &mut Dev, + ) -> Result { + let mut output = dev.make_credentials(self)?; + self.finalize_result(dev, &mut output); + Ok(output) + } +} + +pub(crate) fn dummy_make_credentials_cmd() -> MakeCredentials { + let mut req = MakeCredentials::new( + // Hardcoded hash of: + // CollectedClientData { + // webauthn_type: WebauthnType::Create, + // challenge: Challenge::new(vec![0, 1, 2, 3, 4]), + // origin: String::new(), + // cross_origin: false, + // token_binding: None, + // } + ClientDataHash([ + 208, 206, 230, 252, 125, 191, 89, 154, 145, 157, 184, 251, 149, 19, 17, 38, 159, 14, + 183, 129, 247, 132, 28, 108, 192, 84, 74, 217, 218, 52, 21, 75, + ]), + RelyingParty::from("make.me.blink"), + Some(PublicKeyCredentialUserEntity { + id: vec![0], + name: Some(String::from("make.me.blink")), + ..Default::default() + }), + vec![PublicKeyCredentialParameters { + alg: COSEAlgorithm::ES256, + }], + vec![], + MakeCredentialsOptions::default(), + MakeCredentialsExtensions::default(), + ); + // Using a zero-length pinAuth will trigger the device to blink. + // For CTAP1, this gets ignored anyways and we do a 'normal' register + // command, which also just blinks. + req.pin_uv_auth_param = Some(PinUvAuthParam::create_empty()); + req +} + +#[cfg(test)] +pub mod test { + use super::{MakeCredentials, MakeCredentialsOptions, MakeCredentialsResult}; + use crate::crypto::{COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve}; + use crate::ctap2::attestation::test::create_attestation_obj; + use crate::ctap2::attestation::{ + AAGuid, AttestationCertificate, AttestationObject, AttestationStatement, + AttestationStatementFidoU2F, AttestedCredentialData, AuthenticatorData, + AuthenticatorDataFlags, Signature, + }; + use crate::ctap2::client_data::{Challenge, CollectedClientData, TokenBinding, WebauthnType}; + use crate::ctap2::commands::{RequestCtap1, RequestCtap2}; + use crate::ctap2::server::RpIdHash; + use crate::ctap2::server::{ + AuthenticatorAttachment, PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, + RelyingParty, + }; + use crate::transport::device_selector::Device; + use crate::transport::hid::HIDDevice; + use crate::transport::{FidoDevice, FidoProtocol}; + use base64::Engine; + + #[test] + fn test_make_credentials_ctap2() { + let req = MakeCredentials::new( + CollectedClientData { + webauthn_type: WebauthnType::Create, + challenge: Challenge::from(vec![0x00, 0x01, 0x02, 0x03]), + origin: String::from("example.com"), + cross_origin: false, + token_binding: Some(TokenBinding::Present(String::from("AAECAw"))), + } + .hash() + .expect("failed to serialize client data"), + RelyingParty { + id: String::from("example.com"), + name: Some(String::from("Acme")), + }, + Some(PublicKeyCredentialUserEntity { + id: base64::engine::general_purpose::URL_SAFE + .decode("MIIBkzCCATigAwIBAjCCAZMwggE4oAMCAQIwggGTMII=") + .unwrap(), + name: Some(String::from("johnpsmith@example.com")), + display_name: Some(String::from("John P. Smith")), + }), + vec![ + PublicKeyCredentialParameters { + alg: COSEAlgorithm::ES256, + }, + PublicKeyCredentialParameters { + alg: COSEAlgorithm::RS256, + }, + ], + Vec::new(), + MakeCredentialsOptions { + resident_key: Some(true), + user_verification: None, + }, + Default::default(), + ); + + let mut device = Device::new("commands/make_credentials").unwrap(); // not really used (all functions ignore it) + assert_eq!(device.get_protocol(), FidoProtocol::CTAP2); + let req_serialized = req + .wire_format() + .expect("Failed to serialize MakeCredentials request"); + assert_eq!(req_serialized, MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP2); + let make_cred_result = req + .handle_response_ctap2(&mut device, &MAKE_CREDENTIALS_SAMPLE_RESPONSE_CTAP2) + .expect("Failed to handle CTAP2 response"); + + let expected = MakeCredentialsResult { + att_obj: create_attestation_obj(), + attachment: AuthenticatorAttachment::Unknown, + extensions: Default::default(), + }; + + assert_eq!(make_cred_result, expected); + } + + #[test] + fn test_make_credentials_ctap1() { + let req = MakeCredentials::new( + CollectedClientData { + webauthn_type: WebauthnType::Create, + challenge: Challenge::new(vec![0x00, 0x01, 0x02, 0x03]), + origin: String::from("example.com"), + cross_origin: false, + token_binding: Some(TokenBinding::Present(String::from("AAECAw"))), + } + .hash() + .expect("failed to serialize client data"), + RelyingParty::from("example.com"), + Some(PublicKeyCredentialUserEntity { + id: base64::engine::general_purpose::URL_SAFE + .decode("MIIBkzCCATigAwIBAjCCAZMwggE4oAMCAQIwggGTMII=") + .unwrap(), + name: Some(String::from("johnpsmith@example.com")), + display_name: Some(String::from("John P. Smith")), + }), + vec![ + PublicKeyCredentialParameters { + alg: COSEAlgorithm::ES256, + }, + PublicKeyCredentialParameters { + alg: COSEAlgorithm::RS256, + }, + ], + Vec::new(), + MakeCredentialsOptions { + resident_key: Some(true), + user_verification: None, + }, + Default::default(), + ); + + let (req_serialized, _) = req + .ctap1_format() + .expect("Failed to serialize MakeCredentials request"); + assert_eq!( + req_serialized, MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP1, + "\nGot: {req_serialized:X?}\nExpected: {MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP1:X?}" + ); + let mut device = Device::new("commands/make_credentials").unwrap(); // not really used + let make_cred_result = req + .handle_response_ctap1( + &mut device, + Ok(()), + &MAKE_CREDENTIALS_SAMPLE_RESPONSE_CTAP1, + &(), + ) + .expect("Failed to handle CTAP1 response"); + + let att_obj = 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::USER_PRESENT | AuthenticatorDataFlags::ATTESTED, + 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, + ], + credential_public_key: COSEKey { + alg: COSEAlgorithm::ES256, + key: COSEKeyType::EC2(COSEEC2Key { + curve: Curve::SECP256R1, + x: vec![ + 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, + ], + y: vec![ + 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: Default::default(), + }, + att_stmt: AttestationStatement::FidoU2F(AttestationStatementFidoU2F { + sig: Signature(vec![ + 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, + ]), + attestation_cert: vec![AttestationCertificate(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, + ])], + }), + }; + + let expected = MakeCredentialsResult { + att_obj, + attachment: AuthenticatorAttachment::Unknown, + extensions: Default::default(), + }; + + assert_eq!(make_cred_result, expected); + } + + // This includes a CTAP2 encoded attestation object that is identical to + // the WebAuthn encoded attestation object in `ctap2::attestation::test::SAMPLE_ATTESTATION`. + // Both values decode to `ctap2::attestation::test::create_attestation_obj`. + #[rustfmt::skip] + pub const MAKE_CREDENTIALS_SAMPLE_RESPONSE_CTAP2: [u8; 660] = [ + 0x00, // status = success + 0xa3, // map(3) + 0x01, // unsigned(1) + 0x66, // text(6) + 0x70, 0x61, 0x63, 0x6b, 0x65, 0x64, // "packed" + 0x02, // unsigned(2) + 0x58, 0x94, // bytes(148) + // 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 + // credential public key + 0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20, 0xa5, 0xfd, 0x5c, 0xe1, 0xb1, 0xc4, + 0x58, 0xc5, 0x30, 0xa5, 0x4f, 0xa6, 0x1b, 0x31, 0xbf, 0x6b, 0x04, 0xbe, 0x8b, 0x97, 0xaf, 0xde, + 0x54, 0xdd, 0x8c, 0xbb, 0x69, 0x27, 0x5a, 0x8a, 0x1b, 0xe1, 0x22, 0x58, 0x20, 0xfa, 0x3a, 0x32, + 0x31, 0xdd, 0x9d, 0xee, 0xd9, 0xd1, 0x89, 0x7b, 0xe5, 0xa6, 0x22, 0x8c, 0x59, 0x50, 0x1e, 0x4b, + 0xcd, 0x12, 0x97, 0x5d, 0x3d, 0xff, 0x73, 0x0f, 0x01, 0x27, 0x8e, 0xa6, 0x1c, + 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; 210] = [ + // 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 (see client_data.rs)) + 0xa5, // map(5) + 0x01, // unsigned(1) - clientDataHash + 0x58, 0x20, // bytes(32) + 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, // hash + 0x32, 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, // hash + 0x10, 0x87, 0x54, 0xc3, 0x2d, 0x80, // hash + 0x02, // unsigned(2) - rp + 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" + 0x64, // text(4) + 0x6e, 0x61, 0x6d, 0x65, // "name" + 0x64, // text(4) + 0x41, 0x63, 0x6d, 0x65, // "Acme" + 0x03, // unsigned(3) - user + 0xa3, // map(3) + 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) + 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, // CLA + 0x1, // INS U2F_Register + 0x3, // P1 Flags + 0x0, // P2 + 0x0, 0x0, 0x40, // Lc + // 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: + 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, // hash + 0x32, 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, // hash + 0x10, 0x87, 0x54, 0xc3, 0x2d, 0x80, // 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, // .. + // Le (Ne=65536): + 0x0, 0x0, + ]; + + 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/third_party/rust/authenticator/src/ctap2/commands/mod.rs b/third_party/rust/authenticator/src/ctap2/commands/mod.rs new file mode 100644 index 0000000000..12990122d9 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/mod.rs @@ -0,0 +1,477 @@ +use crate::crypto::{CryptoError, PinUvAuthParam, PinUvAuthToken}; +use crate::ctap2::commands::client_pin::{GetPinRetries, GetUvRetries, PinError}; +use crate::ctap2::commands::get_info::AuthenticatorInfo; +use crate::ctap2::server::UserVerificationRequirement; +use crate::errors::AuthenticatorError; +use crate::transport::errors::{ApduErrorStatus, HIDError}; +use crate::transport::{FidoDevice, VirtualFidoDevice}; +use serde_cbor::{error::Error as CborError, Value}; +use serde_json as json; +use std::error::Error as StdErrorT; +use std::fmt; + +pub mod authenticator_config; +pub mod bio_enrollment; +pub mod client_pin; +pub mod credential_management; +pub mod get_assertion; +pub mod get_info; +pub mod get_next_assertion; +pub mod get_version; +pub mod make_credentials; +pub mod reset; +pub mod selection; + +/// 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 enum Retryable { + Retry, + Error(T), +} + +impl Retryable { + pub fn is_retry(&self) -> bool { + matches!(*self, Retryable::Retry) + } + + pub fn is_error(&self) -> bool { + !self.is_retry() + } +} + +impl From for Retryable { + fn from(e: T) -> Self { + Retryable::Error(e) + } +} + +pub trait RequestCtap1: fmt::Debug { + type Output: CtapResponse; + // E.g.: For GetAssertion, which key-handle is currently being tested + type AdditionalInfo; + + /// Serializes a request into FIDO v1.x / CTAP1 / U2F format. + /// + /// See [`crate::u2ftypes::CTAP1RequestAPDU::serialize()`] + fn ctap1_format(&self) -> Result<(Vec, Self::AdditionalInfo), HIDError>; + + /// Deserializes a response from FIDO v1.x / CTAP1 / U2Fv2 format. + fn handle_response_ctap1( + &self, + dev: &mut Dev, + status: Result<(), ApduErrorStatus>, + input: &[u8], + add_info: &Self::AdditionalInfo, + ) -> Result>; + + fn send_to_virtual_device( + &self, + dev: &mut Dev, + ) -> Result; +} + +pub trait RequestCtap2: fmt::Debug { + type Output: CtapResponse; + + fn command(&self) -> Command; + + fn wire_format(&self) -> Result, HIDError>; + + fn handle_response_ctap2( + &self, + dev: &mut Dev, + input: &[u8], + ) -> Result; + + fn send_to_virtual_device( + &self, + dev: &mut Dev, + ) -> Result; +} + +// Sadly, needs to be 'static to enable us in tests to collect them in a Vec +// but all of them are 'static, so this is currently no problem. +pub trait CtapResponse: std::fmt::Debug + 'static {} + +#[derive(Debug, Clone)] +pub enum PinUvAuthResult { + /// Request is CTAP1 and does not need PinUvAuth + RequestIsCtap1, + /// Device is not capable of CTAP2 + DeviceIsCtap1, + /// Device does not support UV or PINs + NoAuthTypeSupported, + /// Request doesn't want user verification (uv = "discouraged") + NoAuthRequired, + /// Device is CTAP2.0 and has internal UV capability + UsingInternalUv, + /// Successfully established PinUvAuthToken via GetPinToken (CTAP2.0) + SuccessGetPinToken(PinUvAuthToken), + /// Successfully established PinUvAuthToken via UV (CTAP2.1) + SuccessGetPinUvAuthTokenUsingUvWithPermissions(PinUvAuthToken), + /// Successfully established PinUvAuthToken via Pin (CTAP2.1) + SuccessGetPinUvAuthTokenUsingPinWithPermissions(PinUvAuthToken), +} + +impl PinUvAuthResult { + pub(crate) fn get_pin_uv_auth_token(&self) -> Option { + match self { + PinUvAuthResult::RequestIsCtap1 + | PinUvAuthResult::DeviceIsCtap1 + | PinUvAuthResult::NoAuthTypeSupported + | PinUvAuthResult::NoAuthRequired + | PinUvAuthResult::UsingInternalUv => None, + PinUvAuthResult::SuccessGetPinToken(token) => Some(token.clone()), + PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions(token) => { + Some(token.clone()) + } + PinUvAuthResult::SuccessGetPinUvAuthTokenUsingPinWithPermissions(token) => { + Some(token.clone()) + } + } + } +} + +/// Helper-trait to determine pin_uv_auth_param from PIN or UV. +pub(crate) trait PinUvAuthCommand: RequestCtap2 { + fn set_pin_uv_auth_param( + &mut self, + pin_uv_auth_token: Option, + ) -> Result<(), AuthenticatorError>; + fn get_pin_uv_auth_param(&self) -> Option<&PinUvAuthParam>; + fn set_uv_option(&mut self, uv: Option); + fn get_rp_id(&self) -> Option<&String>; + fn can_skip_user_verification( + &mut self, + info: &AuthenticatorInfo, + uv_req: UserVerificationRequirement, + ) -> bool; +} + +pub(crate) fn repackage_pin_errors( + dev: &mut D, + error: HIDError, +) -> AuthenticatorError { + match error { + HIDError::Command(CommandError::StatusCode(StatusCode::PinInvalid, _)) => { + // If the given PIN was wrong, determine no. of left retries + let cmd = GetPinRetries::new(); + // Treat any error as if the device returned a valid response without a pinRetries + // field. + let resp = dev.send_cbor(&cmd).unwrap_or_default(); + AuthenticatorError::PinError(PinError::InvalidPin(resp.pin_retries)) + } + HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthBlocked, _)) => { + AuthenticatorError::PinError(PinError::PinAuthBlocked) + } + HIDError::Command(CommandError::StatusCode(StatusCode::PinBlocked, _)) => { + AuthenticatorError::PinError(PinError::PinBlocked) + } + HIDError::Command(CommandError::StatusCode(StatusCode::PinRequired, _)) => { + AuthenticatorError::PinError(PinError::PinRequired) + } + HIDError::Command(CommandError::StatusCode(StatusCode::PinNotSet, _)) => { + AuthenticatorError::PinError(PinError::PinNotSet) + } + HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthInvalid, _)) => { + AuthenticatorError::PinError(PinError::PinAuthInvalid) + } + HIDError::Command(CommandError::StatusCode(StatusCode::UvInvalid, _)) => { + // If the internal UV failed, determine no. of left retries + let cmd = GetUvRetries::new(); + // Treat any error as if the device returned a valid response without a uvRetries + // field. + let resp = dev.send_cbor(&cmd).unwrap_or_default(); + AuthenticatorError::PinError(PinError::InvalidUv(resp.uv_retries)) + } + HIDError::Command(CommandError::StatusCode(StatusCode::UvBlocked, _)) => { + AuthenticatorError::PinError(PinError::UvBlocked) + } + // TODO(MS): Add "PinPolicyViolated" + err => AuthenticatorError::HIDError(err), + } +} + +// Spec: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticator-api +// and: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#authenticator-api +#[repr(u8)] +#[derive(Debug, PartialEq, Clone)] +pub enum Command { + MakeCredentials = 0x01, + GetAssertion = 0x02, + GetInfo = 0x04, + ClientPin = 0x06, + Reset = 0x07, + GetNextAssertion = 0x08, + BioEnrollment = 0x09, + CredentialManagement = 0x0A, + Selection = 0x0B, + AuthenticatorConfig = 0x0D, + BioEnrollmentPreview = 0x40, + CredentialManagementPreview = 0x41, +} + +#[derive(Debug)] +pub enum StatusCode { + /// Indicates successful response. + OK, + /// The command is not a valid CTAP command. + InvalidCommand, + /// The command included an invalid parameter. + InvalidParameter, + /// Invalid message or item length. + InvalidLength, + /// Invalid message sequencing. + InvalidSeq, + /// Message timed out. + Timeout, + /// Channel busy. + ChannelBusy, + /// Command requires channel lock. + LockRequired, + /// Command not allowed on this cid. + InvalidChannel, + /// Invalid/unexpected CBOR error. + CBORUnexpectedType, + /// Error when parsing CBOR. + InvalidCBOR, + /// Missing non-optional parameter. + MissingParameter, + /// Limit for number of items exceeded. + LimitExceeded, + /// Unsupported extension. + UnsupportedExtension, + /// Valid credential found in the exclude list. + CredentialExcluded, + /// Processing (Lengthy operation is in progress). + Processing, + /// Credential not valid for the authenticator. + InvalidCredential, + /// Authentication is waiting for user interaction. + UserActionPending, + /// Processing, lengthy operation is in progress. + OperationPending, + /// No request is pending. + NoOperations, + /// Authenticator does not support requested algorithm. + UnsupportedAlgorithm, + /// Not authorized for requested operation. + OperationDenied, + /// Internal key storage is full. + KeyStoreFull, + /// No outstanding operations. + NoOperationPending, + /// Unsupported option. + UnsupportedOption, + /// Not a valid option for current operation. + InvalidOption, + /// Pending keep alive was cancelled. + KeepaliveCancel, + /// No valid credentials provided. + NoCredentials, + /// Timeout waiting for user interaction. + UserActionTimeout, + /// Continuation command, such as, authenticatorGetNextAssertion not + /// allowed. + NotAllowed, + /// PIN Invalid. + PinInvalid, + /// PIN Blocked. + PinBlocked, + /// PIN authentication,pinAuth, verification failed. + PinAuthInvalid, + /// PIN authentication,pinAuth, blocked. Requires power recycle to reset. + PinAuthBlocked, + /// No PIN has been set. + PinNotSet, + /// PIN is required for the selected operation. + PinRequired, + /// PIN policy violation. Currently only enforces minimum length. + PinPolicyViolation, + /// pinToken expired on authenticator. + PinTokenExpired, + /// Authenticator cannot handle this request due to memory constraints. + RequestTooLarge, + /// The current operation has timed out. + ActionTimeout, + /// User presence is required for the requested operation. + UpRequired, + /// built-in user verification is disabled. + UvBlocked, + /// A checksum did not match. + IntegrityFailure, + /// The requested subcommand is either invalid or not implemented. + InvalidSubcommand, + /// built-in user verification unsuccessful. The platform SHOULD retry. + UvInvalid, + /// The permissions parameter contains an unauthorized permission. + UnauthorizedPermission, + /// Other unspecified error. + Other, + + /// Unknown status. + Unknown(u8), +} + +impl StatusCode { + fn is_ok(&self) -> bool { + matches!(*self, StatusCode::OK) + } + + fn device_busy(&self) -> bool { + matches!(*self, StatusCode::ChannelBusy) + } +} + +impl From for StatusCode { + fn from(value: u8) -> StatusCode { + match value { + 0x00 => StatusCode::OK, + 0x01 => StatusCode::InvalidCommand, + 0x02 => StatusCode::InvalidParameter, + 0x03 => StatusCode::InvalidLength, + 0x04 => StatusCode::InvalidSeq, + 0x05 => StatusCode::Timeout, + 0x06 => StatusCode::ChannelBusy, + 0x0A => StatusCode::LockRequired, + 0x0B => StatusCode::InvalidChannel, + 0x11 => StatusCode::CBORUnexpectedType, + 0x12 => StatusCode::InvalidCBOR, + 0x14 => StatusCode::MissingParameter, + 0x15 => StatusCode::LimitExceeded, + 0x16 => StatusCode::UnsupportedExtension, + 0x19 => StatusCode::CredentialExcluded, + 0x21 => StatusCode::Processing, + 0x22 => StatusCode::InvalidCredential, + 0x23 => StatusCode::UserActionPending, + 0x24 => StatusCode::OperationPending, + 0x25 => StatusCode::NoOperations, + 0x26 => StatusCode::UnsupportedAlgorithm, + 0x27 => StatusCode::OperationDenied, + 0x28 => StatusCode::KeyStoreFull, + 0x2A => StatusCode::NoOperationPending, + 0x2B => StatusCode::UnsupportedOption, + 0x2C => StatusCode::InvalidOption, + 0x2D => StatusCode::KeepaliveCancel, + 0x2E => StatusCode::NoCredentials, + 0x2f => StatusCode::UserActionTimeout, + 0x30 => StatusCode::NotAllowed, + 0x31 => StatusCode::PinInvalid, + 0x32 => StatusCode::PinBlocked, + 0x33 => StatusCode::PinAuthInvalid, + 0x34 => StatusCode::PinAuthBlocked, + 0x35 => StatusCode::PinNotSet, + 0x36 => StatusCode::PinRequired, + 0x37 => StatusCode::PinPolicyViolation, + 0x38 => StatusCode::PinTokenExpired, + 0x39 => StatusCode::RequestTooLarge, + 0x3A => StatusCode::ActionTimeout, + 0x3B => StatusCode::UpRequired, + 0x3C => StatusCode::UvBlocked, + 0x3D => StatusCode::IntegrityFailure, + 0x3E => StatusCode::InvalidSubcommand, + 0x3F => StatusCode::UvInvalid, + 0x40 => StatusCode::UnauthorizedPermission, + 0x7F => StatusCode::Other, + othr => StatusCode::Unknown(othr), + } + } +} + +#[cfg(test)] +impl From for u8 { + fn from(v: StatusCode) -> u8 { + match v { + StatusCode::OK => 0x00, + StatusCode::InvalidCommand => 0x01, + StatusCode::InvalidParameter => 0x02, + StatusCode::InvalidLength => 0x03, + StatusCode::InvalidSeq => 0x04, + StatusCode::Timeout => 0x05, + StatusCode::ChannelBusy => 0x06, + StatusCode::LockRequired => 0x0A, + StatusCode::InvalidChannel => 0x0B, + StatusCode::CBORUnexpectedType => 0x11, + StatusCode::InvalidCBOR => 0x12, + StatusCode::MissingParameter => 0x14, + StatusCode::LimitExceeded => 0x15, + StatusCode::UnsupportedExtension => 0x16, + StatusCode::CredentialExcluded => 0x19, + StatusCode::Processing => 0x21, + StatusCode::InvalidCredential => 0x22, + StatusCode::UserActionPending => 0x23, + StatusCode::OperationPending => 0x24, + StatusCode::NoOperations => 0x25, + StatusCode::UnsupportedAlgorithm => 0x26, + StatusCode::OperationDenied => 0x27, + StatusCode::KeyStoreFull => 0x28, + StatusCode::NoOperationPending => 0x2A, + StatusCode::UnsupportedOption => 0x2B, + StatusCode::InvalidOption => 0x2C, + StatusCode::KeepaliveCancel => 0x2D, + StatusCode::NoCredentials => 0x2E, + StatusCode::UserActionTimeout => 0x2f, + StatusCode::NotAllowed => 0x30, + StatusCode::PinInvalid => 0x31, + StatusCode::PinBlocked => 0x32, + StatusCode::PinAuthInvalid => 0x33, + StatusCode::PinAuthBlocked => 0x34, + StatusCode::PinNotSet => 0x35, + StatusCode::PinRequired => 0x36, + StatusCode::PinPolicyViolation => 0x37, + StatusCode::PinTokenExpired => 0x38, + StatusCode::RequestTooLarge => 0x39, + StatusCode::ActionTimeout => 0x3A, + StatusCode::UpRequired => 0x3B, + StatusCode::UvBlocked => 0x3C, + StatusCode::IntegrityFailure => 0x3D, + StatusCode::InvalidSubcommand => 0x3E, + StatusCode::UvInvalid => 0x3F, + StatusCode::UnauthorizedPermission => 0x40, + StatusCode::Other => 0x7F, + + StatusCode::Unknown(othr) => othr, + } + } +} + +#[derive(Debug)] +pub enum CommandError { + InputTooSmall, + MissingRequiredField(&'static str), + Deserializing(CborError), + Serializing(CborError), + StatusCode(StatusCode, Option), + Json(json::Error), + Crypto(CryptoError), + UnsupportedPinProtocol, +} + +impl fmt::Display for CommandError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + CommandError::InputTooSmall => write!(f, "CommandError: Input is too small"), + CommandError::MissingRequiredField(field) => { + write!(f, "CommandError: Missing required field {field}") + } + CommandError::Deserializing(ref e) => { + write!(f, "CommandError: Error while parsing: {e}") + } + CommandError::Serializing(ref e) => { + write!(f, "CommandError: Error while serializing: {e}") + } + CommandError::StatusCode(ref code, ref value) => { + 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") + } + } + } +} + +impl StdErrorT for CommandError {} diff --git a/third_party/rust/authenticator/src/ctap2/commands/reset.rs b/third_party/rust/authenticator/src/ctap2/commands/reset.rs new file mode 100644 index 0000000000..a1006800b5 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/reset.rs @@ -0,0 +1,123 @@ +use super::{Command, CommandError, RequestCtap2, StatusCode}; +use crate::transport::errors::HIDError; +use crate::transport::{FidoDevice, VirtualFidoDevice}; +use serde_cbor::{de::from_slice, Value}; + +#[derive(Debug, Default)] +pub struct Reset {} + +impl RequestCtap2 for Reset { + type Output = (); + + fn command(&self) -> Command { + Command::Reset + } + + fn wire_format(&self) -> Result, HIDError> { + Ok(Vec::new()) + } + + fn handle_response_ctap2( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result { + if input.is_empty() { + return Err(CommandError::InputTooSmall.into()); + } + + let status: StatusCode = input[0].into(); + + if status.is_ok() { + Ok(()) + } else { + let msg = if input.len() > 1 { + let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + Some(data) + } else { + None + }; + Err(CommandError::StatusCode(status, msg).into()) + } + } + + fn send_to_virtual_device( + &self, + dev: &mut Dev, + ) -> Result { + dev.reset(self) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::consts::HIDCmd; + use crate::transport::device_selector::Device; + use crate::transport::{hid::HIDDevice, FidoDevice, FidoDeviceIO, FidoProtocol}; + use rand::{thread_rng, RngCore}; + use serde_cbor::{de::from_slice, Value}; + + fn issue_command_and_get_response(cmd: u8, add: &[u8]) -> Result<(), HIDError> { + let mut device = Device::new("commands/Reset").unwrap(); + assert_eq!(device.get_protocol(), FidoProtocol::CTAP2); + // ctap2 request + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + device.set_cid(cid); + + let mut msg = cid.to_vec(); + msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x1]); // cmd + bcnt + msg.extend(vec![0x07]); // authenticatorReset + device.add_write(&msg, 0); + + // ctap2 response + let len = 0x1 + add.len() as u8; + let mut msg = cid.to_vec(); + msg.extend(vec![HIDCmd::Cbor.into(), 0x00, len]); // cmd + bcnt + msg.push(cmd); // Status code + msg.extend(add); // + maybe additional data + device.add_read(&msg, 0); + + device.send_cbor(&Reset {}) + } + + #[test] + fn test_select_ctap2_only() { + // Test, if we can parse the status codes specified by the spec + + // Ok() + issue_command_and_get_response(0, &[]).expect("Unexpected error"); + + // Denied by the user + let response = issue_command_and_get_response(0x27, &[]).expect_err("Not an error!"); + assert!(matches!( + response, + HIDError::Command(CommandError::StatusCode(StatusCode::OperationDenied, None)) + )); + + // Timeout + let response = issue_command_and_get_response(0x2F, &[]).expect_err("Not an error!"); + assert!(matches!( + response, + HIDError::Command(CommandError::StatusCode( + StatusCode::UserActionTimeout, + None + )) + )); + + // Unexpected error with more random CBOR-data + let add_data = vec![ + 0x63, // text(3) + 0x61, 0x6c, 0x67, // "alg" + ]; + let response = issue_command_and_get_response(0x02, &add_data).expect_err("Not an error!"); + match response { + HIDError::Command(CommandError::StatusCode(StatusCode::InvalidParameter, Some(d))) => { + let expected: Value = from_slice(&add_data).unwrap(); + assert_eq!(d, expected) + } + e => panic!("Not the expected response: {:?}", e), + } + } +} diff --git a/third_party/rust/authenticator/src/ctap2/commands/selection.rs b/third_party/rust/authenticator/src/ctap2/commands/selection.rs new file mode 100644 index 0000000000..4e9fc5213e --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/selection.rs @@ -0,0 +1,123 @@ +use super::{Command, CommandError, RequestCtap2, StatusCode}; +use crate::transport::errors::HIDError; +use crate::transport::{FidoDevice, VirtualFidoDevice}; +use serde_cbor::{de::from_slice, Value}; + +#[derive(Debug, Default)] +pub struct Selection {} + +impl RequestCtap2 for Selection { + type Output = (); + + fn command(&self) -> Command { + Command::Selection + } + + fn wire_format(&self) -> Result, HIDError> { + Ok(Vec::new()) + } + + fn handle_response_ctap2( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result { + if input.is_empty() { + return Err(CommandError::InputTooSmall.into()); + } + + let status: StatusCode = input[0].into(); + + if status.is_ok() { + Ok(()) + } else { + let msg = if input.len() > 1 { + let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + Some(data) + } else { + None + }; + Err(CommandError::StatusCode(status, msg).into()) + } + } + + fn send_to_virtual_device( + &self, + dev: &mut Dev, + ) -> Result { + dev.selection(self) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::consts::HIDCmd; + use crate::transport::device_selector::Device; + use crate::transport::{hid::HIDDevice, FidoDevice, FidoDeviceIO, FidoProtocol}; + use rand::{thread_rng, RngCore}; + use serde_cbor::{de::from_slice, Value}; + + fn issue_command_and_get_response(cmd: u8, add: &[u8]) -> Result<(), HIDError> { + let mut device = Device::new("commands/selection").unwrap(); + assert_eq!(device.get_protocol(), FidoProtocol::CTAP2); + // ctap2 request + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + device.set_cid(cid); + + let mut msg = cid.to_vec(); + msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x1]); // cmd + bcnt + msg.extend(vec![0x0B]); // authenticatorSelection + device.add_write(&msg, 0); + + // ctap2 response + let len = 0x1 + add.len() as u8; + let mut msg = cid.to_vec(); + msg.extend(vec![HIDCmd::Cbor.into(), 0x00, len]); // cmd + bcnt + msg.push(cmd); // Status code + msg.extend(add); // + maybe additional data + device.add_read(&msg, 0); + + device.send_cbor(&Selection {}) + } + + #[test] + fn test_select_ctap2_only() { + // Test, if we can parse the status codes specified by the spec + + // Ok() + issue_command_and_get_response(0, &[]).expect("Unexpected error"); + + // Denied by the user + let response = issue_command_and_get_response(0x27, &[]).expect_err("Not an error!"); + assert!(matches!( + response, + HIDError::Command(CommandError::StatusCode(StatusCode::OperationDenied, None)) + )); + + // Timeout + let response = issue_command_and_get_response(0x2F, &[]).expect_err("Not an error!"); + assert!(matches!( + response, + HIDError::Command(CommandError::StatusCode( + StatusCode::UserActionTimeout, + None + )) + )); + + // Unexpected error with more random CBOR-data + let add_data = vec![ + 0x63, // text(3) + 0x61, 0x6c, 0x67, // "alg" + ]; + let response = issue_command_and_get_response(0x02, &add_data).expect_err("Not an error!"); + match response { + HIDError::Command(CommandError::StatusCode(StatusCode::InvalidParameter, Some(d))) => { + let expected: Value = from_slice(&add_data).unwrap(); + assert_eq!(d, expected) + } + e => panic!("Not the expected response: {:?}", e), + } + } +} diff --git a/third_party/rust/authenticator/src/ctap2/mod.rs b/third_party/rust/authenticator/src/ctap2/mod.rs new file mode 100644 index 0000000000..bc45ceb9eb --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/mod.rs @@ -0,0 +1,1518 @@ +pub mod attestation; +pub mod client_data; +#[allow(dead_code)] // TODO(MS): Remove me asap +pub mod commands; +pub mod preflight; +pub mod server; +pub(crate) mod utils; + +use crate::authenticatorservice::{RegisterArgs, SignArgs}; +use crate::crypto::COSEAlgorithm; +use crate::ctap2::client_data::ClientDataHash; +use crate::ctap2::commands::authenticator_config::{ + AuthConfigCommand, AuthConfigResult, AuthenticatorConfig, +}; +use crate::ctap2::commands::bio_enrollment::{ + BioEnrollment, BioEnrollmentCommand, BioEnrollmentResult, FingerprintSensorInfo, +}; +use crate::ctap2::commands::client_pin::{ + ChangeExistingPin, Pin, PinError, PinUvAuthTokenPermission, SetNewPin, +}; +use crate::ctap2::commands::credential_management::{ + CredManagementCommand, CredentialList, CredentialListEntry, CredentialManagement, + CredentialManagementResult, CredentialRpListEntry, +}; +use crate::ctap2::commands::get_assertion::{GetAssertion, GetAssertionOptions}; +use crate::ctap2::commands::make_credentials::{ + dummy_make_credentials_cmd, MakeCredentials, MakeCredentialsOptions, +}; +use crate::ctap2::commands::reset::Reset; +use crate::ctap2::commands::{ + repackage_pin_errors, CommandError, PinUvAuthCommand, PinUvAuthResult, RequestCtap2, StatusCode, +}; +use crate::ctap2::preflight::{ + do_credential_list_filtering_ctap1, do_credential_list_filtering_ctap2, + silently_discover_credentials, +}; +use crate::ctap2::server::{ + CredentialProtectionPolicy, RelyingParty, ResidentKeyRequirement, UserVerificationRequirement, +}; +use crate::errors::{AuthenticatorError, UnsupportedOption}; +use crate::statecallback::StateCallback; +use crate::status_update::{send_status, BioEnrollmentCmd, CredManagementCmd, InteractiveUpdate}; +use crate::transport::device_selector::{Device, DeviceSelectorEvent}; +use crate::transport::{errors::HIDError, hid::HIDDevice, FidoDevice, FidoDeviceIO, FidoProtocol}; +use crate::{ManageResult, ResetResult, StatusPinUv, StatusUpdate}; +use std::sync::mpsc::{channel, RecvError, Sender}; +use std::thread; +use std::time::Duration; + +use self::commands::get_info::AuthenticatorVersion; + +macro_rules! unwrap_option { + ($item: expr, $callback: expr) => { + match $item { + Some(r) => r, + None => { + $callback.call(Err(AuthenticatorError::Platform)); + return false; + } + } + }; +} + +macro_rules! unwrap_result { + ($item: expr, $callback: expr) => { + match $item { + Ok(r) => r, + Err(e) => { + $callback.call(Err(e.into())); + return false; + } + } + }; +} + +macro_rules! handle_errors { + ($error: expr, $status: expr, $callback: expr, $pin_uv_auth_result: expr, $skip_uv: expr) => { + let mut _dummy_skip_puap = false; + let mut _dummy_cached_puat = false; + handle_errors!( + $error, + $status, + $callback, + $pin_uv_auth_result, + $skip_uv, + _dummy_skip_puap, + _dummy_cached_puat + ) + }; + ($error: expr, $status: expr, $callback: expr, $pin_uv_auth_result: expr, $skip_uv: expr, $skip_puap: expr) => { + let mut _dummy_cached_puat = false; + handle_errors!( + $error, + $status, + $callback, + $pin_uv_auth_result, + $skip_uv, + $skip_puap, + _dummy_cached_puat + ) + }; + ($error: expr, $status: expr, $callback: expr, $pin_uv_auth_result: expr, $skip_uv: expr, $skip_puap: expr, $cached_puat: expr) => { + match $error { + HIDError::Command(CommandError::StatusCode(StatusCode::ChannelBusy, _)) => { + // Channel busy. Client SHOULD retry the request after a short delay. + thread::sleep(Duration::from_millis(100)); + continue; + } + HIDError::Command(CommandError::StatusCode(StatusCode::OperationDenied, _)) + | HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthInvalid, _)) + if matches!($pin_uv_auth_result, PinUvAuthResult::UsingInternalUv) => + { + // This should only happen for CTAP2.0 tokens that use internal UV and failed + // (e.g. wrong fingerprint used), while doing GetAssertion or MakeCredentials. + send_status( + &$status, + StatusUpdate::PinUvError(StatusPinUv::InvalidUv(None)), + ); + $skip_puap = false; + continue; + } + HIDError::Command(CommandError::StatusCode(StatusCode::PinRequired, _)) + if matches!($pin_uv_auth_result, PinUvAuthResult::UsingInternalUv) => + { + // This should only happen for CTAP2.0 tokens that use internal UV and failed + // repeatedly, so that we have to fall back to PINs + $skip_uv = true; + $skip_puap = false; + continue; + } + HIDError::Command(CommandError::StatusCode(StatusCode::UvBlocked, _)) + if matches!( + $pin_uv_auth_result, + PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions(..) + ) => + { + // This should only happen for CTAP2.1 tokens that use internal UV and failed + // repeatedly, so that we have to fall back to PINs + $skip_uv = true; + $skip_puap = false; + continue; + } + HIDError::Command(CommandError::StatusCode(StatusCode::CredentialExcluded, _)) => { + $callback.call(Err(AuthenticatorError::CredentialExcluded)); + break; + } + HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthInvalid, _)) + if $cached_puat => + { + // We used the cached PUAT, but it was invalid. So we just try again + // without the cached one and potentially trigger a new PIN/UV entry from + // the user. This happens e.g. if the PUAT expires, or we get an all-zeros + // PUAT from outside for whatever reason, etc. + $cached_puat = false; + $skip_puap = false; + continue; + } + e => { + warn!("error happened: {e}"); + $callback.call(Err(AuthenticatorError::HIDError(e))); + break; + } + } + }; +} + +fn ask_user_for_pin( + was_invalid: bool, + retries: Option, + status: &Sender, +) -> Result { + info!("PIN Error that requires user interaction detected. Sending it back and waiting for a reply"); + let (tx, rx) = channel(); + if was_invalid { + send_status( + status, + crate::StatusUpdate::PinUvError(StatusPinUv::InvalidPin(tx, retries)), + ); + } else { + send_status( + status, + crate::StatusUpdate::PinUvError(StatusPinUv::PinRequired(tx)), + ); + } + match rx.recv() { + Ok(pin) => Ok(pin), + Err(RecvError) => { + // recv() can only fail, if the other side is dropping the Sender. + info!("Callback dropped the channel. Aborting."); + Err(AuthenticatorError::CancelledByUser) + } + } +} + +/// Try to fetch PinUvAuthToken from the device and derive from it PinUvAuthParam. +/// Prefer UV, fallback to PIN. +/// Prefer newer pinUvAuth-methods, if supported by the device. +fn get_pin_uv_auth_param( + cmd: &mut T, + dev: &mut Dev, + permission: PinUvAuthTokenPermission, + skip_uv: bool, + uv_req: UserVerificationRequirement, + alive: &dyn Fn() -> bool, + pin: &Option, +) -> Result { + // CTAP 2.1 is very specific that the request should either include pinUvAuthParam + // OR uv=true, but not both at the same time. We now have to decide which (if either) + // to send. We may omit both values. Will never send an explicit uv=false, because + // a) this is the default, and + // b) some CTAP 2.0 authenticators return UnsupportedOption when uv=false. + + // We ensure both pinUvAuthParam and uv are not set to start. + cmd.set_pin_uv_auth_param(None)?; + cmd.set_uv_option(None); + + // Skip user verification if we're using CTAP1 or if the device does not support CTAP2. + let info = match (dev.get_protocol(), dev.get_authenticator_info()) { + (FidoProtocol::CTAP2, Some(info)) => info, + _ => return Ok(PinUvAuthResult::DeviceIsCtap1), + }; + + // Only use UV, if the device supports it and we don't skip it + // which happens as a fallback, if UV-usage failed too many times + // Note: In theory, we could also repeatedly query GetInfo here and check + // if uv is set to Some(true), as tokens should set it to Some(false) + // if UV is blocked (too many failed attempts). But the CTAP2.0-spec is + // vague and I don't trust all tokens to implement it that way. So we + // keep track of it ourselves, using `skip_uv`. + let supports_uv = info.options.user_verification == Some(true); + let supports_pin = info.options.client_pin.is_some(); + let pin_configured = info.options.client_pin == Some(true); + + // Check if the combination of device-protection and request-options + // are allowing for 'discouraged', meaning no auth required. + if cmd.can_skip_user_verification(info, uv_req) { + return Ok(PinUvAuthResult::NoAuthRequired); + } + + // Device does not support any (remaining) auth-method + if (skip_uv || !supports_uv) && !supports_pin { + if supports_uv && uv_req == UserVerificationRequirement::Required { + // We should always set the uv option in the Required case, but the CTAP 2.1 spec + // says 'Platforms MUST NOT include the "uv" option key if the authenticator does + // not support built-in user verification.' This is to work around some CTAP 2.0 + // authenticators which incorrectly error out with CTAP2_ERR_UNSUPPORTED_OPTION + // when the "uv" option is set. The RP that requested UV will (hopefully) reject our + // response in the !supports_uv case. + cmd.set_uv_option(Some(true)); + } + return Ok(PinUvAuthResult::NoAuthTypeSupported); + } + + // Device supports PINs, but a PIN is not configured. Signal that we + // can complete the operation if the user sets a PIN first. + if (skip_uv || !supports_uv) && !pin_configured { + return Err(AuthenticatorError::PinError(PinError::PinNotSet)); + } + + if info.options.pin_uv_auth_token == Some(true) { + if !skip_uv && supports_uv { + // CTAP 2.1 - UV + let pin_auth_token = dev + .get_pin_uv_auth_token_using_uv_with_permissions(permission, cmd.get_rp_id(), alive) + .map_err(|e| repackage_pin_errors(dev, e))?; + cmd.set_pin_uv_auth_param(Some(pin_auth_token.clone()))?; + Ok(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions(pin_auth_token)) + } else { + // CTAP 2.1 - PIN + // We did not take the `!skip_uv && supports_uv` branch, so we have + // `(skip_uv || !supports_uv)`. Moreover we did not exit early in the + // `(skip_uv || !supports_uv) && !pin_configured` case. So we have + // `pin_configured`. + let pin_auth_token = dev + .get_pin_uv_auth_token_using_pin_with_permissions( + pin, + permission, + cmd.get_rp_id(), + alive, + ) + .map_err(|e| repackage_pin_errors(dev, e))?; + cmd.set_pin_uv_auth_param(Some(pin_auth_token.clone()))?; + Ok(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingPinWithPermissions(pin_auth_token)) + } + } else { + // CTAP 2.0 fallback + if !skip_uv && supports_uv && pin.is_none() { + // If the device supports internal user-verification (e.g. fingerprints), + // skip PIN-stuff + + // We may need the shared secret for HMAC-extension, so we + // have to establish one + if info.supports_hmac_secret() { + let _shared_secret = dev.establish_shared_secret(alive)?; + } + // CTAP 2.1, Section 6.1.1, Step 1.1.2.1.2. + cmd.set_uv_option(Some(true)); + return Ok(PinUvAuthResult::UsingInternalUv); + } + + let pin_auth_token = dev + .get_pin_token(pin, alive) + .map_err(|e| repackage_pin_errors(dev, e))?; + cmd.set_pin_uv_auth_param(Some(pin_auth_token.clone()))?; + Ok(PinUvAuthResult::SuccessGetPinToken(pin_auth_token)) + } +} + +/// PUAP, as per spec: PinUvAuthParam +/// Determines, if we need to establish a PinUvAuthParam, based on the +/// capabilities of the device and the incoming request. +/// If it is needed, tries to establish one and save it inside the Request. +/// Returns Ok() if we can proceed with sending the actual Request to +/// the device, Err() otherwise. +/// Handles asking the user for a PIN, if needed and sending StatusUpdates +/// regarding PIN and UV usage. +#[allow(clippy::too_many_arguments)] +fn determine_puap_if_needed( + cmd: &mut T, + dev: &mut Dev, + mut skip_uv: bool, + permission: PinUvAuthTokenPermission, + uv_req: UserVerificationRequirement, + status: &Sender, + alive: &dyn Fn() -> bool, + pin: &mut Option, +) -> Result { + while alive() { + debug!("-----------------------------------------------------------------"); + debug!("Getting pinUvAuthParam"); + match get_pin_uv_auth_param(cmd, dev, permission, skip_uv, uv_req, alive, pin) { + Ok(r) => { + return Ok(r); + } + + Err(AuthenticatorError::PinError(PinError::PinRequired)) => { + let new_pin = ask_user_for_pin(false, None, status)?; + *pin = Some(new_pin); + skip_uv = true; + continue; + } + Err(AuthenticatorError::PinError(PinError::InvalidPin(retries))) => { + let new_pin = ask_user_for_pin(true, retries, status)?; + *pin = Some(new_pin); + continue; + } + Err(AuthenticatorError::PinError(PinError::InvalidUv(retries))) => { + if retries == Some(0) { + skip_uv = true; + } + send_status( + status, + StatusUpdate::PinUvError(StatusPinUv::InvalidUv(retries)), + ) + } + Err(e @ AuthenticatorError::PinError(PinError::PinAuthBlocked)) => { + send_status( + status, + StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked), + ); + error!("Error when determining pinAuth: {:?}", e); + return Err(e); + } + Err(e @ AuthenticatorError::PinError(PinError::PinBlocked)) => { + send_status(status, StatusUpdate::PinUvError(StatusPinUv::PinBlocked)); + error!("Error when determining pinAuth: {:?}", e); + return Err(e); + } + Err(e @ AuthenticatorError::PinError(PinError::PinNotSet)) => { + send_status(status, StatusUpdate::PinUvError(StatusPinUv::PinNotSet)); + error!("Error when determining pinAuth: {:?}", e); + return Err(e); + } + Err(AuthenticatorError::PinError(PinError::UvBlocked)) => { + skip_uv = true; + send_status(status, StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) + } + // Used for CTAP2.0 UV (fingerprints) + Err(AuthenticatorError::PinError(PinError::PinAuthInvalid)) => { + skip_uv = true; + send_status( + status, + StatusUpdate::PinUvError(StatusPinUv::InvalidUv(None)), + ) + } + Err(e) => { + error!("Error when determining pinAuth: {:?}", e); + return Err(e); + } + } + } + Err(AuthenticatorError::CancelledByUser) +} + +pub fn register( + dev: &mut Dev, + args: RegisterArgs, + status: Sender, + callback: StateCallback>, + alive: &dyn Fn() -> bool, +) -> bool { + let mut options = MakeCredentialsOptions::default(); + + if dev.get_protocol() == FidoProtocol::CTAP2 { + let info = match dev.get_authenticator_info() { + Some(info) => info, + None => { + callback.call(Err(HIDError::DeviceNotInitialized.into())); + return false; + } + }; + + // Set options based on the arguments and the device info. + // The user verification option will be set in `determine_puap_if_needed`. + options.resident_key = match args.resident_key_req { + ResidentKeyRequirement::Required => Some(true), + ResidentKeyRequirement::Preferred => { + // Use a resident key if the authenticator supports it + Some(info.options.resident_key) + } + ResidentKeyRequirement::Discouraged => Some(false), + } + } else { + // Check that the request can be processed by a CTAP1 device. + // See CTAP 2.1 Section 10.2. Some additional checks are performed in + // MakeCredentials::RequestCtap1 + if args.resident_key_req == ResidentKeyRequirement::Required { + callback.call(Err(AuthenticatorError::UnsupportedOption( + UnsupportedOption::ResidentKey, + ))); + return false; + } + if args.user_verification_req == UserVerificationRequirement::Required { + callback.call(Err(AuthenticatorError::UnsupportedOption( + UnsupportedOption::UserVerification, + ))); + return false; + } + if !args + .pub_cred_params + .iter() + .any(|x| x.alg == COSEAlgorithm::ES256) + { + callback.call(Err(AuthenticatorError::UnsupportedOption( + UnsupportedOption::PubCredParams, + ))); + return false; + } + } + + // Client extension processing for credProtect: + // "When enforceCredentialProtectionPolicy is true, and credentialProtectionPolicy's value is + // [not "Optional"], the platform SHOULD NOT create the credential in a way that does not + // implement the requested protection policy. (For example, by creating it on an authenticator + // that does not support this extension.)" + let dev_supports_cred_protect = dev + .get_authenticator_info() + .map_or(false, |info| info.supports_cred_protect()); + if args.extensions.enforce_credential_protection_policy == Some(true) + && args.extensions.credential_protection_policy + != Some(CredentialProtectionPolicy::UserVerificationOptional) + && !dev_supports_cred_protect + { + callback.call(Err(AuthenticatorError::UnsupportedOption( + UnsupportedOption::CredProtect, + ))); + return false; + } + + let mut makecred = MakeCredentials::new( + ClientDataHash(args.client_data_hash), + args.relying_party, + Some(args.user), + args.pub_cred_params, + args.exclude_list, + options, + args.extensions.into(), + ); + + let mut skip_uv = false; + let mut pin = args.pin; + while alive() { + // Requesting both because pre-flighting (credential list filtering) + // can potentially send GetAssertion-commands + let permissions = + PinUvAuthTokenPermission::MakeCredential | PinUvAuthTokenPermission::GetAssertion; + + let pin_uv_auth_result = unwrap_result!( + determine_puap_if_needed( + &mut makecred, + dev, + skip_uv, + permissions, + args.user_verification_req, + &status, + alive, + &mut pin, + ), + callback + ); + // Do "pre-flight": Filter the exclude-list + if dev.get_protocol() == FidoProtocol::CTAP2 { + makecred.exclude_list = unwrap_result!( + do_credential_list_filtering_ctap2( + dev, + &makecred.exclude_list, + &makecred.rp, + pin_uv_auth_result.get_pin_uv_auth_token(), + ), + callback + ); + } else { + let key_handle = do_credential_list_filtering_ctap1( + dev, + &makecred.exclude_list, + &makecred.rp, + &makecred.client_data_hash, + ); + // That handle was already registered with the token + if key_handle.is_some() { + // Now we need to send a dummy registration request, to make the token blink + // Spec says "dummy appid and invalid challenge". We use the same, as we do for + // making the token blink upon device selection. + send_status(&status, crate::StatusUpdate::PresenceRequired); + let msg = dummy_make_credentials_cmd(); + let _ = dev.send_msg_cancellable(&msg, alive); // Ignore answer, return "CredentialExcluded" + callback.call(Err(AuthenticatorError::CredentialExcluded)); + return false; + } + } + + debug!("------------------------------------------------------------------"); + debug!("{makecred:?} using {pin_uv_auth_result:?}"); + debug!("------------------------------------------------------------------"); + send_status(&status, crate::StatusUpdate::PresenceRequired); + let resp = dev.send_msg_cancellable(&makecred, alive); + match resp { + Ok(result) => { + callback.call(Ok(result)); + return true; + } + Err(e) => { + handle_errors!(e, status, callback, pin_uv_auth_result, skip_uv); + } + } + } + false +} + +pub fn sign( + dev: &mut Dev, + args: SignArgs, + status: Sender, + callback: StateCallback>, + alive: &dyn Fn() -> bool, +) -> bool { + if dev.get_protocol() == FidoProtocol::CTAP1 { + // Check that the request can be processed by a CTAP1 device. + // See CTAP 2.1 Section 10.3. Some additional checks are performed in + // GetAssertion::RequestCtap1 + if args.user_verification_req == UserVerificationRequirement::Required { + callback.call(Err(AuthenticatorError::UnsupportedOption( + UnsupportedOption::UserVerification, + ))); + return false; + } + if args.allow_list.is_empty() { + callback.call(Err(AuthenticatorError::UnsupportedOption( + UnsupportedOption::EmptyAllowList, + ))); + return false; + } + } + + let mut allow_list = args.allow_list; + let mut rp_id = RelyingParty::from(args.relying_party_id); + let client_data_hash = ClientDataHash(args.client_data_hash); + if let Some(ref app_id) = args.extensions.app_id { + if !allow_list.is_empty() { + // Try to silently discover U2F credentials that require the FIDO App ID extension. If + // any are found, we should use the alternate RP ID instead of the provided RP ID. + let alt_rp_id = RelyingParty::from(app_id); + let silent_creds = + silently_discover_credentials(dev, &allow_list, &alt_rp_id, &client_data_hash); + if !silent_creds.is_empty() { + allow_list = silent_creds; + rp_id = alt_rp_id; + } + } + } + + let mut get_assertion = GetAssertion::new( + client_data_hash, + rp_id, + allow_list, + GetAssertionOptions { + user_presence: Some(args.user_presence_req), + user_verification: None, + }, + args.extensions.into(), + ); + + let mut skip_uv = false; + let mut pin = args.pin; + while alive() { + let pin_uv_auth_result = unwrap_result!( + determine_puap_if_needed( + &mut get_assertion, + dev, + skip_uv, + PinUvAuthTokenPermission::GetAssertion, + args.user_verification_req, + &status, + alive, + &mut pin, + ), + callback + ); + // Third, use the shared secret in the extensions, if requested + if let Some(extension) = get_assertion.extensions.hmac_secret.as_mut() { + if let Some(secret) = dev.get_shared_secret() { + match extension.calculate(secret) { + Ok(x) => x, + Err(e) => { + callback.call(Err(e)); + return false; + } + } + } + } + + // Do "pre-flight": Filter the allow-list + let original_allow_list_was_empty = get_assertion.allow_list.is_empty(); + if dev.get_protocol() == FidoProtocol::CTAP2 { + get_assertion.allow_list = unwrap_result!( + do_credential_list_filtering_ctap2( + dev, + &get_assertion.allow_list, + &get_assertion.rp, + pin_uv_auth_result.get_pin_uv_auth_token(), + ), + callback + ); + } else { + let key_handle = do_credential_list_filtering_ctap1( + dev, + &get_assertion.allow_list, + &get_assertion.rp, + &get_assertion.client_data_hash, + ); + match key_handle { + Some(key_handle) => { + get_assertion.allow_list = vec![key_handle]; + } + None => { + get_assertion.allow_list.clear(); + } + } + } + + // If the incoming list was not empty, but the filtered list is, we have to error out + if !original_allow_list_was_empty && get_assertion.allow_list.is_empty() { + // We have to collect a user interaction + send_status(&status, crate::StatusUpdate::PresenceRequired); + let msg = dummy_make_credentials_cmd(); + let _ = dev.send_msg_cancellable(&msg, alive); // Ignore answer, return "NoCredentials" + callback.call(Err(HIDError::Command(CommandError::StatusCode( + StatusCode::NoCredentials, + None, + )) + .into())); + return false; + } + + debug!("------------------------------------------------------------------"); + debug!("{get_assertion:?} using {pin_uv_auth_result:?}"); + debug!("------------------------------------------------------------------"); + send_status(&status, crate::StatusUpdate::PresenceRequired); + let mut results = match dev.send_msg_cancellable(&get_assertion, alive) { + Ok(results) => results, + Err(e) => { + handle_errors!(e, status, callback, pin_uv_auth_result, skip_uv); + } + }; + if results.len() == 1 { + callback.call(Ok(results.swap_remove(0))); + return true; + } + let (tx, rx) = channel(); + let user_entities = results + .iter() + .filter_map(|x| x.assertion.user.clone()) + .collect(); + send_status( + &status, + crate::StatusUpdate::SelectResultNotice(tx, user_entities), + ); + match rx.recv() { + Ok(Some(index)) if index < results.len() => { + callback.call(Ok(results.swap_remove(index))); + return true; + } + _ => { + callback.call(Err(AuthenticatorError::CancelledByUser)); + return true; + } + } + } + false +} + +pub(crate) fn reset_helper>( + dev: &mut Device, + selector: Sender, + status: Sender, + callback: StateCallback>, + keep_alive: &dyn Fn() -> bool, +) { + let reset = Reset {}; + info!("Device {:?} continues with the reset process", dev.id()); + + debug!("------------------------------------------------------------------"); + debug!("{:?}", reset); + debug!("------------------------------------------------------------------"); + send_status(&status, crate::StatusUpdate::PresenceRequired); + let resp = dev.send_cbor_cancellable(&reset, keep_alive); + if resp.is_ok() { + // The DeviceSelector could already be dead, but it might also wait + // for us to respond, in order to cancel all other tokens in case + // we skipped the "blinking"-action and went straight for the actual + // request. + let _ = selector.send(DeviceSelectorEvent::SelectedToken(dev.id())); + } + + match resp { + Ok(()) => callback.call(Ok(T::from(()))), + Err(HIDError::DeviceNotSupported) | Err(HIDError::UnsupportedCommand) => {} + Err(HIDError::Command(CommandError::StatusCode(StatusCode::ChannelBusy, _))) => {} + Err(e) => { + warn!("error happened: {}", e); + callback.call(Err(AuthenticatorError::HIDError(e))); + } + } +} + +pub(crate) fn set_or_change_pin_helper>( + dev: &mut Device, + mut current_pin: Option, + new_pin: Pin, + status: Sender, + callback: StateCallback>, + alive: &dyn Fn() -> bool, +) { + let mut shared_secret = match dev.establish_shared_secret(alive) { + Ok(s) => s, + Err(e) => { + callback.call(Err(AuthenticatorError::HIDError(e))); + return; + } + }; + + let authinfo = match dev.get_authenticator_info() { + Some(i) => i.clone(), + None => { + callback.call(Err(HIDError::DeviceNotInitialized.into())); + return; + } + }; + + // If the device has a min PIN use that, otherwise default to 4 according to Spec + if new_pin.as_bytes().len() < authinfo.min_pin_length.unwrap_or(4) as usize { + callback.call(Err(AuthenticatorError::PinError(PinError::PinIsTooShort))); + return; + } + + // As per Spec: "Maximum PIN Length: UTF-8 representation MUST NOT exceed 63 bytes" + if new_pin.as_bytes().len() >= 64 { + callback.call(Err(AuthenticatorError::PinError(PinError::PinIsTooLong( + new_pin.as_bytes().len(), + )))); + return; + } + + // Check if a client-pin is already set, or if a new one should be created + let res = if Some(true) == authinfo.options.client_pin { + let mut res; + let mut was_invalid = false; + let mut retries = None; + loop { + // current_pin will only be Some() in the interactive mode (running `manage()`) + // In case that PIN is wrong, we want to avoid an endless-loop here with re-trying + // that wrong PIN all the time. So we `take()` it, and only test it once. + // If that PIN is wrong, we fall back to the "ask_user_for_pin"-method. + let curr_pin = match current_pin.take() { + None => match ask_user_for_pin(was_invalid, retries, &status) { + Ok(pin) => pin, + Err(e) => { + callback.call(Err(e)); + return; + } + }, + Some(pin) => pin, + }; + + res = ChangeExistingPin::new(&authinfo, &shared_secret, &curr_pin, &new_pin) + .map_err(HIDError::Command) + .and_then(|msg| dev.send_cbor_cancellable(&msg, alive)) + .map_err(|e| repackage_pin_errors(dev, e)); + + if let Err(AuthenticatorError::PinError(PinError::InvalidPin(r))) = res { + was_invalid = true; + retries = r; + // We need to re-establish the shared secret for the next round. + match dev.establish_shared_secret(alive) { + Ok(s) => { + shared_secret = s; + } + Err(e) => { + callback.call(Err(AuthenticatorError::HIDError(e))); + return; + } + }; + + continue; + } else { + break; + } + } + res + } else { + dev.send_cbor_cancellable(&SetNewPin::new(&shared_secret, &new_pin), alive) + .map_err(AuthenticatorError::HIDError) + }; + // the callback is expecting `Result<(), AuthenticatorError>`, but `ChangeExistingPin` + // and `SetNewPin` return the default `ClientPinResponse` on success. Just discard it. + callback.call(res.map(|_| T::from(()))); +} + +pub(crate) fn bio_enrollment( + dev: &mut Device, + puat_result: Option, + command: BioEnrollmentCmd, + status: Sender, + callback: StateCallback>, + alive: &dyn Fn() -> bool, +) -> bool { + let authinfo = match dev.get_authenticator_info() { + Some(i) => i, + None => { + callback.call(Err(HIDError::DeviceNotInitialized.into())); + return false; + } + }; + + if authinfo.options.bio_enroll.is_none() + && authinfo.options.user_verification_mgmt_preview.is_none() + { + callback.call(Err(AuthenticatorError::HIDError( + HIDError::UnsupportedCommand, + ))); + return false; + } + + let use_legacy_preview = authinfo.options.bio_enroll.is_none(); + + // We are not allowed to request the BE-permission using UV, so we have to skip UV + let mut skip_uv = authinfo.options.uv_bio_enroll != Some(true); + // Currently not used, but if we want, we can just set the value here. + let timeout = None; + + let mut bio_cmd = match &command { + BioEnrollmentCmd::StartNewEnrollment(_name) => BioEnrollment::new( + BioEnrollmentCommand::EnrollBegin(timeout), + use_legacy_preview, + ), + BioEnrollmentCmd::DeleteEnrollment(id) => BioEnrollment::new( + BioEnrollmentCommand::RemoveEnrollment(id.clone()), + use_legacy_preview, + ), + BioEnrollmentCmd::ChangeName(id, name) => BioEnrollment::new( + BioEnrollmentCommand::SetFriendlyName((id.clone(), name.clone())), + use_legacy_preview, + ), + BioEnrollmentCmd::GetEnrollments => BioEnrollment::new( + BioEnrollmentCommand::EnumerateEnrollments, + use_legacy_preview, + ), + BioEnrollmentCmd::GetFingerprintSensorInfo => BioEnrollment::new( + BioEnrollmentCommand::GetFingerprintSensorInfo, + use_legacy_preview, + ), + }; + + let mut skip_puap = false; + let mut cached_puat = false; // If we were provided with a cached puat from the outside + let mut pin_uv_auth_result = puat_result + .clone() + .unwrap_or(PinUvAuthResult::NoAuthRequired); + // See, if we have a cached PUAT with matching permissions. + match puat_result { + Some(PinUvAuthResult::SuccessGetPinToken(t)) + | Some(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions(t)) + | Some(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingPinWithPermissions(t)) + if !authinfo.versions.contains(&AuthenticatorVersion::FIDO_2_1) // Only 2.1 has a permission-system + || use_legacy_preview // Preview doesn't use permissions + || t.permissions + .contains(PinUvAuthTokenPermission::BioEnrollment) => + { + skip_puap = true; + cached_puat = true; + unwrap_result!(bio_cmd.set_pin_uv_auth_param(Some(t)), callback); + } + _ => {} + } + let mut pin = None; + while alive() { + if !skip_puap { + pin_uv_auth_result = unwrap_result!( + determine_puap_if_needed( + &mut bio_cmd, + dev, + skip_uv, + PinUvAuthTokenPermission::BioEnrollment, + UserVerificationRequirement::Preferred, + &status, + alive, + &mut pin, + ), + callback + ); + } + + debug!("------------------------------------------------------------------"); + debug!("{bio_cmd:?} using {pin_uv_auth_result:?}"); + debug!("------------------------------------------------------------------"); + + let resp = dev.send_cbor_cancellable(&bio_cmd, alive); + match resp { + Ok(result) => { + skip_puap = true; + match bio_cmd.subcommand { + BioEnrollmentCommand::EnrollBegin(..) + | BioEnrollmentCommand::EnrollCaptureNextSample(..) => { + let template_id = + if let BioEnrollmentCommand::EnrollCaptureNextSample((id, ..)) = + bio_cmd.subcommand + { + id + } else { + unwrap_option!(result.template_id, callback) + }; + let last_enroll_sample_status = + unwrap_option!(result.last_enroll_sample_status, callback); + let remaining_samples = unwrap_option!(result.remaining_samples, callback); + + send_status( + &status, + StatusUpdate::InteractiveManagement( + InteractiveUpdate::BioEnrollmentUpdate(( + BioEnrollmentResult::SampleStatus( + last_enroll_sample_status, + remaining_samples, + ), + Some(pin_uv_auth_result.clone()), + )), + ), + ); + + if remaining_samples == 0 { + if let BioEnrollmentCmd::StartNewEnrollment(Some(ref name)) = command { + bio_cmd.subcommand = BioEnrollmentCommand::SetFriendlyName(( + template_id.to_vec(), + name.clone(), + )); + // We have to regenerate PUAP here. PUAT hasn't changed, but the content + // of the command has changed, and that is part of the PUAP-calculation + unwrap_result!( + bio_cmd.set_pin_uv_auth_param( + pin_uv_auth_result.get_pin_uv_auth_token() + ), + callback + ); + continue; + } else { + let auth_info = + unwrap_option!(dev.refresh_authenticator_info(), callback); + send_status( + &status, + StatusUpdate::InteractiveManagement( + InteractiveUpdate::BioEnrollmentUpdate(( + BioEnrollmentResult::AddSuccess(auth_info.clone()), + Some(pin_uv_auth_result), + )), + ), + ); + return true; + } + } else { + bio_cmd.subcommand = BioEnrollmentCommand::EnrollCaptureNextSample(( + template_id, + timeout, + )); + // We have to regenerate PUAP here. PUAT hasn't changed, but the content + // of the command has changed, and that is part of the PUAP-calculation + unwrap_result!( + bio_cmd.set_pin_uv_auth_param( + pin_uv_auth_result.get_pin_uv_auth_token() + ), + callback + ); + continue; + } + } + BioEnrollmentCommand::EnumerateEnrollments => { + let list = result.template_infos.iter().map(|x| x.into()).collect(); + send_status( + &status, + StatusUpdate::InteractiveManagement( + InteractiveUpdate::BioEnrollmentUpdate(( + BioEnrollmentResult::EnrollmentList(list), + Some(pin_uv_auth_result), + )), + ), + ); + return true; + } + BioEnrollmentCommand::SetFriendlyName(_) => { + let res = match command { + BioEnrollmentCmd::StartNewEnrollment(..) => { + let auth_info = + unwrap_option!(dev.refresh_authenticator_info(), callback); + BioEnrollmentResult::AddSuccess(auth_info.clone()) + } + _ => BioEnrollmentResult::UpdateSuccess, + }; + send_status( + &status, + StatusUpdate::InteractiveManagement( + InteractiveUpdate::BioEnrollmentUpdate(( + res, + Some(pin_uv_auth_result), + )), + ), + ); + return true; + } + BioEnrollmentCommand::RemoveEnrollment(_) => { + let auth_info = unwrap_option!(dev.refresh_authenticator_info(), callback); + send_status( + &status, + StatusUpdate::InteractiveManagement( + InteractiveUpdate::BioEnrollmentUpdate(( + BioEnrollmentResult::DeleteSuccess(auth_info.clone()), + Some(pin_uv_auth_result), + )), + ), + ); + return true; + } + BioEnrollmentCommand::CancelCurrentEnrollment => { + callback.call(Ok(ManageResult::Success)); + return true; + } + BioEnrollmentCommand::GetFingerprintSensorInfo => { + let fingerprint_kind = unwrap_option!(result.fingerprint_kind, callback); + let max_capture_samples_required_for_enroll = unwrap_option!( + result.max_capture_samples_required_for_enroll, + callback + ); + // FIDO_2_1_PRE-devices do not report this field. So we leave it optional. + let max_template_friendly_name = result.max_template_friendly_name; + send_status( + &status, + StatusUpdate::InteractiveManagement( + InteractiveUpdate::BioEnrollmentUpdate(( + BioEnrollmentResult::FingerprintSensorInfo( + FingerprintSensorInfo { + fingerprint_kind, + max_capture_samples_required_for_enroll, + max_template_friendly_name, + }, + ), + Some(pin_uv_auth_result), + )), + ), + ); + return true; + } + }; + } + Err(e) => { + handle_errors!( + e, + status, + callback, + pin_uv_auth_result, + skip_uv, + skip_puap, + cached_puat + ); + } + } + } + false +} + +pub(crate) fn credential_management( + dev: &mut Device, + puat_result: Option, + command: CredManagementCmd, + status: Sender, + callback: StateCallback>, + alive: &dyn Fn() -> bool, +) -> bool { + let mut skip_uv = false; + let authinfo = match dev.get_authenticator_info() { + Some(i) => i.clone(), + None => { + callback.call(Err(HIDError::DeviceNotInitialized.into())); + return false; + } + }; + + if authinfo.options.cred_mgmt != Some(true) + && authinfo.options.credential_mgmt_preview != Some(true) + { + callback.call(Err(AuthenticatorError::HIDError( + HIDError::UnsupportedCommand, + ))); + return false; + } + + let use_legacy_preview = authinfo.options.cred_mgmt != Some(true); + + // FIDO_2_1_PRE-devices do not support UpdateUserInformation. + if use_legacy_preview + && !authinfo.versions.contains(&AuthenticatorVersion::FIDO_2_1) + && matches!(command, CredManagementCmd::UpdateUserInformation(..)) + { + callback.call(Err(AuthenticatorError::HIDError( + HIDError::UnsupportedCommand, + ))); + return false; + } + + // If puap is provided, we can skip puap-determination (i.e. PIN entry) + let mut cred_management = match command { + CredManagementCmd::GetCredentials => { + CredentialManagement::new(CredManagementCommand::GetCredsMetadata, use_legacy_preview) + } + CredManagementCmd::DeleteCredential(cred_id) => CredentialManagement::new( + CredManagementCommand::DeleteCredential(cred_id), + use_legacy_preview, + ), + CredManagementCmd::UpdateUserInformation(cred_id, user) => CredentialManagement::new( + CredManagementCommand::UpdateUserInformation((cred_id, user)), + use_legacy_preview, + ), + }; + let mut credential_result = CredentialList::new(); + let mut remaining_rps = 0; + let mut remaining_cred_ids = 0; + let mut current_rp = 0; + let mut skip_puap = false; + let mut cached_puat = false; // If we were provided with a cached puat from the outside + let mut pin_uv_auth_result = puat_result + .clone() + .unwrap_or(PinUvAuthResult::NoAuthRequired); + // See, if we have a cached PUAT with matching permissions. + match puat_result { + Some(PinUvAuthResult::SuccessGetPinToken(t)) + | Some(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions(t)) + | Some(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingPinWithPermissions(t)) + if !authinfo.versions.contains(&AuthenticatorVersion::FIDO_2_1) // Only 2.1 has a permission-system + || use_legacy_preview // Preview doesn't use permissions + || t.permissions + .contains(PinUvAuthTokenPermission::CredentialManagement) => + { + skip_puap = true; + cached_puat = true; + unwrap_result!(cred_management.set_pin_uv_auth_param(Some(t)), callback); + } + _ => {} + } + let mut pin = None; + while alive() { + if !skip_puap { + pin_uv_auth_result = unwrap_result!( + determine_puap_if_needed( + &mut cred_management, + dev, + skip_uv, + PinUvAuthTokenPermission::CredentialManagement, + UserVerificationRequirement::Preferred, + &status, + alive, + &mut pin, + ), + callback + ); + } + + debug!("------------------------------------------------------------------"); + debug!("{cred_management:?} using {pin_uv_auth_result:?}"); + debug!("------------------------------------------------------------------"); + + let resp = dev.send_cbor_cancellable(&cred_management, alive); + match resp { + Ok(result) => { + skip_puap = true; + match cred_management.subcommand { + CredManagementCommand::GetCredsMetadata => { + let existing_resident_credentials_count = + unwrap_option!(result.existing_resident_credentials_count, callback); + let max_possible_remaining_resident_credentials_count = unwrap_option!( + result.max_possible_remaining_resident_credentials_count, + callback + ); + credential_result.existing_resident_credentials_count = + existing_resident_credentials_count; + credential_result.max_possible_remaining_resident_credentials_count = + max_possible_remaining_resident_credentials_count; + if existing_resident_credentials_count > 0 { + cred_management.subcommand = CredManagementCommand::EnumerateRPsBegin; + // We have to regenerate PUAP here. PUAT hasn't changed, but the content + // of the command has changed, and that is part of the PUAP-calculation + unwrap_result!( + cred_management.set_pin_uv_auth_param( + pin_uv_auth_result.get_pin_uv_auth_token() + ), + callback + ); + continue; + } else { + // This token doesn't have any resident keys, but its not an error, + // so we send an empty list. + send_status( + &status, + StatusUpdate::InteractiveManagement( + InteractiveUpdate::CredentialManagementUpdate(( + CredentialManagementResult::CredentialList( + credential_result, + ), + Some(pin_uv_auth_result), + )), + ), + ); + return true; + } + } + CredManagementCommand::EnumerateRPsBegin + | CredManagementCommand::EnumerateRPsGetNextRP => { + if matches!( + cred_management.subcommand, + CredManagementCommand::EnumerateRPsBegin + ) { + let total_rps = unwrap_option!(result.total_rps, callback); + if total_rps == 0 { + // This token doesn't have any RPs, but its not an error, + // so we return an Ok with an empty list. + send_status( + &status, + StatusUpdate::InteractiveManagement( + InteractiveUpdate::CredentialManagementUpdate(( + CredentialManagementResult::CredentialList( + credential_result, + ), + Some(pin_uv_auth_result), + )), + ), + ); + return true; + } + remaining_rps = total_rps - 1; + } else { + remaining_rps -= 1; + } + + let rp = unwrap_option!(result.rp, callback); + let rp_id_hash = unwrap_option!(result.rp_id_hash, callback); + let rp_res = CredentialRpListEntry { + rp, + rp_id_hash, + credentials: vec![], + }; + credential_result.credential_list.push(rp_res); + if remaining_rps > 0 { + cred_management.subcommand = + CredManagementCommand::EnumerateRPsGetNextRP; + } else { + // We have queried all RPs, now start querying the corresponding credentials for each RP + cred_management.subcommand = + CredManagementCommand::EnumerateCredentialsBegin( + credential_result.credential_list[0].rp_id_hash.clone(), + ); + } + // We have to regenerate PUAP here. PUAT hasn't changed, but the content + // of the command has changed, and that is part of the PUAP-calculation + unwrap_result!( + cred_management + .set_pin_uv_auth_param(pin_uv_auth_result.get_pin_uv_auth_token()), + callback + ); + continue; + } + CredManagementCommand::EnumerateCredentialsBegin(..) + | CredManagementCommand::EnumerateCredentialsGetNextCredential => { + let user = unwrap_option!(result.user, callback); + let credential_id = unwrap_option!(result.credential_id, callback); + let public_key = unwrap_option!(result.public_key, callback); + let cred_protect = unwrap_option!(result.cred_protect, callback); + let large_blob_key = result.large_blob_key; + + if matches!( + cred_management.subcommand, + CredManagementCommand::EnumerateCredentialsBegin(..) + ) { + remaining_cred_ids = + unwrap_option!(result.total_credentials, callback) - 1; + } else { + remaining_cred_ids -= 1; + } + // We might have to change the global variable, but need the unmodified below + let current_rp_backup = current_rp; + let mut we_are_done = false; + if remaining_cred_ids > 0 { + cred_management.subcommand = + CredManagementCommand::EnumerateCredentialsGetNextCredential; + } else { + current_rp += 1; + // We have all credentials from this RP. Starting with the next RP. + if current_rp < credential_result.credential_list.len() { + cred_management.subcommand = + CredManagementCommand::EnumerateCredentialsBegin( + credential_result.credential_list[current_rp] + .rp_id_hash + .clone(), + ); + // We have to regenerate PUAP here. PUAT hasn't changed, but the content + // of the command has changed, and that is part of the PUAP-calculation + unwrap_result!( + cred_management.set_pin_uv_auth_param( + pin_uv_auth_result.get_pin_uv_auth_token() + ), + callback + ); + } else { + // Finally done iterating over all RPs and their Credentials + we_are_done = true; + } + } + let key = CredentialListEntry { + user, + credential_id, + public_key, + cred_protect, + large_blob_key, + }; + credential_result.credential_list[current_rp_backup] + .credentials + .push(key); + if we_are_done { + send_status( + &status, + StatusUpdate::InteractiveManagement( + InteractiveUpdate::CredentialManagementUpdate(( + CredentialManagementResult::CredentialList( + credential_result, + ), + Some(pin_uv_auth_result), + )), + ), + ); + return true; + } else { + continue; + } + } + CredManagementCommand::DeleteCredential(_) => { + send_status( + &status, + StatusUpdate::InteractiveManagement( + InteractiveUpdate::CredentialManagementUpdate(( + CredentialManagementResult::DeleteSucess, + Some(pin_uv_auth_result), + )), + ), + ); + return true; + } + CredManagementCommand::UpdateUserInformation(_) => { + send_status( + &status, + StatusUpdate::InteractiveManagement( + InteractiveUpdate::CredentialManagementUpdate(( + CredentialManagementResult::UpdateSuccess, + Some(pin_uv_auth_result), + )), + ), + ); + return true; + } + }; + } + Err(e) => { + handle_errors!( + e, + status, + callback, + pin_uv_auth_result, + skip_uv, + skip_puap, + cached_puat + ); + } + } + } + false +} + +pub(crate) fn configure_authenticator( + dev: &mut Device, + puat_result: Option, + cfg_subcommand: AuthConfigCommand, + status: Sender, + callback: StateCallback>, + alive: &dyn Fn() -> bool, +) -> bool { + let mut authcfg = AuthenticatorConfig::new(cfg_subcommand); + let mut skip_uv = false; + let authinfo = match dev.get_authenticator_info() { + Some(i) => i.clone(), + None => { + callback.call(Err(HIDError::DeviceNotInitialized.into())); + return false; + } + }; + + if authinfo.options.authnr_cfg != Some(true) { + callback.call(Err(AuthenticatorError::HIDError( + HIDError::UnsupportedCommand, + ))); + return false; + } + + let mut skip_puap = false; + let mut cached_puat = false; // If we were provided with a cached puat from the outside + let mut pin_uv_auth_result = puat_result + .clone() + .unwrap_or(PinUvAuthResult::NoAuthRequired); + match puat_result { + Some(PinUvAuthResult::SuccessGetPinToken(t)) + | Some(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions(t)) + | Some(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingPinWithPermissions(t)) + if t.permissions + .contains(PinUvAuthTokenPermission::AuthenticatorConfiguration) => + { + skip_puap = true; + cached_puat = true; + unwrap_result!(authcfg.set_pin_uv_auth_param(Some(t)), callback); + } + _ => {} + } + let mut pin = None; + while alive() { + // We can use the AuthenticatorConfiguration-command only in two cases: + // 1. The device also supports the uv_acfg-permission (otherwise we can't establish a PUAP) + // 2. The device is NOT protected by PIN/UV (yet). This allows organizations to configure + // the token, before handing them out. + // If authinfo.options.uv_acfg is not supported, this will return UnauthorizedPermission + if !skip_puap { + pin_uv_auth_result = unwrap_result!( + determine_puap_if_needed( + &mut authcfg, + dev, + skip_uv, + PinUvAuthTokenPermission::AuthenticatorConfiguration, + UserVerificationRequirement::Preferred, + &status, + alive, + &mut pin, + ), + callback + ); + } + + debug!("------------------------------------------------------------------"); + debug!("{authcfg:?} using {pin_uv_auth_result:?}"); + debug!("------------------------------------------------------------------"); + + let resp = dev.send_cbor_cancellable(&authcfg, alive); + match resp { + Ok(()) => { + let auth_info = unwrap_option!(dev.refresh_authenticator_info(), callback); + send_status( + &status, + StatusUpdate::InteractiveManagement(InteractiveUpdate::AuthConfigUpdate(( + AuthConfigResult::Success(auth_info.clone()), + Some(pin_uv_auth_result), + ))), + ); + return true; + } + Err(e) => { + handle_errors!( + e, + status, + callback, + pin_uv_auth_result, + skip_uv, + skip_puap, + cached_puat + ); + } + } + } + false +} diff --git a/third_party/rust/authenticator/src/ctap2/preflight.rs b/third_party/rust/authenticator/src/ctap2/preflight.rs new file mode 100644 index 0000000000..6574042136 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/preflight.rs @@ -0,0 +1,530 @@ +use super::client_data::ClientDataHash; +use super::commands::get_assertion::{GetAssertion, GetAssertionExtensions, GetAssertionOptions}; +use super::commands::{CtapResponse, PinUvAuthCommand, RequestCtap1, Retryable}; +use crate::consts::{PARAMETER_SIZE, U2F_AUTHENTICATE, U2F_CHECK_IS_REGISTERED}; +use crate::crypto::PinUvAuthToken; +use crate::ctap2::server::{PublicKeyCredentialDescriptor, RelyingParty}; +use crate::errors::AuthenticatorError; +use crate::transport::errors::{ApduErrorStatus, HIDError}; +use crate::transport::{FidoDevice, FidoProtocol, VirtualFidoDevice}; +use crate::u2ftypes::CTAP1RequestAPDU; +use sha2::{Digest, Sha256}; + +/// 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. Or before a MakeCredential command, to determine +/// if this token is already registered or not. +#[derive(Debug)] +pub struct CheckKeyHandle<'assertion> { + pub key_handle: &'assertion [u8], + pub client_data_hash: &'assertion [u8], + pub rp: &'assertion RelyingParty, +} + +type EmptyResponse = (); +impl CtapResponse for EmptyResponse {} + +impl<'assertion> RequestCtap1 for CheckKeyHandle<'assertion> { + type Output = EmptyResponse; + type AdditionalInfo = (); + + fn ctap1_format(&self) -> Result<(Vec, Self::AdditionalInfo), HIDError> { + // In theory, we only need to do this for up=true, for up=false, we could + // use U2F_DONT_ENFORCE_USER_PRESENCE_AND_SIGN instead and use the answer directly. + // But that would involve another major refactoring to implement, and so we accept + // that we will send the final request twice to the authenticator. Once with + // U2F_CHECK_IS_REGISTERED followed by U2F_DONT_ENFORCE_USER_PRESENCE_AND_SIGN. + let flags = U2F_CHECK_IS_REGISTERED; + let mut auth_data = Vec::with_capacity(2 * PARAMETER_SIZE + 1 + self.key_handle.len()); + + auth_data.extend_from_slice(self.client_data_hash); + 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 = CTAP1RequestAPDU::serialize(cmd, flags, &auth_data)?; + Ok((apdu, ())) + } + + fn handle_response_ctap1( + &self, + _dev: &mut Dev, + status: Result<(), ApduErrorStatus>, + _input: &[u8], + _add_info: &Self::AdditionalInfo, + ) -> Result> { + // From the U2F-spec: https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#registration-request-message---u2f_register + // if the control byte is set to 0x07 by the FIDO Client, the U2F token is supposed to + // simply check whether the provided key handle was originally created by this token, + // and whether it was created for the provided application parameter. If so, the U2F + // token MUST respond with an authentication response + // message:error:test-of-user-presence-required (note that despite the name this + // signals a success condition). If the key handle was not created by this U2F + // token, or if it was created for a different application parameter, the token MUST + // respond with an authentication response message:error:bad-key-handle. + match status { + Ok(_) | Err(ApduErrorStatus::ConditionsNotSatisfied) => Ok(()), + Err(e) => Err(Retryable::Error(HIDError::ApduStatus(e))), + } + } + + fn send_to_virtual_device( + &self, + dev: &mut Dev, + ) -> Result { + dev.check_key_handle(self) + } +} + +/// "pre-flight": In order to determine whether authenticatorMakeCredential's excludeList or +/// authenticatorGetAssertion's allowList contain credential IDs that are already +/// present on an authenticator, a platform typically invokes authenticatorGetAssertion +/// with the "up" option key set to false and optionally pinUvAuthParam one or more times. +/// For CTAP1, the resulting list will always be of length 1. +pub(crate) fn do_credential_list_filtering_ctap1( + dev: &mut Dev, + cred_list: &[PublicKeyCredentialDescriptor], + rp: &RelyingParty, + client_data_hash: &ClientDataHash, +) -> Option { + let key_handle = cred_list + .iter() + // key-handles in CTAP1 are limited to 255 bytes, but are not limited in CTAP2. + // Filter out key-handles that are too long (can happen if this is a CTAP2-request, + // but the token only speaks CTAP1). + .filter(|key_handle| key_handle.id.len() < 256) + .find_map(|key_handle| { + let check_command = CheckKeyHandle { + key_handle: key_handle.id.as_ref(), + client_data_hash: client_data_hash.as_ref(), + rp, + }; + let res = dev.send_ctap1(&check_command); + match res { + Ok(_) => Some(key_handle.clone()), + _ => None, + } + }); + key_handle +} + +/// "pre-flight": In order to determine whether authenticatorMakeCredential's excludeList or +/// authenticatorGetAssertion's allowList contain credential IDs that are already +/// present on an authenticator, a platform typically invokes authenticatorGetAssertion +/// with the "up" option key set to false and optionally pinUvAuthParam one or more times. +pub(crate) fn do_credential_list_filtering_ctap2( + dev: &mut Dev, + cred_list: &[PublicKeyCredentialDescriptor], + rp: &RelyingParty, + pin_uv_auth_token: Option, +) -> Result, AuthenticatorError> { + let info = dev + .get_authenticator_info() + .ok_or(HIDError::DeviceNotInitialized)?; + let mut cred_list = cred_list.to_vec(); + // Step 1.0: Find out how long the exclude_list/allow_list is allowed to be + // If the token doesn't tell us, we assume a length of 1 + let mut chunk_size = match info.max_credential_count_in_list { + // Length 0 is not allowed by the spec, so we assume the device can't be trusted, which means + // falling back to a chunk size of 1 as the bare minimum. + None | Some(0) => 1, + Some(x) => x, + }; + + // Step 1.1: The device only supports keys up to a certain length. + // Filter out all keys that are longer, because they can't be + // from this device anyways. + match info.max_credential_id_length { + None => { /* no-op */ } + // Length 0 is not allowed by the spec, so we assume the device can't be trusted, which means + // falling back to a chunk size of 1 as the bare minimum. + Some(0) => { + chunk_size = 1; + } + Some(max_key_length) => { + cred_list.retain(|k| k.id.len() <= max_key_length); + } + } + + let chunked_list = cred_list.chunks(chunk_size); + + // Step 2: If we have more than one chunk: Loop over all, doing GetAssertion + // and if one of them comes back with a success, use only that chunk. + let mut final_list = Vec::new(); + for chunk in chunked_list { + let mut silent_assert = GetAssertion::new( + ClientDataHash(Sha256::digest("").into()), + rp.clone(), + chunk.to_vec(), + GetAssertionOptions { + user_verification: None, // defaults to Some(false) if puap is absent + user_presence: Some(false), + }, + GetAssertionExtensions::default(), + ); + silent_assert.set_pin_uv_auth_param(pin_uv_auth_token.clone())?; + match dev.send_msg(&silent_assert) { + Ok(mut response) => { + // This chunk contains a key_handle that is already known to the device. + // Filter out all credentials the device returned. Those are valid. + let credential_ids = response + .iter_mut() + .filter_map(|result| { + // CTAP 2.0 devices can omit the credentials in their response, + // if the given allowList was only 1 entry long. If so, we have + // to fill it in ourselfs. + if chunk.len() == 1 && result.assertion.credentials.is_none() { + Some(chunk[0].clone()) + } else { + result.assertion.credentials.take() + } + }) + .collect(); + // Replace credential_id_list with the valid credentials + final_list = credential_ids; + break; + } + Err(_) => { + // No-op: Go to next chunk. + // NOTE: while we expect a StatusCode::NoCredentials error here, some tokens return + // other values. + continue; + } + } + } + + // Step 3: Now ExcludeList/AllowList is either empty or has one batch with a 'known' credential. + // Send it as a normal Request and expect a "CredentialExcluded"-error in case of + // MakeCredential or a Success in case of GetAssertion + Ok(final_list) +} + +pub(crate) fn silently_discover_credentials( + dev: &mut Dev, + cred_list: &[PublicKeyCredentialDescriptor], + rp: &RelyingParty, + client_data_hash: &ClientDataHash, +) -> Vec { + if dev.get_protocol() == FidoProtocol::CTAP2 { + if let Ok(cred_list) = do_credential_list_filtering_ctap2(dev, cred_list, rp, None) { + return cred_list; + } + } else if let Some(key_handle) = + do_credential_list_filtering_ctap1(dev, cred_list, rp, client_data_hash) + { + return vec![key_handle]; + } + vec![] +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::{ + crypto::{COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve}, + ctap2::{ + attestation::{ + AAGuid, AttestedCredentialData, AuthenticatorData, AuthenticatorDataFlags, + Extension, + }, + commands::{CommandError, StatusCode}, + server::{AuthenticationExtensionsClientOutputs, AuthenticatorAttachment, Transport}, + }, + transport::{ + device_selector::tests::{make_device_simple_u2f, make_device_with_pin}, + hid::HIDDevice, + platform::device::Device, + }, + Assertion, GetAssertionResult, + }; + + fn new_relying_party(name: &str) -> RelyingParty { + RelyingParty { + id: String::from(name), + name: Some(String::from(name)), + } + } + + fn new_silent_assert( + rp: &RelyingParty, + allow_list: &[PublicKeyCredentialDescriptor], + ) -> GetAssertion { + GetAssertion::new( + ClientDataHash(Sha256::digest("").into()), + rp.clone(), + allow_list.to_vec(), + GetAssertionOptions { + user_verification: None, // defaults to Some(false) if puap is absent + user_presence: Some(false), + }, + GetAssertionExtensions::default(), + ) + } + + fn new_credential(fill: u8, repeat: usize) -> PublicKeyCredentialDescriptor { + PublicKeyCredentialDescriptor { + id: vec![fill; repeat], + transports: vec![Transport::USB], + } + } + + fn new_assertion_response( + rp: &RelyingParty, + cred: Option<&PublicKeyCredentialDescriptor>, + ) -> GetAssertionResult { + let credential_data = cred.map(|cred| AttestedCredentialData { + aaguid: AAGuid::default(), + credential_id: cred.id.clone(), + credential_public_key: COSEKey { + alg: COSEAlgorithm::RS256, + key: COSEKeyType::EC2(COSEEC2Key { + curve: Curve::SECP256R1, + x: vec![], + y: vec![], + }), + }, + }); + GetAssertionResult { + assertion: Assertion { + credentials: cred.cloned(), + auth_data: AuthenticatorData { + rp_id_hash: rp.hash(), + flags: AuthenticatorDataFlags::empty(), + counter: 0, + credential_data, + extensions: Extension::default(), + }, + signature: vec![], + user: None, + }, + attachment: AuthenticatorAttachment::Platform, + extensions: AuthenticationExtensionsClientOutputs::default(), + } + } + + fn new_check_key_handle<'a>( + rp: &'a RelyingParty, + client_data_hash: &'a ClientDataHash, + cred: &'a PublicKeyCredentialDescriptor, + ) -> CheckKeyHandle<'a> { + CheckKeyHandle { + key_handle: cred.id.as_ref(), + client_data_hash: client_data_hash.as_ref(), + rp, + } + } + + #[test] + fn test_preflight_ctap1_empty() { + let mut dev = Device::new("preflight").unwrap(); + make_device_simple_u2f(&mut dev); + let client_data_hash = ClientDataHash(Sha256::digest("").into()); + let rp = new_relying_party("preflight test"); + let res = silently_discover_credentials(&mut dev, &[], &rp, &client_data_hash); + assert!(res.is_empty()); + } + + #[test] + fn test_preflight_ctap1_multiple_replies() { + let mut dev = Device::new_skipping_serialization("preflight").unwrap(); + make_device_simple_u2f(&mut dev); + let rp = new_relying_party("preflight test"); + let cdh = ClientDataHash(Sha256::digest("").into()); + let allow_list = vec![ + new_credential(4, 4), + new_credential(3, 4), + new_credential(2, 4), + new_credential(1, 4), + ]; + dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[0])); + dev.add_upcoming_ctap_error(HIDError::ApduStatus( + ApduErrorStatus::WrongData, // Not a registered cred + )); + dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[1])); + dev.add_upcoming_ctap_error(HIDError::ApduStatus( + ApduErrorStatus::WrongData, // Not a registered cred + )); + dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[2])); + dev.add_upcoming_ctap_response(()); // Valid credential - the code exits here now and doesn't even look at the last one + + let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &cdh); + assert_eq!(res, vec![allow_list[2].clone()]); + } + + #[test] + fn test_preflight_ctap1_too_long_entries() { + let mut dev = Device::new_skipping_serialization("preflight").unwrap(); + make_device_simple_u2f(&mut dev); + let rp = new_relying_party("preflight test"); + let cdh = ClientDataHash(Sha256::digest("").into()); + let allow_list = vec![ + new_credential(4, 300), // ctap1 limit is 256 + new_credential(3, 4), + new_credential(2, 4), + new_credential(1, 4), + ]; + // allow_list[0] is filtered out due to its size + dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[1])); + dev.add_upcoming_ctap_error(HIDError::ApduStatus( + ApduErrorStatus::WrongData, // Not a registered cred + )); + dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[2])); + dev.add_upcoming_ctap_response(()); // Valid credential - the code exits here now and doesn't even look at the last one + + let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &cdh); + assert_eq!(res, vec![allow_list[2].clone()]); + } + + #[test] + fn test_preflight_ctap2_empty() { + let mut dev = Device::new("preflight").unwrap(); + make_device_with_pin(&mut dev); + let rp = new_relying_party("preflight test"); + let client_data_hash = ClientDataHash(Sha256::digest("").into()); + let res = silently_discover_credentials(&mut dev, &[], &rp, &client_data_hash); + assert!(res.is_empty()); + } + + #[test] + fn test_preflight_ctap20_no_cred_data() { + // CTAP2.0 tokens are allowed to not send any credential-data in their + // response, if the allow-list is of length one. See https://github.com/mozilla/authenticator-rs/issues/319 + let mut dev = Device::new_skipping_serialization("preflight").unwrap(); + make_device_with_pin(&mut dev); + let rp = new_relying_party("preflight test"); + let client_data_hash = ClientDataHash(Sha256::digest("").into()); + let allow_list = vec![new_credential(1, 4)]; + dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &allow_list)); + dev.add_upcoming_ctap_response(vec![new_assertion_response(&rp, None)]); + let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash); + assert_eq!(res, allow_list); + } + + #[test] + fn test_preflight_ctap2_one_valid_entry() { + let mut dev = Device::new_skipping_serialization("preflight").unwrap(); + make_device_with_pin(&mut dev); + let rp = new_relying_party("preflight test"); + let client_data_hash = ClientDataHash(Sha256::digest("").into()); + let allow_list = vec![new_credential(1, 4)]; + dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &allow_list)); + dev.add_upcoming_ctap_response(vec![new_assertion_response(&rp, Some(&allow_list[0]))]); + let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash); + assert_eq!(res, allow_list); + } + + #[test] + fn test_preflight_ctap2_multiple_entries() { + let mut dev = Device::new_skipping_serialization("preflight").unwrap(); + make_device_with_pin(&mut dev); + let rp = new_relying_party("preflight test"); + let client_data_hash = ClientDataHash(Sha256::digest("").into()); + let allow_list = vec![ + new_credential(3, 4), + new_credential(2, 4), + new_credential(1, 4), + new_credential(0, 4), + ]; + // Our test device doesn't say how many allow_list-entries it supports, so our code + // defaults to one. Thus three requests, with three answers. Only one of them + // valid. + dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &[allow_list[0].clone()])); + dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &[allow_list[1].clone()])); + dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &[allow_list[2].clone()])); + dev.add_upcoming_ctap_error(HIDError::Command(CommandError::StatusCode( + StatusCode::NoCredentials, + None, + ))); + dev.add_upcoming_ctap_error(HIDError::Command(CommandError::StatusCode( + StatusCode::NoCredentials, + None, + ))); + dev.add_upcoming_ctap_response(vec![new_assertion_response(&rp, Some(&allow_list[2]))]); + let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash); + assert_eq!(res, vec![allow_list[2].clone()]); + } + + #[test] + fn test_preflight_ctap2_multiple_replies() { + let mut dev = Device::new_skipping_serialization("preflight").unwrap(); + make_device_with_pin(&mut dev); + let rp = new_relying_party("preflight test"); + let client_data_hash = ClientDataHash(Sha256::digest("").into()); + let allow_list = vec![ + new_credential(4, 4), + new_credential(3, 4), + new_credential(2, 4), + new_credential(1, 4), + ]; + let mut info = dev.get_authenticator_info().unwrap().clone(); + info.max_credential_count_in_list = Some(5); + dev.set_authenticator_info(info); + // Our test device now says that it supports 5 allow_list-entries, + // so we can send all of them in one request + dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &allow_list)); + dev.add_upcoming_ctap_response(vec![ + new_assertion_response(&rp, Some(&allow_list[1])), + new_assertion_response(&rp, Some(&allow_list[2])), + new_assertion_response(&rp, Some(&allow_list[3])), + ]); + let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash); + assert_eq!(res, allow_list[1..].to_vec()); + } + + #[test] + fn test_preflight_ctap2_multiple_replies_some_invalid() { + let mut dev = Device::new_skipping_serialization("preflight").unwrap(); + make_device_with_pin(&mut dev); + let rp = new_relying_party("preflight test"); + let client_data_hash = ClientDataHash(Sha256::digest("").into()); + let allow_list = vec![ + new_credential(4, 4), + new_credential(3, 4), + new_credential(2, 4), + new_credential(1, 4), + ]; + let mut info = dev.get_authenticator_info().unwrap().clone(); + info.max_credential_count_in_list = Some(5); + dev.set_authenticator_info(info); + // Our test device now says that it supports 5 allow_list-entries, + // so we can send all of them in one request + dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &allow_list)); + dev.add_upcoming_ctap_response(vec![ + new_assertion_response(&rp, Some(&allow_list[1])), + new_assertion_response(&rp, None), // This will be ignored + new_assertion_response(&rp, Some(&allow_list[2])), + new_assertion_response(&rp, None), // This will be ignored + ]); + let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash); + assert_eq!(res, allow_list[1..=2].to_vec()); + } + + #[test] + fn test_preflight_ctap2_too_long_entries() { + let mut dev = Device::new_skipping_serialization("preflight").unwrap(); + make_device_with_pin(&mut dev); + let rp = new_relying_party("preflight test"); + let client_data_hash = ClientDataHash(Sha256::digest("").into()); + let allow_list = vec![ + new_credential(4, 50), // too long + new_credential(3, 4), + new_credential(2, 50), // too long + new_credential(1, 4), + ]; + let mut info = dev.get_authenticator_info().unwrap().clone(); + info.max_credential_count_in_list = Some(5); + info.max_credential_id_length = Some(20); + dev.set_authenticator_info(info); + // Our test device now says that it supports 5 allow_list-entries, + // so we can send all of them in one request, except for those + // that got pre-filtered, as they were too long. + dev.add_upcoming_ctap2_request(&new_silent_assert( + &rp, + &[allow_list[1].clone(), allow_list[3].clone()], + )); + dev.add_upcoming_ctap_response(vec![new_assertion_response(&rp, Some(&allow_list[1]))]); + let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash); + assert_eq!(res, vec![allow_list[1].clone()]); + } +} diff --git a/third_party/rust/authenticator/src/ctap2/server.rs b/third_party/rust/authenticator/src/ctap2/server.rs new file mode 100644 index 0000000000..fdf2637464 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/server.rs @@ -0,0 +1,629 @@ +use crate::crypto::COSEAlgorithm; +use crate::{errors::AuthenticatorError, AuthenticatorTransports, KeyHandle}; +use base64::Engine; +use serde::de::MapAccess; +use serde::{ + de::{Error as SerdeError, Unexpected, Visitor}, + ser::SerializeMap, + Deserialize, Deserializer, Serialize, Serializer, +}; +use serde_bytes::{ByteBuf, Bytes}; +use sha2::{Digest, Sha256}; +use std::convert::{Into, TryFrom}; +use std::fmt; + +#[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::engine::general_purpose::URL_SAFE_NO_PAD.encode(self.0); + 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(AuthenticatorError::InvalidRelyingPartyInput) + } else { + payload.copy_from_slice(src); + Ok(RpIdHash(payload)) + } + } +} + +// NOTE: WebAuthn requires all fields and CTAP2 does not. +#[derive(Debug, Serialize, Clone, Default, Deserialize, PartialEq, Eq)] +pub struct RelyingParty { + pub id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +impl RelyingParty { + pub fn from(id: S) -> Self + where + S: Into, + { + Self { + id: id.into(), + name: None, + } + } + + pub fn hash(&self) -> RpIdHash { + let mut hasher = Sha256::new(); + hasher.update(&self.id); + + let mut output = [0u8; 32]; + output.copy_from_slice(hasher.finalize().as_slice()); + + RpIdHash(output) + } +} + +// NOTE: WebAuthn requires all fields and CTAP2 does not. +#[derive(Debug, Serialize, Clone, Eq, PartialEq, Deserialize, Default)] +pub struct PublicKeyCredentialUserEntity { + #[serde(with = "serde_bytes")] + pub id: Vec, + pub name: Option, + #[serde(skip_serializing_if = "Option::is_none", rename = "displayName")] + pub display_name: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PublicKeyCredentialParameters { + pub alg: COSEAlgorithm, +} + +impl TryFrom for PublicKeyCredentialParameters { + type Error = AuthenticatorError; + fn try_from(arg: i32) -> Result { + let alg = COSEAlgorithm::try_from(arg as i64)?; + Ok(PublicKeyCredentialParameters { 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() + } +} + +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_else(|| SerdeError::missing_field("alg"))?; + + Ok(PublicKeyCredentialParameters { alg }) + } + } + + deserializer.deserialize_bytes(PublicKeyCredentialParametersVisitor) + } +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Eq, Clone)] +#[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 + } +} + +pub type PublicKeyCredentialId = Vec; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PublicKeyCredentialDescriptor { + pub id: PublicKeyCredentialId, + pub transports: Vec, +} + +impl Serialize for PublicKeyCredentialDescriptor { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // TODO(MS): Transports is OPTIONAL, but some older tokens don't understand it + // and return a CBOR-Parsing error. It is only a hint for the token, + // so we'll leave it out for the moment + let mut map = serializer.serialize_map(Some(2))?; + // let mut map = serializer.serialize_map(Some(3))?; + map.serialize_entry("id", Bytes::new(&self.id))?; + map.serialize_entry("type", "public-key")?; + // map.serialize_entry("transports", &self.transports)?; + map.end() + } +} + +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")); + } + let id_bytes: ByteBuf = map.next_value()?; + id = Some(id_bytes.into_vec()); + } + "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_else(|| SerdeError::missing_field("id"))?; + let transports = transports.unwrap_or_default(); + + Ok(PublicKeyCredentialDescriptor { id, transports }) + } + } + + deserializer.deserialize_any(PublicKeyCredentialDescriptorVisitor) + } +} + +impl From<&KeyHandle> for PublicKeyCredentialDescriptor { + fn from(kh: &KeyHandle) -> Self { + Self { + id: kh.credential.clone(), + transports: kh.transports.into(), + } + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum ResidentKeyRequirement { + Discouraged, + Preferred, + Required, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum UserVerificationRequirement { + Discouraged, + Preferred, + Required, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum CredentialProtectionPolicy { + UserVerificationOptional = 1, + UserVerificationOptionalWithCredentialIDList = 2, + UserVerificationRequired = 3, +} + +impl Serialize for CredentialProtectionPolicy { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_u64(*self as u64) + } +} + +impl<'de> Deserialize<'de> for CredentialProtectionPolicy { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct CredentialProtectionPolicyVisitor; + + impl<'de> Visitor<'de> for CredentialProtectionPolicyVisitor { + type Value = CredentialProtectionPolicy; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an integer") + } + + fn visit_u64(self, v: u64) -> Result + where + E: SerdeError, + { + match v { + 1 => Ok(CredentialProtectionPolicy::UserVerificationOptional), + 2 => Ok( + CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIDList, + ), + 3 => Ok(CredentialProtectionPolicy::UserVerificationRequired), + _ => Err(SerdeError::invalid_value( + Unexpected::Unsigned(v), + &"valid CredentialProtectionPolicy", + )), + } + } + } + + deserializer.deserialize_any(CredentialProtectionPolicyVisitor) + } +} + +#[derive(Clone, Debug, Default)] +pub struct AuthenticationExtensionsClientInputs { + pub app_id: Option, + pub cred_props: Option, + pub credential_protection_policy: Option, + pub enforce_credential_protection_policy: Option, + pub hmac_create_secret: Option, + pub min_pin_length: Option, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct CredentialProperties { + pub rk: bool, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct AuthenticationExtensionsClientOutputs { + pub app_id: Option, + pub cred_props: Option, + pub hmac_create_secret: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum AuthenticatorAttachment { + CrossPlatform, + Platform, + Unknown, +} + +#[cfg(test)] +mod test { + use super::{ + COSEAlgorithm, PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, + PublicKeyCredentialUserEntity, RelyingParty, Transport, + }; + use serde_cbor::from_slice; + + fn create_user() -> PublicKeyCredentialUserEntity { + PublicKeyCredentialUserEntity { + 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, + ], + name: Some(String::from("johnpsmith@example.com")), + display_name: Some(String::from("John P. Smith")), + } + } + #[test] + fn serialize_rp() { + let rp = RelyingParty { + id: String::from("Acme"), + name: None, + }; + + let payload = ser::to_vec(&rp).unwrap(); + assert_eq!( + &payload, + &[ + 0xa1, // map(1) + 0x62, // text(2) + 0x69, 0x64, // "id" + 0x64, // text(4) + 0x41, 0x63, 0x6d, 0x65 + ] + ); + } + + #[test] + fn test_deserialize_user() { + // This includes an obsolete "icon" field to test that deserialization + // ignores it. + let input = 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, // ... + ]; + let expected = create_user(); + let actual: PublicKeyCredentialUserEntity = from_slice(&input).unwrap(); + assert_eq!(expected, actual); + } + + #[test] + fn serialize_user() { + let user = create_user(); + + let payload = ser::to_vec(&user).unwrap(); + println!("payload = {payload:?}"); + assert_eq!( + payload, + vec![ + 0xa3, // map(3) + 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, // ... + 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_nodisplayname() { + let user = PublicKeyCredentialUserEntity { + 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, + ], + name: Some(String::from("johnpsmith@example.com")), + display_name: None, + }; + + let payload = ser::to_vec(&user).unwrap(); + println!("payload = {payload:?}"); + 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: COSEAlgorithm::ES256, + }, + PublicKeyCredentialParameters { + alg: COSEAlgorithm::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) + 0xa2, // map(2) + 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, // ... + 0x64, // text(4) + 0x74, 0x79, 0x70, 0x65, // "type" + 0x6a, // text(10) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, // "public-key" + 0x2D, 0x6B, 0x65, + 0x79, // ... + + // Deactivated for now + //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/third_party/rust/authenticator/src/ctap2/utils.rs b/third_party/rust/authenticator/src/ctap2/utils.rs new file mode 100644 index 0000000000..b5d84c7d34 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/utils.rs @@ -0,0 +1,39 @@ +use serde::de; +use serde_cbor::Deserializer; +use std::io::Read; + +pub fn serde_parse_err(s: &str) -> E { + E::custom(format!("Failed to parse {s}")) +} + +pub fn from_slice_stream<'a, T, R: Read, E: de::Error>(data: &mut R) -> Result +where + T: de::Deserialize<'a>, +{ + let mut deserializer = Deserializer::from_reader(data); + de::Deserialize::deserialize(&mut deserializer) + .map_err(|x| serde_parse_err(&format!("{}: {}", stringify!(T), &x.to_string()))) +} + +// Parsing routines + +pub fn read_be_u32(data: &mut R) -> Result { + let mut buf = [0; 4]; + data.read_exact(&mut buf) + .map_err(|_| serde_parse_err("u32"))?; + Ok(u32::from_be_bytes(buf)) +} + +pub fn read_be_u16(data: &mut R) -> Result { + let mut buf = [0; 2]; + data.read_exact(&mut buf) + .map_err(|_| serde_parse_err("u16"))?; + Ok(u16::from_be_bytes(buf)) +} + +pub fn read_byte(data: &mut R) -> Result { + match data.bytes().next() { + Some(Ok(s)) => Ok(s), + _ => Err(serde_parse_err("u8")), + } +} -- cgit v1.2.3