summaryrefslogtreecommitdiffstats
path: root/third_party/rust/authenticator/src/ctap2/commands
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/rust/authenticator/src/ctap2/commands')
-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
12 files changed, 6700 insertions, 0 deletions
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),
+ }
+ }
+}