#[cfg(all(unix, feature = "libusb"))]
use anyhow::{bail, Context, Result};
#[cfg(all(unix, feature = "libusb"))]
use ring::rand::SecureRandom;
#[cfg(all(unix, feature = "libusb"))]
use ring::signature::{EcdsaKeyPair, KeyPair, ECDSA_P256_SHA256_FIXED_SIGNING};
#[cfg(all(unix, feature = "libusb"))]
use serde_cbor::Value as CborValue;
#[cfg(all(unix, feature = "libusb"))]
use std::collections::BTreeMap;
#[cfg(all(unix, feature = "libusb"))]
use tracing::{debug, info, warn};
#[cfg(all(unix, feature = "libusb"))]
use crate::server::usb::fido2::approval::{ApprovalDetails, OperationType, UserPresence};
#[cfg(all(unix, feature = "libusb"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Ctap2Command {
MakeCredential = 0x01,
GetAssertion = 0x02,
GetInfo = 0x04,
ClientPin = 0x06,
Reset = 0x07,
GetNextAssertion = 0x08,
}
#[cfg(all(unix, feature = "libusb"))]
impl Ctap2Command {
pub fn from_u8(value: u8) -> Option<Self> {
match value {
0x01 => Some(Self::MakeCredential),
0x02 => Some(Self::GetAssertion),
0x04 => Some(Self::GetInfo),
0x06 => Some(Self::ClientPin),
0x07 => Some(Self::Reset),
0x08 => Some(Self::GetNextAssertion),
_ => None,
}
}
}
#[cfg(all(unix, feature = "libusb"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Ctap2Status {
Success = 0x00,
InvalidCommand = 0x01,
InvalidParameter = 0x02,
InvalidLength = 0x03,
InvalidSeq = 0x04,
Timeout = 0x05,
ChannelBusy = 0x06,
LockRequired = 0x0A,
InvalidChannel = 0x0B,
CborUnexpectedType = 0x11,
InvalidCbor = 0x12,
MissingParameter = 0x14,
LimitExceeded = 0x15,
UnsupportedExtension = 0x16,
CredentialExcluded = 0x19,
Processing = 0x21,
InvalidCredential = 0x22,
UserActionPending = 0x23,
OperationPending = 0x24,
NoOperations = 0x25,
UnsupportedAlgorithm = 0x26,
OperationDenied = 0x27,
KeyStoreFull = 0x28,
NoCredentials = 0x2E,
UserActionTimeout = 0x2F,
NotAllowed = 0x30,
PinInvalid = 0x31,
Other = 0x7F,
}
#[cfg(all(unix, feature = "libusb"))]
pub struct Ctap2Request {
pub command: Ctap2Command,
pub cbor_params: Option<CborValue>,
}
#[cfg(all(unix, feature = "libusb"))]
impl Ctap2Request {
pub fn parse(data: &[u8]) -> Result<Self> {
if data.is_empty() {
bail!("Empty CTAP2 request");
}
let command = Ctap2Command::from_u8(data[0]).context("Invalid CTAP2 command")?;
let cbor_params = if data.len() > 1 {
Some(serde_cbor::from_slice(&data[1..])?)
} else {
None
};
Ok(Self {
command,
cbor_params,
})
}
}
#[cfg(all(unix, feature = "libusb"))]
pub struct Ctap2Response {
pub status: Ctap2Status,
pub cbor_data: Option<CborValue>,
}
#[cfg(all(unix, feature = "libusb"))]
impl Ctap2Response {
pub fn success(cbor_data: CborValue) -> Self {
Self {
status: Ctap2Status::Success,
cbor_data: Some(cbor_data),
}
}
pub fn error(status: Ctap2Status) -> Self {
Self {
status,
cbor_data: None,
}
}
pub fn to_bytes(self) -> Vec<u8> {
let mut response = vec![self.status as u8];
if let Some(cbor) = self.cbor_data {
if let Ok(encoded) = serde_cbor::to_vec(&cbor) {
response.extend_from_slice(&encoded);
}
}
response
}
}
#[cfg(all(unix, feature = "libusb"))]
#[derive(Clone)]
pub struct Ctap2Credential {
credential_id: Vec<u8>,
private_key: Vec<u8>,
public_key_cose: Vec<u8>,
rp_id: String,
#[allow(dead_code)]
user_handle: Vec<u8>,
#[allow(dead_code)]
user_name: String,
counter: u32,
#[allow(dead_code)]
is_resident: bool,
}
#[cfg(all(unix, feature = "libusb"))]
#[derive(Clone)]
struct PinState {
pin_hash: Option<Vec<u8>>,
retries: u8,
verified: bool,
}
#[cfg(all(unix, feature = "libusb"))]
impl PinState {
fn new() -> Self {
Self {
pin_hash: None,
retries: 8,
verified: false,
}
}
fn set_pin(&mut self, pin: &str) -> Result<()> {
if pin.len() < 4 || pin.len() > 63 {
bail!("PIN must be 4-63 characters");
}
let hash = ring::digest::digest(&ring::digest::SHA256, pin.as_bytes());
self.pin_hash = Some(hash.as_ref().to_vec());
self.retries = 8;
self.verified = false;
info!("PIN set successfully");
Ok(())
}
fn verify_pin(&mut self, pin: &str) -> Result<bool> {
if self.retries == 0 {
bail!("PIN blocked - too many failed attempts");
}
let Some(ref stored_hash) = self.pin_hash else {
bail!("PIN not set");
};
let hash = ring::digest::digest(&ring::digest::SHA256, pin.as_bytes());
if hash.as_ref() == stored_hash.as_slice() {
self.verified = true;
self.retries = 8; info!("PIN verified successfully");
Ok(true)
} else {
self.retries -= 1;
self.verified = false;
warn!(
"PIN verification failed - {} retries remaining",
self.retries
);
Ok(false)
}
}
fn is_set(&self) -> bool {
self.pin_hash.is_some()
}
fn is_verified(&self) -> bool {
self.verified
}
fn reset(&mut self) {
self.verified = false;
}
}
#[cfg(all(unix, feature = "libusb"))]
pub struct Ctap2CredentialStore {
credentials: std::collections::HashMap<String, Vec<Ctap2Credential>>,
resident_credentials: Vec<Ctap2Credential>,
rng: ring::rand::SystemRandom,
pin_state: PinState,
}
#[cfg(all(unix, feature = "libusb"))]
impl Default for Ctap2CredentialStore {
fn default() -> Self {
Self::new()
}
}
#[cfg(all(unix, feature = "libusb"))]
impl Ctap2CredentialStore {
pub fn new() -> Self {
Self {
credentials: std::collections::HashMap::new(),
resident_credentials: Vec::new(),
rng: ring::rand::SystemRandom::new(),
pin_state: PinState::new(),
}
}
pub fn make_credential(
&mut self,
rp_id: &str,
user_handle: &[u8],
user_name: &str,
require_resident_key: bool,
require_user_verification: bool,
) -> Result<Ctap2Credential> {
if require_user_verification && !self.pin_state.is_verified() {
bail!("User verification required but PIN not verified");
}
let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &self.rng)?;
let key_pair =
EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &self.rng)?;
let public_key_bytes = key_pair.public_key().as_ref();
let public_key_cose = Self::encode_cose_key(public_key_bytes)?;
let mut credential_id = vec![0u8; 32];
self.rng.fill(&mut credential_id)?;
let credential = Ctap2Credential {
credential_id: credential_id.clone(),
private_key: pkcs8.as_ref().to_vec(),
public_key_cose,
rp_id: rp_id.to_string(),
user_handle: user_handle.to_vec(),
user_name: user_name.to_string(),
counter: 0,
is_resident: require_resident_key,
};
self.credentials
.entry(rp_id.to_string())
.or_default()
.push(credential.clone());
if require_resident_key {
self.resident_credentials.push(credential.clone());
info!(
"Created resident credential for RP '{}', user '{}'",
rp_id, user_name
);
} else {
info!(
"Created credential for RP '{}', user '{}'",
rp_id, user_name
);
}
Ok(credential)
}
pub fn find_credentials(
&mut self,
rp_id: &str,
credential_id: Option<&[u8]>,
) -> Option<&mut Ctap2Credential> {
if credential_id.is_none() {
if let Some(cred) = self
.resident_credentials
.iter_mut()
.find(|c| c.rp_id == rp_id)
{
return Some(cred);
}
}
let creds = self.credentials.get_mut(rp_id)?;
if let Some(cred_id) = credential_id {
creds.iter_mut().find(|c| c.credential_id == cred_id)
} else {
creds.first_mut()
}
}
pub fn credential_count(&self, rp_id: &str) -> usize {
self.credentials.get(rp_id).map_or(0, |c| c.len())
}
pub fn first_user_name(&self, rp_id: &str) -> Option<String> {
self.credentials
.get(rp_id)
.and_then(|c| c.first())
.map(|c| c.user_name.clone())
}
pub fn describe_credentials(&self) -> Vec<(String, String, bool, u32)> {
let mut out: Vec<(String, String, bool, u32)> = self
.credentials
.values()
.flatten()
.map(|c| {
(
c.rp_id.clone(),
c.user_name.clone(),
c.is_resident,
c.counter,
)
})
.collect();
out.sort();
out
}
pub fn delete_credentials(&mut self, rp_id: &str) -> usize {
let removed = self.credentials.remove(rp_id).map_or(0, |c| c.len());
self.resident_credentials.retain(|c| c.rp_id != rp_id);
removed
}
pub fn get_resident_credentials(&self, rp_id: &str) -> Vec<&Ctap2Credential> {
self.resident_credentials
.iter()
.filter(|c| c.rp_id == rp_id)
.collect()
}
pub fn set_pin(&mut self, pin: &str) -> Result<()> {
self.pin_state.set_pin(pin)
}
pub fn verify_pin(&mut self, pin: &str) -> Result<bool> {
self.pin_state.verify_pin(pin)
}
pub fn has_pin(&self) -> bool {
self.pin_state.is_set()
}
pub fn pin_verified(&self) -> bool {
self.pin_state.is_verified()
}
pub fn pin_retries(&self) -> u8 {
self.pin_state.retries
}
pub fn reset_pin_verification(&mut self) {
self.pin_state.reset()
}
fn encode_cose_key(public_key: &[u8]) -> Result<Vec<u8>> {
if public_key.len() != 65 || public_key[0] != 0x04 {
bail!("Invalid public key format");
}
let x = &public_key[1..33];
let y = &public_key[33..65];
let mut cose_key = BTreeMap::new();
cose_key.insert(CborValue::Integer(1), CborValue::Integer(2)); cose_key.insert(CborValue::Integer(3), CborValue::Integer(-7)); cose_key.insert(CborValue::Integer(-1), CborValue::Integer(1)); cose_key.insert(CborValue::Integer(-2), CborValue::Bytes(x.to_vec())); cose_key.insert(CborValue::Integer(-3), CborValue::Bytes(y.to_vec()));
Ok(serde_cbor::to_vec(&CborValue::Map(cose_key))?)
}
}
#[cfg(all(unix, feature = "libusb"))]
pub enum Ctap2Outcome {
Response(Vec<u8>),
NeedsApproval(ApprovalDetails),
}
#[cfg(all(unix, feature = "libusb"))]
enum Step {
Done(Ctap2Response),
Ask(ApprovalDetails),
}
#[cfg(all(unix, feature = "libusb"))]
pub struct Ctap2Handler {
aaguid: [u8; 16],
store: Ctap2CredentialStore,
}
#[cfg(all(unix, feature = "libusb"))]
impl Ctap2Handler {
pub fn new() -> Self {
let aaguid = [
0x4e, 0x65, 0x74, 0x47, 0x65, 0x74, 0x2d, 0x46, 0x49, 0x44, 0x4f, 0x32, 0x00, 0x00, 0x00, 0x01, ];
Self {
aaguid,
store: Ctap2CredentialStore::new(),
}
}
pub fn store(&self) -> &Ctap2CredentialStore {
&self.store
}
pub fn store_mut(&mut self) -> &mut Ctap2CredentialStore {
&mut self.store
}
pub fn process_command(&mut self, data: &[u8], presence: UserPresence) -> Ctap2Outcome {
let request = match Ctap2Request::parse(data) {
Ok(req) => req,
Err(e) => {
warn!("Failed to parse CTAP2 request: {}", e);
return Ctap2Outcome::Response(
Ctap2Response::error(Ctap2Status::InvalidCbor).to_bytes(),
);
}
};
debug!(
"CTAP2 command: {:?} (presence {:?})",
request.command, presence
);
let step = match request.command {
Ctap2Command::GetInfo => Step::Done(self.handle_get_info()),
Ctap2Command::MakeCredential => {
self.handle_make_credential(request.cbor_params, presence)
}
Ctap2Command::GetAssertion => self.handle_get_assertion(request.cbor_params, presence),
Ctap2Command::ClientPin => Step::Done(self.handle_client_pin(request.cbor_params)),
Ctap2Command::GetNextAssertion => Step::Done(self.handle_get_next_assertion()),
Ctap2Command::Reset => Step::Done(self.handle_reset()),
};
match step {
Step::Done(response) => Ctap2Outcome::Response(response.to_bytes()),
Step::Ask(details) => Ctap2Outcome::NeedsApproval(details),
}
}
fn handle_get_info(&self) -> Ctap2Response {
debug!("CTAP2 GetInfo");
let mut info = BTreeMap::new();
let versions = CborValue::Array(vec![
CborValue::Text("U2F_V2".to_string()),
CborValue::Text("FIDO_2_0".to_string()),
]);
info.insert(CborValue::Integer(0x01), versions);
info.insert(
CborValue::Integer(0x03),
CborValue::Bytes(self.aaguid.to_vec()),
);
let mut options = BTreeMap::new();
options.insert(CborValue::Text("rk".to_string()), CborValue::Bool(true)); options.insert(CborValue::Text("up".to_string()), CborValue::Bool(true)); options.insert(CborValue::Text("uv".to_string()), CborValue::Bool(true)); options.insert(CborValue::Text("plat".to_string()), CborValue::Bool(false)); options.insert(
CborValue::Text("clientPin".to_string()),
CborValue::Bool(self.store.has_pin()),
); info.insert(CborValue::Integer(0x04), CborValue::Map(options));
info.insert(CborValue::Integer(0x05), CborValue::Integer(1200));
info.insert(
CborValue::Integer(0x06),
CborValue::Array(vec![CborValue::Integer(1)]),
);
Ctap2Response::success(CborValue::Map(info))
}
fn handle_make_credential(
&mut self,
params: Option<CborValue>,
presence: UserPresence,
) -> Step {
debug!("CTAP2 MakeCredential");
let params = match params {
Some(CborValue::Map(m)) => m,
_ => return Step::Done(Ctap2Response::error(Ctap2Status::MissingParameter)),
};
let _client_data_hash = match params.get(&CborValue::Integer(0x01)) {
Some(CborValue::Bytes(b)) if b.len() == 32 => b.clone(),
_ => return Step::Done(Ctap2Response::error(Ctap2Status::MissingParameter)),
};
let rp = match params.get(&CborValue::Integer(0x02)) {
Some(CborValue::Map(m)) => m,
_ => return Step::Done(Ctap2Response::error(Ctap2Status::MissingParameter)),
};
let rp_id = match rp.get(&CborValue::Text("id".to_string())) {
Some(CborValue::Text(s)) => s.clone(),
_ => return Step::Done(Ctap2Response::error(Ctap2Status::MissingParameter)),
};
let user = match params.get(&CborValue::Integer(0x03)) {
Some(CborValue::Map(m)) => m,
_ => return Step::Done(Ctap2Response::error(Ctap2Status::MissingParameter)),
};
let user_handle = match user.get(&CborValue::Text("id".to_string())) {
Some(CborValue::Bytes(b)) => b.clone(),
_ => return Step::Done(Ctap2Response::error(Ctap2Status::MissingParameter)),
};
let user_name = match user.get(&CborValue::Text("name".to_string())) {
Some(CborValue::Text(s)) => s.clone(),
_ => "user".to_string(),
};
let options = params.get(&CborValue::Integer(0x07));
let require_resident_key = options
.and_then(|o| match o {
CborValue::Map(m) => Some(m),
_ => None,
})
.and_then(|m| m.get(&CborValue::Text("rk".to_string())))
.and_then(|v| match v {
CborValue::Bool(b) => Some(*b),
_ => None,
})
.unwrap_or(false);
let require_user_verification = options
.and_then(|o| match o {
CborValue::Map(m) => Some(m),
_ => None,
})
.and_then(|m| m.get(&CborValue::Text("uv".to_string())))
.and_then(|v| match v {
CborValue::Bool(b) => Some(*b),
_ => None,
})
.unwrap_or(false);
info!(
"MakeCredential for RP '{}', user '{}' (rk={}, uv={})",
rp_id, user_name, require_resident_key, require_user_verification
);
match presence {
UserPresence::Ask => {
return Step::Ask(ApprovalDetails {
operation: OperationType::Register,
rp_id: rp_id.clone(),
user_name: Some(user_name.clone()),
credential_count: self.store.credential_count(&rp_id),
});
}
UserPresence::Denied => {
warn!("MakeCredential denied for RP '{}'", rp_id);
return Step::Done(Ctap2Response::error(Ctap2Status::OperationDenied));
}
UserPresence::Approved => {
info!("MakeCredential approved for RP '{}'", rp_id);
}
}
let credential = match self.store.make_credential(
&rp_id,
&user_handle,
&user_name,
require_resident_key,
require_user_verification,
) {
Ok(c) => c,
Err(e) => {
warn!("Failed to create credential: {}", e);
if e.to_string().contains("PIN not verified") {
return Step::Done(Ctap2Response::error(Ctap2Status::PinInvalid));
}
return Step::Done(Ctap2Response::error(Ctap2Status::Other));
}
};
let att_stmt: BTreeMap<CborValue, CborValue> = BTreeMap::new();
let mut auth_data = Vec::new();
auth_data.extend_from_slice(
ring::digest::digest(&ring::digest::SHA256, rp_id.as_bytes()).as_ref(),
);
let flags = 0x41
| if self.store.pin_verified() {
0x04
} else {
0x00
}; auth_data.push(flags);
auth_data.extend_from_slice(&0u32.to_be_bytes());
auth_data.extend_from_slice(&self.aaguid);
auth_data.extend_from_slice(&(credential.credential_id.len() as u16).to_be_bytes());
auth_data.extend_from_slice(&credential.credential_id);
auth_data.extend_from_slice(&credential.public_key_cose);
let mut response = BTreeMap::new();
response.insert(
CborValue::Integer(0x01),
CborValue::Text("none".to_string()),
);
response.insert(CborValue::Integer(0x02), CborValue::Bytes(auth_data));
response.insert(CborValue::Integer(0x03), CborValue::Map(att_stmt));
info!("MakeCredential successful");
Step::Done(Ctap2Response::success(CborValue::Map(response)))
}
fn handle_get_assertion(&mut self, params: Option<CborValue>, presence: UserPresence) -> Step {
debug!("CTAP2 GetAssertion");
let params = match params {
Some(CborValue::Map(m)) => m,
_ => return Step::Done(Ctap2Response::error(Ctap2Status::MissingParameter)),
};
let rp_id = match params.get(&CborValue::Integer(0x01)) {
Some(CborValue::Text(s)) => s.clone(),
_ => return Step::Done(Ctap2Response::error(Ctap2Status::MissingParameter)),
};
let client_data_hash = match params.get(&CborValue::Integer(0x02)) {
Some(CborValue::Bytes(b)) if b.len() == 32 => b.clone(),
_ => return Step::Done(Ctap2Response::error(Ctap2Status::MissingParameter)),
};
info!("GetAssertion for RP '{}'", rp_id);
if self.store.credential_count(&rp_id) == 0 {
warn!("No credentials found for RP '{}'", rp_id);
return Step::Done(Ctap2Response::error(Ctap2Status::NoCredentials));
}
match presence {
UserPresence::Ask => {
return Step::Ask(ApprovalDetails {
operation: OperationType::Authenticate,
rp_id: rp_id.clone(),
user_name: self.store.first_user_name(&rp_id),
credential_count: self.store.credential_count(&rp_id),
});
}
UserPresence::Denied => {
warn!("GetAssertion denied for RP '{}'", rp_id);
return Step::Done(Ctap2Response::error(Ctap2Status::OperationDenied));
}
UserPresence::Approved => {
info!("GetAssertion approved for RP '{}'", rp_id);
}
}
let pin_verified = self.store.pin_verified();
let credential = match self.store.find_credentials(&rp_id, None) {
Some(c) => c,
None => {
warn!("No credentials found for RP '{}'", rp_id);
return Step::Done(Ctap2Response::error(Ctap2Status::NoCredentials));
}
};
credential.counter += 1;
let counter = credential.counter;
let credential_id = credential.credential_id.clone();
let private_key = credential.private_key.clone();
let mut auth_data = Vec::new();
auth_data.extend_from_slice(
ring::digest::digest(&ring::digest::SHA256, rp_id.as_bytes()).as_ref(),
);
let flags = 0x01 | if pin_verified { 0x04 } else { 0x00 };
auth_data.push(flags);
auth_data.extend_from_slice(&counter.to_be_bytes());
let mut sig_data = auth_data.clone();
sig_data.extend_from_slice(&client_data_hash);
let key_pair = match EcdsaKeyPair::from_pkcs8(
&ECDSA_P256_SHA256_FIXED_SIGNING,
&private_key,
&ring::rand::SystemRandom::new(),
) {
Ok(kp) => kp,
Err(e) => {
warn!("Failed to load private key: {}", e);
return Step::Done(Ctap2Response::error(Ctap2Status::Other));
}
};
let signature = match key_pair.sign(&self.store.rng, &sig_data) {
Ok(sig) => sig.as_ref().to_vec(),
Err(e) => {
warn!("Failed to sign: {}", e);
return Step::Done(Ctap2Response::error(Ctap2Status::Other));
}
};
let mut descriptor = BTreeMap::new();
descriptor.insert(
CborValue::Text("id".to_string()),
CborValue::Bytes(credential_id),
);
descriptor.insert(
CborValue::Text("type".to_string()),
CborValue::Text("public-key".to_string()),
);
let mut response = BTreeMap::new();
response.insert(CborValue::Integer(0x01), CborValue::Map(descriptor));
response.insert(CborValue::Integer(0x02), CborValue::Bytes(auth_data));
response.insert(CborValue::Integer(0x03), CborValue::Bytes(signature));
info!("GetAssertion successful");
Step::Done(Ctap2Response::success(CborValue::Map(response)))
}
fn handle_client_pin(&mut self, params: Option<CborValue>) -> Ctap2Response {
debug!("CTAP2 ClientPin");
let params = match params {
Some(CborValue::Map(m)) => m,
_ => return Ctap2Response::error(Ctap2Status::MissingParameter),
};
let _pin_protocol = match params.get(&CborValue::Integer(0x01)) {
Some(CborValue::Integer(1)) => 1, _ => return Ctap2Response::error(Ctap2Status::InvalidParameter),
};
let sub_command = match params.get(&CborValue::Integer(0x02)) {
Some(CborValue::Integer(n)) => *n as u8,
_ => return Ctap2Response::error(Ctap2Status::MissingParameter),
};
match sub_command {
0x03 => {
let mut response = BTreeMap::new();
response.insert(
CborValue::Integer(0x03),
CborValue::Integer(self.store.pin_retries() as i128),
);
info!("ClientPin: getPinRetries = {}", self.store.pin_retries());
Ctap2Response::success(CborValue::Map(response))
}
0x06 => {
if let Some(CborValue::Text(pin)) = params.get(&CborValue::Integer(0x05)) {
match self.store.set_pin(pin) {
Ok(()) => {
info!("ClientPin: PIN set successfully");
Ctap2Response::success(CborValue::Map(BTreeMap::new()))
}
Err(e) => {
warn!("ClientPin: Failed to set PIN: {}", e);
Ctap2Response::error(Ctap2Status::PinInvalid)
}
}
} else {
Ctap2Response::error(Ctap2Status::MissingParameter)
}
}
0x08 => {
if let Some(CborValue::Text(pin)) = params.get(&CborValue::Integer(0x05)) {
match self.store.verify_pin(pin) {
Ok(true) => {
info!("ClientPin: PIN verified successfully");
Ctap2Response::success(CborValue::Map(BTreeMap::new()))
}
Ok(false) => {
warn!("ClientPin: PIN verification failed");
Ctap2Response::error(Ctap2Status::PinInvalid)
}
Err(e) => {
warn!("ClientPin: PIN verification error: {}", e);
Ctap2Response::error(Ctap2Status::PinInvalid)
}
}
} else {
Ctap2Response::error(Ctap2Status::MissingParameter)
}
}
_ => {
warn!("ClientPin: Unsupported subCommand: {:#04x}", sub_command);
Ctap2Response::error(Ctap2Status::InvalidParameter)
}
}
}
fn handle_get_next_assertion(&mut self) -> Ctap2Response {
debug!("CTAP2 GetNextAssertion");
Ctap2Response::error(Ctap2Status::NoCredentials)
}
fn handle_reset(&mut self) -> Ctap2Response {
debug!("CTAP2 Reset");
self.store.credentials.clear();
self.store.resident_credentials.clear();
info!("All credentials cleared");
Ctap2Response::success(CborValue::Map(BTreeMap::new()))
}
}
#[cfg(all(unix, feature = "libusb"))]
impl Default for Ctap2Handler {
fn default() -> Self {
Self::new()
}
}