summaryrefslogtreecommitdiffstats
path: root/third_party/rust/authenticator/src/ctap2
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /third_party/rust/authenticator/src/ctap2
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/authenticator/src/ctap2')
-rw-r--r--third_party/rust/authenticator/src/ctap2/attestation.rs1129
-rw-r--r--third_party/rust/authenticator/src/ctap2/client_data.rs339
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/authenticator_config.rs225
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/bio_enrollment.rs660
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/client_pin.rs851
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/credential_management.rs457
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/get_assertion.rs1494
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/get_info.rs1054
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/get_next_assertion.rs54
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/get_version.rs121
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/make_credentials.rs1061
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/mod.rs477
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/reset.rs123
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/selection.rs123
-rw-r--r--third_party/rust/authenticator/src/ctap2/mod.rs1518
-rw-r--r--third_party/rust/authenticator/src/ctap2/preflight.rs530
-rw-r--r--third_party/rust/authenticator/src/ctap2/server.rs629
-rw-r--r--third_party/rust/authenticator/src/ctap2/utils.rs39
18 files changed, 10884 insertions, 0 deletions
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<u8>),
+}
+
+impl Serialize for HmacSecretResponse {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ 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<D>(deserializer: D) -> Result<Self, D::Error>
+ 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<E>(self, v: &[u8]) -> Result<Self::Value, E>
+ where
+ E: SerdeError,
+ {
+ Ok(HmacSecretResponse::Secret(v.to_vec()))
+ }
+
+ fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
+ 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<CredentialProtectionPolicy>,
+ #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")]
+ pub hmac_secret: Option<HmacSecretResponse>,
+ #[serde(rename = "minPinLength", skip_serializing_if = "Option::is_none")]
+ pub min_pin_length: Option<u64>,
+}
+
+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<AAGuid, AuthenticatorError> {
+ 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<D>(deserializer: D) -> Result<Self, D::Error>
+ 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<E>(self, v: &[u8]) -> Result<Self::Value, E>
+ 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<u8>,
+ pub credential_public_key: COSEKey,
+}
+
+fn parse_attested_cred_data<R: Read, E: SerdeError>(
+ data: &mut R,
+) -> Result<AttestedCredentialData, E> {
+ 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<AttestedCredentialData>,
+ pub extensions: Extension,
+}
+
+impl AuthenticatorData {
+ pub fn to_vec(&self) -> Vec<u8> {
+ 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<D>(deserializer: D) -> Result<Self, D::Error>
+ 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<E>(self, input: &[u8]) -> Result<Self::Value, E>
+ 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ 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<u8>);
+
+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<u8>);
+
+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 = <std::collections::BTreeMap<(), ()>>::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<bool, AuthenticatorError> {
+// 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<AttestationCertificate>, // (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<AttestationCertificate>, // (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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ 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<AttestationCertificate> = 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<u8> = 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ 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<D>(deserializer: D) -> Result<Self, D::Error>
+ 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<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ 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<D>(deserializer: D) -> Result<Self, D::Error>
+ 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<E>(self, v: &str) -> Result<Self::Value, E>
+ 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<u8>) -> Self {
+ let value = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(input);
+ Challenge(value)
+ }
+}
+
+impl From<Vec<u8>> for Challenge {
+ fn from(v: Vec<u8>) -> 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<TokenBinding>,
+}
+
+impl CollectedClientData {
+ pub fn hash(&self) -> Result<ClientDataHash, HIDError> {
+ // 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ 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<u64>,
+ /// 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<Vec<String>>,
+ /// The authenticator returns CTAP2_ERR_PIN_POLICY_VIOLATION until changePIN is successful.
+ pub force_change_pin: Option<bool>,
+}
+
+impl Serialize for SetMinPINLength {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ 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<PinUvAuthParam>, // 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ 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, &params)?;
+ }
+ }
+
+ 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<Vec<u8>, HIDError> {
+ let output = to_vec(&self).map_err(CommandError::Serializing)?;
+ trace!("client subcommmand: {:04X?}", &output);
+ Ok(output)
+ }
+
+ fn handle_response_ctap2<Dev>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError>
+ 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<Dev: crate::VirtualFidoDevice>(
+ &self,
+ _dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ unimplemented!()
+ }
+}
+
+impl PinUvAuthCommand for AuthenticatorConfig {
+ fn set_pin_uv_auth_param(
+ &mut self,
+ pin_uv_auth_token: Option<PinUvAuthToken>,
+ ) -> 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<bool>) {
+ /* 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<u8> for BioEnrollmentModality {
+ fn from(value: u8) -> Self {
+ match value {
+ 0x01 => BioEnrollmentModality::Fingerprint,
+ x => BioEnrollmentModality::Other(x),
+ }
+ }
+}
+
+impl From<BioEnrollmentModality> for u8 {
+ fn from(value: BioEnrollmentModality) -> Self {
+ match value {
+ BioEnrollmentModality::Fingerprint => 0x01,
+ BioEnrollmentModality::Other(x) => x,
+ }
+ }
+}
+
+impl Serialize for BioEnrollmentModality {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_u8((*self).into())
+ }
+}
+
+impl<'de> Deserialize<'de> for BioEnrollmentModality {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ 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<E>(self, v: u8) -> Result<Self::Value, E>
+ where
+ E: SerdeError,
+ {
+ Ok(BioEnrollmentModality::from(v))
+ }
+ }
+
+ deserializer.deserialize_u8(BioEnrollmentModalityVisitor)
+ }
+}
+
+pub type BioTemplateId = Vec<u8>;
+#[derive(Debug, Clone, Deserialize, Default)]
+struct BioEnrollmentParams {
+ template_id: Option<BioTemplateId>, // Template Identifier.
+ template_friendly_name: Option<String>, // Template Friendly Name.
+ timeout_milliseconds: Option<u64>, // 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ 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<u64>),
+ EnrollCaptureNextSample((BioTemplateId, Option<u64>)),
+ 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<PinUvAuthParam>,
+ 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ 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, &params)?;
+ }
+
+ 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<PinUvAuthToken>,
+ ) -> 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(&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,
+ _info: &crate::AuthenticatorInfo,
+ _uv: UserVerificationRequirement,
+ ) -> bool {
+ // "discouraged" does not exist for BioEnrollment
+ false
+ }
+
+ fn set_uv_option(&mut self, _uv: Option<bool>) {
+ /* 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<Vec<u8>, HIDError> {
+ let output = to_vec(&self).map_err(CommandError::Serializing)?;
+ trace!("client subcommmand: {:04X?}", &output);
+ Ok(output)
+ }
+
+ fn handle_response_ctap2<Dev>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError>
+ 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<Value> = 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<Dev: crate::VirtualFidoDevice>(
+ &self,
+ _dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ 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<D>(deserializer: D) -> Result<Self, D::Error>
+ 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<E>(self, v: u8) -> Result<Self::Value, E>
+ 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<D>(deserializer: D) -> Result<Self, D::Error>
+ 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<E>(self, v: u8) -> Result<Self::Value, E>
+ 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<String>,
+}
+
+impl<'de> Deserialize<'de> for BioTemplateInfo {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ 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<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ 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::<ByteBuf>()?.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::<IgnoredAny>()?;
+ 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<BioEnrollmentModality>,
+ /// 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<FingerprintKind>,
+ /// Indicates the maximum good samples required for enrollment.
+ pub(crate) max_capture_samples_required_for_enroll: Option<u64>,
+ /// Template Identifier.
+ pub(crate) template_id: Option<BioTemplateId>,
+ /// Last enrollment sample status.
+ pub(crate) last_enroll_sample_status: Option<LastEnrollmentSampleStatus>,
+ /// Number of more sample required for enrollment to complete
+ pub(crate) remaining_samples: Option<u64>,
+ /// Array of templateInfo’s
+ pub(crate) template_infos: Vec<BioTemplateInfo>,
+ /// Indicates the maximum number of bytes the authenticator will accept as a templateFriendlyName.
+ pub(crate) max_template_friendly_name: Option<u64>,
+}
+
+impl CtapResponse for BioEnrollmentResponse {}
+
+impl<'de> Deserialize<'de> for BioEnrollmentResponse {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ 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<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ 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::<ByteBuf>()?.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::<IgnoredAny>()?;
+ 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<u8>,
+ pub template_friendly_name: Option<String>,
+}
+
+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<u64>,
+}
+
+#[derive(Debug, Serialize)]
+pub enum BioEnrollmentResult {
+ EnrollmentList(Vec<EnrollmentInfo>),
+ 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<PinUvAuthProtocol>,
+ pub subcommand: PINSubcommand,
+ pub key_agreement: Option<COSEKey>,
+ pub pin_auth: Option<Vec<u8>>,
+ pub new_pin_enc: Option<Vec<u8>>,
+ pub pin_hash_enc: Option<Vec<u8>>,
+ pub permissions: Option<u8>,
+ pub rp_id: Option<String>,
+}
+
+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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ 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<ClientPIN, CommandError>;
+ fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError>;
+}
+
+#[derive(Default, Debug, PartialEq, Eq)]
+pub struct ClientPinResponse {
+ pub key_agreement: Option<COSEKey>,
+ pub pin_token: Option<Vec<u8>>,
+ /// Number of PIN attempts remaining before lockout.
+ pub pin_retries: Option<u8>,
+ pub power_cycle_state: Option<bool>,
+ pub uv_retries: Option<u8>,
+}
+
+impl CtapResponse for ClientPinResponse {}
+
+impl<'de> Deserialize<'de> for ClientPinResponse {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ 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<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ 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::<IgnoredAny>()?;
+ 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<ClientPIN, CommandError> {
+ Ok(ClientPIN {
+ pin_protocol: Some(self.pin_protocol.clone()),
+ subcommand: PINSubcommand::GetKeyAgreement,
+ ..ClientPIN::default()
+ })
+ }
+
+ fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> {
+ 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<ClientPIN, CommandError> {
+ 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<Self::Output, CommandError> {
+ 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<String>,
+}
+
+impl<'sc, 'pin> GetPinUvAuthTokenUsingPinWithPermissions<'sc, 'pin> {
+ pub fn new(
+ shared_secret: &'sc SharedSecret,
+ pin: &'pin Pin,
+ permissions: PinUvAuthTokenPermission,
+ rp_id: Option<String>,
+ ) -> 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<ClientPIN, CommandError> {
+ 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<Self::Output, CommandError> {
+ 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<ClientPIN, CommandError> {
+ Ok(ClientPIN {
+ subcommand: PINSubcommand::$name,
+ ..ClientPIN::default()
+ })
+ }
+
+ fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> {
+ 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<String>,
+}
+
+impl<'sc> GetPinUvAuthTokenUsingUvWithPermissions<'sc> {
+ pub fn new(
+ shared_secret: &'sc SharedSecret,
+ permissions: PinUvAuthTokenPermission,
+ rp_id: Option<String>,
+ ) -> Self {
+ GetPinUvAuthTokenUsingUvWithPermissions {
+ shared_secret,
+ permissions,
+ rp_id,
+ }
+ }
+}
+
+impl<'sc> ClientPINSubCommand for GetPinUvAuthTokenUsingUvWithPermissions<'sc> {
+ type Output = ClientPinResponse;
+
+ fn as_client_pin(&self) -> Result<ClientPIN, CommandError> {
+ 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<Self::Output, CommandError> {
+ 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<ClientPIN, CommandError> {
+ 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<Self::Output, CommandError> {
+ // 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<Self, CommandError> {
+ 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<ClientPIN, CommandError> {
+ 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<Self::Output, CommandError> {
+ // 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<T> RequestCtap2 for T
+where
+ T: ClientPINSubCommand<Output = ClientPinResponse>,
+ T: fmt::Debug,
+{
+ type Output = <T as ClientPINSubCommand>::Output;
+
+ fn command(&self) -> Command {
+ Command::ClientPin
+ }
+
+ fn wire_format(&self) -> Result<Vec<u8>, 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<Dev: FidoDevice>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError> {
+ 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() {
+ <T as ClientPINSubCommand>::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<Dev: VirtualFidoDevice>(
+ &self,
+ dev: &mut Dev,
+ ) -> Result<ClientPinResponse, HIDError> {
+ 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<u8> {
+ 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<u8> {
+ 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<u8>),
+ InvalidUv(Option<u8>),
+ 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<CryptoError> 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<RpIdHash>, // RP ID SHA-256 hash
+ credential_id: Option<PublicKeyCredentialDescriptor>, // Credential Identifier
+ user: Option<PublicKeyCredentialUserEntity>, // 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ 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<PinUvAuthParam>, // 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ 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, &params)?;
+ }
+
+ 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<u64>,
+ /// Number of maximum possible remaining discoverable credentials which can be created on the authenticator.
+ pub max_possible_remaining_resident_credentials_count: Option<u64>,
+ /// RP Information
+ pub rp: Option<RelyingParty>,
+ /// RP ID SHA-256 hash
+ pub rp_id_hash: Option<RpIdHash>,
+ /// Total number of RPs present on the authenticator
+ pub total_rps: Option<u64>,
+ /// User Information
+ pub user: Option<PublicKeyCredentialUserEntity>,
+ /// Credential ID
+ pub credential_id: Option<PublicKeyCredentialDescriptor>,
+ /// Public key of the credential.
+ pub public_key: Option<COSEKey>,
+ /// Total number of credentials present on the authenticator for the RP in question
+ pub total_credentials: Option<u64>,
+ /// Credential protection policy.
+ pub cred_protect: Option<u64>,
+ /// Large blob encryption key.
+ pub large_blob_key: Option<Vec<u8>>,
+}
+
+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<CredentialListEntry>,
+}
+
+#[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<Vec<u8>>,
+}
+
+#[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<CredentialRpListEntry>,
+}
+
+impl CredentialList {
+ pub fn new() -> Self {
+ Default::default()
+ }
+}
+
+impl<'de> Deserialize<'de> for CredentialManagementResponse {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ 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<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ 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::<ByteBuf>()?;
+ 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::<ByteBuf>()?.into_vec());
+ }
+
+ k => {
+ warn!("ClientPinResponse: unexpected key: {:?}", k);
+ let _ = map.next_value::<IgnoredAny>()?;
+ 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<Vec<u8>, HIDError> {
+ let output = to_vec(&self).map_err(CommandError::Serializing)?;
+ trace!("client subcommmand: {:04X?}", &output);
+ Ok(output)
+ }
+
+ fn handle_response_ctap2<Dev>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError>
+ 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<Value> = 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<Dev: crate::VirtualFidoDevice>(
+ &self,
+ _dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ 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<PinUvAuthToken>,
+ ) -> 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(&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,
+ _info: &crate::AuthenticatorInfo,
+ _uv: UserVerificationRequirement,
+ ) -> bool {
+ // "discouraged" does not exist for AuthenticatorConfig
+ false
+ }
+
+ fn set_uv_option(&mut self, _uv: Option<bool>) {
+ /* 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<bool>,
+ #[serde(rename = "up", skip_serializing_if = "Option::is_none")]
+ pub user_presence: Option<bool>,
+}
+
+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<u8>,
+ pub salt_auth: Vec<u8>,
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct HmacSecretExtension {
+ pub salt1: Vec<u8>,
+ pub salt2: Option<Vec<u8>>,
+ calculated_hmac: Option<CalculatedHmacSecretExtension>,
+}
+
+impl HmacSecretExtension {
+ pub fn new(salt1: Vec<u8>, salt2: Option<Vec<u8>>) -> 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ 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<String>,
+ #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")]
+ pub hmac_secret: Option<HmacSecretExtension>,
+}
+
+impl From<AuthenticationExtensionsClientInputs> 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<PublicKeyCredentialDescriptor>,
+
+ // 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<PinUvAuthParam>,
+}
+
+impl GetAssertion {
+ pub fn new(
+ client_data_hash: ClientDataHash,
+ rp: RelyingParty,
+ allow_list: Vec<PublicKeyCredentialDescriptor>,
+ options: GetAssertionOptions,
+ extensions: GetAssertionExtensions,
+ ) -> Self {
+ Self {
+ client_data_hash,
+ rp,
+ allow_list,
+ extensions,
+ options,
+ pin_uv_auth_param: None,
+ }
+ }
+
+ pub fn finalize_result<Dev: FidoDevice>(&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<PinUvAuthToken>,
+ ) -> 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<bool>) {
+ 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ 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<GetAssertionResult>;
+impl CtapResponse for GetAssertionOutput {}
+
+impl RequestCtap1 for GetAssertion {
+ type Output = Vec<GetAssertionResult>;
+ type AdditionalInfo = PublicKeyCredentialDescriptor;
+
+ fn ctap1_format(&self) -> Result<(Vec<u8>, 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<Dev: FidoDevice>(
+ &self,
+ dev: &mut Dev,
+ status: Result<(), ApduErrorStatus>,
+ input: &[u8],
+ add_info: &PublicKeyCredentialDescriptor,
+ ) -> Result<Self::Output, Retryable<HIDError>> {
+ 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<Dev: VirtualFidoDevice>(
+ &self,
+ dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ 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<GetAssertionResult>;
+
+ fn command(&self) -> Command {
+ Command::GetAssertion
+ }
+
+ fn wire_format(&self) -> Result<Vec<u8>, HIDError> {
+ Ok(ser::to_vec(&self).map_err(CommandError::Serializing)?)
+ }
+
+ fn handle_response_ctap2<Dev: FidoDevice>(
+ &self,
+ dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError> {
+ 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<Dev: VirtualFidoDevice>(
+ &self,
+ dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ 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<PublicKeyCredentialDescriptor>, /* Was optional in CTAP2.0, is
+ * mandatory in CTAP2.1 */
+ pub auth_data: AuthenticatorData,
+ pub signature: Vec<u8>,
+ pub user: Option<PublicKeyCredentialUserEntity>,
+}
+
+impl From<GetAssertionResponse> 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<GetAssertionResult, CommandError> {
+ 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<PublicKeyCredentialDescriptor>,
+ pub auth_data: AuthenticatorData,
+ pub signature: Vec<u8>,
+ pub user: Option<PublicKeyCredentialUserEntity>,
+ pub number_of_credentials: Option<usize>,
+}
+
+impl CtapResponse for GetAssertionResponse {}
+
+impl<'de> Deserialize<'de> for GetAssertionResponse {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ 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<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ 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<u8> = 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<Vec<u8>, HIDError> {
+ Ok(Vec::new())
+ }
+
+ fn handle_response_ctap2<Dev: FidoDevice>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError> {
+ 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<Dev: VirtualFidoDevice>(
+ &self,
+ dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ 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<bool>,
+
+ /// 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<bool>,
+
+ // ----------------------------------------------------
+ // 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<bool>,
+
+ /// 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<bool>,
+
+ /// 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<bool>,
+
+ /// 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<bool>,
+
+ /// 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<bool>,
+
+ /// "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<bool>,
+
+ /// 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<bool>,
+
+ /// 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<bool>,
+
+ /// 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<bool>,
+
+ /// 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<bool>,
+
+ /// "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<bool>,
+
+ /// 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<bool>,
+
+ /// 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<bool>,
+
+ /// 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<bool>,
+}
+
+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<AuthenticatorVersion>,
+ pub extensions: Vec<String>,
+ pub aaguid: AAGuid,
+ pub options: AuthenticatorOptions,
+ pub max_msg_size: Option<usize>,
+ pub pin_protocols: Option<Vec<u64>>,
+ // CTAP 2.1
+ pub max_credential_count_in_list: Option<usize>,
+ pub max_credential_id_length: Option<usize>,
+ pub transports: Option<Vec<String>>,
+ pub algorithms: Option<Vec<PublicKeyCredentialParameters>>,
+ pub max_ser_large_blob_array: Option<u64>,
+ pub force_pin_change: Option<bool>,
+ pub min_pin_length: Option<u64>,
+ pub firmware_version: Option<u64>,
+ pub max_cred_blob_length: Option<u64>,
+ pub max_rpids_for_set_min_pin_length: Option<u64>,
+ pub preferred_platform_uv_attempts: Option<u64>,
+ pub uv_modality: Option<u64>,
+ pub certifications: Option<BTreeMap<String, u64>>,
+ pub remaining_discoverable_credentials: Option<u64>,
+ pub vendor_prototype_config_commands: Option<Vec<u64>>,
+}
+
+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<D>(deserializer: D) -> Result<Self, D::Error>
+ 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<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ 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<Vec<_>> = 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::<IgnoredAny>()?;
+ 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::<AuthenticatorInfo>(&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<Vec<u8>, HIDError> {
+ Ok(Vec::new())
+ }
+
+ fn handle_response_ctap2<Dev: FidoDevice>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError> {
+ 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<Dev: VirtualFidoDevice>(
+ &self,
+ _dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ 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<Dev: FidoDevice>(
+ &self,
+ _dev: &mut Dev,
+ _status: Result<(), ApduErrorStatus>,
+ input: &[u8],
+ _add_info: &(),
+ ) -> Result<Self::Output, Retryable<HIDError>> {
+ 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<u8>, ()), HIDError> {
+ let flags = 0;
+
+ let cmd = U2F_VERSION;
+ let data = CTAP1RequestAPDU::serialize(cmd, flags, &[])?;
+ Ok((data, ()))
+ }
+
+ fn send_to_virtual_device<Dev: VirtualFidoDevice>(
+ &self,
+ dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ 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<Self, CommandError> {
+ 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<D>(deserializer: D) -> Result<Self, D::Error>
+ 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<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ where
+ M: MapAccess<'de>,
+ {
+ let mut format: Option<&str> = None;
+ let mut auth_data: Option<AuthenticatorData> = None;
+ let mut att_stmt: Option<AttestationStatement> = 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<bool>,
+ #[serde(rename = "uv", skip_serializing_if = "Option::is_none")]
+ pub user_verification: Option<bool>,
+ // 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<bool>,
+ #[serde(rename = "credProtect", skip_serializing_if = "Option::is_none")]
+ pub cred_protect: Option<CredentialProtectionPolicy>,
+ #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")]
+ pub hmac_secret: Option<bool>,
+ #[serde(rename = "minPinLength", skip_serializing_if = "Option::is_none")]
+ pub min_pin_length: Option<bool>,
+}
+
+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<AuthenticationExtensionsClientInputs> 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<PublicKeyCredentialUserEntity>,
+ pub pub_cred_params: Vec<PublicKeyCredentialParameters>,
+ pub exclude_list: Vec<PublicKeyCredentialDescriptor>,
+
+ // 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<PinUvAuthParam>,
+ pub enterprise_attestation: Option<u64>,
+}
+
+impl MakeCredentials {
+ #[allow(clippy::too_many_arguments)]
+ pub fn new(
+ client_data_hash: ClientDataHash,
+ rp: RelyingParty,
+ user: Option<PublicKeyCredentialUserEntity>,
+ pub_cred_params: Vec<PublicKeyCredentialParameters>,
+ exclude_list: Vec<PublicKeyCredentialDescriptor>,
+ 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<Dev: FidoDevice>(&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<PinUvAuthToken>,
+ ) -> 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<bool>) {
+ 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ 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<u8>, ()), 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, &register_data)?;
+
+ Ok((apdu, ()))
+ }
+
+ fn handle_response_ctap1<Dev: FidoDevice>(
+ &self,
+ dev: &mut Dev,
+ status: Result<(), ApduErrorStatus>,
+ input: &[u8],
+ _add_info: &(),
+ ) -> Result<Self::Output, Retryable<HIDError>> {
+ 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<Dev: VirtualFidoDevice>(
+ &self,
+ dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ 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<Vec<u8>, HIDError> {
+ Ok(ser::to_vec(&self).map_err(CommandError::Serializing)?)
+ }
+
+ fn handle_response_ctap2<Dev: FidoDevice>(
+ &self,
+ dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError> {
+ 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<Dev: VirtualFidoDevice>(
+ &self,
+ dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ 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<T> {
+ Retry,
+ Error(T),
+}
+
+impl<T> Retryable<T> {
+ pub fn is_retry(&self) -> bool {
+ matches!(*self, Retryable::Retry)
+ }
+
+ pub fn is_error(&self) -> bool {
+ !self.is_retry()
+ }
+}
+
+impl<T> From<T> for Retryable<T> {
+ 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<u8>, Self::AdditionalInfo), HIDError>;
+
+ /// Deserializes a response from FIDO v1.x / CTAP1 / U2Fv2 format.
+ fn handle_response_ctap1<Dev: FidoDevice>(
+ &self,
+ dev: &mut Dev,
+ status: Result<(), ApduErrorStatus>,
+ input: &[u8],
+ add_info: &Self::AdditionalInfo,
+ ) -> Result<Self::Output, Retryable<HIDError>>;
+
+ fn send_to_virtual_device<Dev: VirtualFidoDevice>(
+ &self,
+ dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError>;
+}
+
+pub trait RequestCtap2: fmt::Debug {
+ type Output: CtapResponse;
+
+ fn command(&self) -> Command;
+
+ fn wire_format(&self) -> Result<Vec<u8>, HIDError>;
+
+ fn handle_response_ctap2<Dev: FidoDevice>(
+ &self,
+ dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError>;
+
+ fn send_to_virtual_device<Dev: VirtualFidoDevice>(
+ &self,
+ dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError>;
+}
+
+// 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<PinUvAuthToken> {
+ 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<PinUvAuthToken>,
+ ) -> Result<(), AuthenticatorError>;
+ fn get_pin_uv_auth_param(&self) -> Option<&PinUvAuthParam>;
+ fn set_uv_option(&mut self, uv: Option<bool>);
+ 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<D: FidoDevice>(
+ 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<u8> 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<StatusCode> 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<Value>),
+ 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<Vec<u8>, HIDError> {
+ Ok(Vec::new())
+ }
+
+ fn handle_response_ctap2<Dev: FidoDevice>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError> {
+ 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<Dev: VirtualFidoDevice>(
+ &self,
+ dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ 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<Vec<u8>, HIDError> {
+ Ok(Vec::new())
+ }
+
+ fn handle_response_ctap2<Dev: FidoDevice>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError> {
+ 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<Dev: VirtualFidoDevice>(
+ &self,
+ dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ 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<u8>,
+ status: &Sender<StatusUpdate>,
+) -> Result<Pin, AuthenticatorError> {
+ 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<Dev: FidoDevice, T: PinUvAuthCommand + RequestCtap2>(
+ cmd: &mut T,
+ dev: &mut Dev,
+ permission: PinUvAuthTokenPermission,
+ skip_uv: bool,
+ uv_req: UserVerificationRequirement,
+ alive: &dyn Fn() -> bool,
+ pin: &Option<Pin>,
+) -> Result<PinUvAuthResult, AuthenticatorError> {
+ // 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<Dev: FidoDevice, T: PinUvAuthCommand + RequestCtap2>(
+ cmd: &mut T,
+ dev: &mut Dev,
+ mut skip_uv: bool,
+ permission: PinUvAuthTokenPermission,
+ uv_req: UserVerificationRequirement,
+ status: &Sender<StatusUpdate>,
+ alive: &dyn Fn() -> bool,
+ pin: &mut Option<Pin>,
+) -> Result<PinUvAuthResult, AuthenticatorError> {
+ 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: FidoDevice>(
+ dev: &mut Dev,
+ args: RegisterArgs,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::RegisterResult>>,
+ 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: FidoDevice>(
+ dev: &mut Dev,
+ args: SignArgs,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::SignResult>>,
+ 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<T: From<ResetResult>>(
+ dev: &mut Device,
+ selector: Sender<DeviceSelectorEvent>,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<T>>,
+ 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<T: From<()>>(
+ dev: &mut Device,
+ mut current_pin: Option<Pin>,
+ new_pin: Pin,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<T>>,
+ 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<PinUvAuthResult>,
+ command: BioEnrollmentCmd,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ManageResult>>,
+ 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<PinUvAuthResult>,
+ command: CredManagementCmd,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ManageResult>>,
+ 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<PinUvAuthResult>,
+ cfg_subcommand: AuthConfigCommand,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ManageResult>>,
+ 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<u8>, 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<Dev: FidoDevice>(
+ &self,
+ _dev: &mut Dev,
+ status: Result<(), ApduErrorStatus>,
+ _input: &[u8],
+ _add_info: &Self::AdditionalInfo,
+ ) -> Result<Self::Output, Retryable<HIDError>> {
+ // 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<Dev: VirtualFidoDevice>(
+ &self,
+ dev: &mut Dev,
+ ) -> Result<Self::Output, HIDError> {
+ 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: FidoDevice>(
+ dev: &mut Dev,
+ cred_list: &[PublicKeyCredentialDescriptor],
+ rp: &RelyingParty,
+ client_data_hash: &ClientDataHash,
+) -> Option<PublicKeyCredentialDescriptor> {
+ 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: FidoDevice>(
+ dev: &mut Dev,
+ cred_list: &[PublicKeyCredentialDescriptor],
+ rp: &RelyingParty,
+ pin_uv_auth_token: Option<PinUvAuthToken>,
+) -> Result<Vec<PublicKeyCredentialDescriptor>, 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: FidoDevice>(
+ dev: &mut Dev,
+ cred_list: &[PublicKeyCredentialDescriptor],
+ rp: &RelyingParty,
+ client_data_hash: &ClientDataHash,
+) -> Vec<PublicKeyCredentialDescriptor> {
+ 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<RpIdHash, AuthenticatorError> {
+ 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<String>,
+}
+
+impl RelyingParty {
+ pub fn from<S>(id: S) -> Self
+ where
+ S: Into<String>,
+ {
+ 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<u8>,
+ pub name: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none", rename = "displayName")]
+ pub display_name: Option<String>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct PublicKeyCredentialParameters {
+ pub alg: COSEAlgorithm,
+}
+
+impl TryFrom<i32> for PublicKeyCredentialParameters {
+ type Error = AuthenticatorError;
+ fn try_from(arg: i32) -> Result<Self, Self::Error> {
+ let alg = COSEAlgorithm::try_from(arg as i64)?;
+ Ok(PublicKeyCredentialParameters { alg })
+ }
+}
+
+impl Serialize for PublicKeyCredentialParameters {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ 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<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct PublicKeyCredentialParametersVisitor;
+
+ impl<'de> Visitor<'de> for PublicKeyCredentialParametersVisitor {
+ type Value = PublicKeyCredentialParameters;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a map")
+ }
+
+ fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ where
+ M: MapAccess<'de>,
+ {
+ let mut found_type = false;
+ let mut alg = None;
+ while let Some(key) = map.next_key()? {
+ match key {
+ "alg" => {
+ if alg.is_some() {
+ return Err(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<AuthenticatorTransports> for Vec<Transport> {
+ 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<u8>;
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct PublicKeyCredentialDescriptor {
+ pub id: PublicKeyCredentialId,
+ pub transports: Vec<Transport>,
+}
+
+impl Serialize for PublicKeyCredentialDescriptor {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ 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<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct PublicKeyCredentialDescriptorVisitor;
+
+ impl<'de> Visitor<'de> for PublicKeyCredentialDescriptorVisitor {
+ type Value = PublicKeyCredentialDescriptor;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a map")
+ }
+
+ fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ where
+ M: MapAccess<'de>,
+ {
+ let mut found_type = false;
+ let mut id = None;
+ let mut transports = None;
+ while let Some(key) = map.next_key()? {
+ match key {
+ "id" => {
+ if id.is_some() {
+ return Err(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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_u64(*self as u64)
+ }
+}
+
+impl<'de> Deserialize<'de> for CredentialProtectionPolicy {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ 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<E>(self, v: u64) -> Result<Self::Value, E>
+ 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<String>,
+ pub cred_props: Option<bool>,
+ pub credential_protection_policy: Option<CredentialProtectionPolicy>,
+ pub enforce_credential_protection_policy: Option<bool>,
+ pub hmac_create_secret: Option<bool>,
+ pub min_pin_length: Option<bool>,
+}
+
+#[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<bool>,
+ pub cred_props: Option<CredentialProperties>,
+ pub hmac_create_secret: Option<bool>,
+}
+
+#[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<E: de::Error>(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<T, E>
+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<R: Read, E: de::Error>(data: &mut R) -> Result<u32, E> {
+ 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<R: Read, E: de::Error>(data: &mut R) -> Result<u16, E> {
+ 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<R: Read, E: de::Error>(data: &mut R) -> Result<u8, E> {
+ match data.bytes().next() {
+ Some(Ok(s)) => Ok(s),
+ _ => Err(serde_parse_err("u8")),
+ }
+}