use std::collections::HashMap;
use serde_bytes::ByteBuf;
use serde_indexed::DeserializeIndexed;
#[cfg(test)]
use serde_indexed::SerializeIndexed;
use tracing::debug;
use super::{Ctap2CredentialType, Ctap2UserVerificationOperation};
#[cfg_attr(test, derive(SerializeIndexed))]
#[derive(Debug, Clone, DeserializeIndexed, Default)]
pub struct Ctap2GetInfoResponse {
#[serde(index = 0x01)]
pub versions: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x02)]
pub extensions: Option<Vec<String>>,
#[serde(index = 0x03)]
pub aaguid: ByteBuf,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x04)]
pub options: Option<HashMap<String, bool>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x05)]
pub max_msg_size: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x06)]
pub pin_auth_protos: Option<Vec<u32>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x07)]
pub max_credential_count: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x08)]
pub max_credential_id_length: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x09)]
pub transports: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x0A)]
pub algorithms: Option<Vec<Ctap2CredentialType>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x0B)]
pub max_blob_array: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x0C)]
pub force_pin_change: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x0D)]
pub min_pin_length: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x0E)]
pub firmware_version: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x0F)]
pub max_cred_blob_length: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x10)]
pub max_rpids_for_setminpinlength: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x11)]
pub preferred_platform_uv_attempts: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x12)]
pub uv_modality: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x13)]
pub certifications: Option<HashMap<String, u32>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x14)]
pub remaining_discoverable_creds: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x15)]
pub vendor_proto_config_cmds: Option<Vec<u32>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x16)]
pub attestation_formats: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x17)]
pub uv_count_since_last_pin_entry: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x18)]
pub long_touch_for_reset: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x19)]
pub enc_identifier: Option<ByteBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x1A)]
pub transports_for_reset: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x1B)]
pub pin_complexity_policy: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x1C)]
pub pin_complexity_policy_url: Option<ByteBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x1D)]
pub max_pin_length: Option<u32>,
}
impl Ctap2GetInfoResponse {
pub fn option_exists(&self, name: &str) -> bool {
let Some(options) = self.options.as_ref() else {
return false;
};
options.get(name).is_some()
}
pub fn option_enabled(&self, name: &str) -> bool {
let Some(options) = self.options.as_ref() else {
return false;
};
options.get(name) == Some(&true)
}
pub fn supports_fido_2_1(&self) -> bool {
self.versions.iter().any(|v| v == "FIDO_2_1")
}
pub fn supports_extension(&self, name: &str) -> bool {
self.extensions
.as_ref()
.is_some_and(|exts| exts.iter().any(|e| e == name))
}
pub fn supports_credential_management(&self) -> bool {
self.option_enabled("credMgmt") || self.option_enabled("credentialMgmtPreview")
}
pub fn supports_bio_enrollment(&self) -> bool {
if let Some(options) = &self.options {
return options.get("bioEnroll").is_some()
|| options.get("userVerificationMgmtPreview").is_some();
}
false
}
pub fn has_bio_enrollments(&self) -> bool {
if let Some(options) = &self.options {
return options.get("bioEnroll") == Some(&true)
|| options.get("userVerificationMgmtPreview") == Some(&true);
}
false
}
pub fn is_uv_protected(&self) -> bool {
self.option_enabled("uv") || self.option_enabled("clientPin") ||
(self.option_enabled("pinUvAuthToken") && self.option_enabled("uv"))
}
pub fn can_establish_shared_secret(&self) -> bool {
self.option_exists("clientPin")
|| (self.option_exists("uv") && self.option_enabled("pinUvAuthToken"))
}
pub fn uv_operation(&self, uv_blocked: bool) -> Option<Ctap2UserVerificationOperation> {
if self.option_enabled("uv") && !uv_blocked {
if self.option_enabled("pinUvAuthToken") {
debug!("getPinUvAuthTokenUsingUvWithPermissions");
Some(Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingUvWithPermissions)
} else {
debug!("Deprecated FIDO 2.0 behaviour: populating 'uv' flag");
Some(Ctap2UserVerificationOperation::LegacyUv)
}
} else {
if self.option_exists("clientPin") && !self.option_enabled("clientPin") {
return Some(Ctap2UserVerificationOperation::OnlyForSharedSecret);
}
if !self.option_enabled("clientPin")
&& self.option_exists("uv")
&& self.option_enabled("pinUvAuthToken")
{
return Some(Ctap2UserVerificationOperation::OnlyForSharedSecret);
}
if self.option_enabled("pinUvAuthToken") {
if !self.option_enabled("clientPin") {
debug!(
"Device advertises pinUvAuthToken without clientPin; \
falling back to OnlyForSharedSecret"
);
return Some(Ctap2UserVerificationOperation::OnlyForSharedSecret);
}
debug!("getPinUvAuthTokenUsingPinWithPermissions");
Some(Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingPinWithPermissions)
} else if self.option_enabled("clientPin") {
debug!("getPinToken");
Some(Ctap2UserVerificationOperation::GetPinToken)
} else {
debug!("No UV and no PIN (e.g. maybe UV was blocked and no PIN available)");
None
}
}
}
}
#[cfg(test)]
mod test {
use std::collections::HashMap;
use crate::proto::ctap2::Ctap2UserVerificationOperation;
use super::Ctap2GetInfoResponse;
fn create_info(options: &[(&str, bool)]) -> Ctap2GetInfoResponse {
let mut info = Ctap2GetInfoResponse::default();
let mut input = HashMap::new();
for (key, val) in options {
input.insert(key.to_string(), *val);
}
info.options = Some(input);
info
}
#[test]
fn device_no_options() {
let info = Ctap2GetInfoResponse::default();
assert!(!info.supports_fido_2_1());
assert!(!info.supports_credential_management());
assert!(!info.supports_bio_enrollment());
assert!(!info.is_uv_protected());
assert!(!info.can_establish_shared_secret());
assert_eq!(info.uv_operation(false), None);
assert_eq!(info.uv_operation(true), None);
}
#[test]
fn device_empty_options() {
let info = create_info(&[]);
assert!(!info.supports_fido_2_1());
assert!(!info.supports_credential_management());
assert!(!info.supports_bio_enrollment());
assert!(!info.is_uv_protected());
assert!(!info.can_establish_shared_secret());
assert_eq!(info.uv_operation(false), None);
assert_eq!(info.uv_operation(true), None);
}
#[test]
fn device_legacy_uv() {
let info = create_info(&[("uv", true)]);
assert!(info.is_uv_protected());
assert!(!info.can_establish_shared_secret());
assert_eq!(
info.uv_operation(false),
Some(Ctap2UserVerificationOperation::LegacyUv)
);
assert_eq!(info.uv_operation(true), None);
}
#[test]
fn device_legacy_uv_but_not_set() {
let info = create_info(&[("uv", false)]);
assert!(!info.is_uv_protected());
assert!(!info.can_establish_shared_secret());
assert_eq!(info.uv_operation(false), None);
assert_eq!(info.uv_operation(true), None);
}
#[test]
fn device_ctap20_pin_only() {
let info = create_info(&[("clientPin", true)]);
assert!(info.is_uv_protected());
assert!(info.can_establish_shared_secret());
assert_eq!(
info.uv_operation(false),
Some(Ctap2UserVerificationOperation::GetPinToken)
);
assert_eq!(
info.uv_operation(true),
Some(Ctap2UserVerificationOperation::GetPinToken)
);
}
#[test]
fn device_ctap20_pin_only_but_not_set() {
let info = create_info(&[("clientPin", false)]);
assert!(!info.is_uv_protected());
assert!(info.can_establish_shared_secret());
assert_eq!(
info.uv_operation(false),
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
);
assert_eq!(
info.uv_operation(true),
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
);
}
#[test]
fn device_ctap20_pin_and_uv() {
let info = create_info(&[("clientPin", true), ("uv", true)]);
assert!(info.is_uv_protected());
assert!(info.can_establish_shared_secret());
assert_eq!(
info.uv_operation(false),
Some(Ctap2UserVerificationOperation::LegacyUv)
);
assert_eq!(
info.uv_operation(true),
Some(Ctap2UserVerificationOperation::GetPinToken)
);
}
#[test]
fn device_ctap20_pin_and_uv_but_only_pin_set() {
let info = create_info(&[("clientPin", true), ("uv", false)]);
assert!(info.is_uv_protected());
assert!(info.can_establish_shared_secret());
assert_eq!(
info.uv_operation(false),
Some(Ctap2UserVerificationOperation::GetPinToken)
);
assert_eq!(
info.uv_operation(true),
Some(Ctap2UserVerificationOperation::GetPinToken)
);
}
#[test]
fn device_ctap20_pin_and_uv_but_only_uv_set() {
let info = create_info(&[("clientPin", false), ("uv", true)]);
assert!(info.is_uv_protected());
assert!(info.can_establish_shared_secret());
assert_eq!(
info.uv_operation(false),
Some(Ctap2UserVerificationOperation::LegacyUv)
);
assert_eq!(
info.uv_operation(true),
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
);
}
#[test]
fn device_ctap20_pin_and_uv_but_neither_set() {
let info = create_info(&[("clientPin", false), ("uv", false)]);
assert!(!info.is_uv_protected());
assert!(info.can_establish_shared_secret());
assert_eq!(
info.uv_operation(false),
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
);
assert_eq!(
info.uv_operation(true),
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
);
}
#[test]
fn device_ctap21_pin_only() {
let info = create_info(&[("clientPin", true), ("pinUvAuthToken", true)]);
assert!(info.is_uv_protected());
assert!(info.can_establish_shared_secret());
assert_eq!(
info.uv_operation(false),
Some(Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingPinWithPermissions)
);
assert_eq!(
info.uv_operation(true),
Some(Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingPinWithPermissions)
);
}
#[test]
fn device_ctap21_uv_only() {
let info = create_info(&[("uv", true), ("pinUvAuthToken", true)]);
assert!(info.is_uv_protected());
assert!(info.can_establish_shared_secret());
assert_eq!(
info.uv_operation(false),
Some(Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingUvWithPermissions)
);
assert_eq!(
info.uv_operation(true),
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
);
}
#[test]
fn device_ctap21_pin_and_uv() {
let info = create_info(&[("clientPin", true), ("uv", true), ("pinUvAuthToken", true)]);
assert!(info.is_uv_protected());
assert!(info.can_establish_shared_secret());
assert_eq!(
info.uv_operation(false),
Some(Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingUvWithPermissions)
);
assert_eq!(
info.uv_operation(true),
Some(Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingPinWithPermissions)
);
}
#[test]
fn device_ctap21_pin_only_but_not_set() {
let info = create_info(&[("clientPin", false), ("pinUvAuthToken", true)]);
assert!(!info.is_uv_protected());
assert!(info.can_establish_shared_secret());
assert_eq!(
info.uv_operation(false),
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
);
assert_eq!(
info.uv_operation(true),
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
);
}
#[test]
fn device_ctap21_uv_only_but_not_set() {
let info = create_info(&[("uv", false), ("pinUvAuthToken", true)]);
assert!(!info.is_uv_protected());
assert!(info.can_establish_shared_secret());
assert_eq!(
info.uv_operation(false),
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
);
assert_eq!(
info.uv_operation(true),
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
);
}
#[test]
fn device_ctap21_pin_and_uv_but_only_pin_set() {
let info = create_info(&[("clientPin", true), ("uv", false), ("pinUvAuthToken", true)]);
assert!(info.is_uv_protected());
assert!(info.can_establish_shared_secret());
assert_eq!(
info.uv_operation(false),
Some(Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingPinWithPermissions)
);
assert_eq!(
info.uv_operation(true),
Some(Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingPinWithPermissions)
);
}
#[test]
fn device_ctap21_pin_and_uv_but_only_uv_set() {
let info = create_info(&[("clientPin", false), ("uv", true), ("pinUvAuthToken", true)]);
assert!(info.is_uv_protected());
assert!(info.can_establish_shared_secret());
assert_eq!(
info.uv_operation(false),
Some(Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingUvWithPermissions)
);
assert_eq!(
info.uv_operation(true),
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
);
}
#[test]
fn device_ctap21_pin_and_uv_but_neither_set() {
let info = create_info(&[
("clientPin", false),
("uv", false),
("pinUvAuthToken", true),
]);
assert!(!info.is_uv_protected());
assert!(info.can_establish_shared_secret());
assert_eq!(
info.uv_operation(false),
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
);
assert_eq!(
info.uv_operation(true),
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
);
}
#[test]
fn device_pin_uv_auth_token_without_client_pin_does_not_panic() {
let info = create_info(&[("pinUvAuthToken", true)]);
assert_eq!(
info.uv_operation(false),
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
);
assert_eq!(
info.uv_operation(true),
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
);
}
}