#![cfg(feature = "fido2-hardware")]
use ctap_hid_fido2::fidokey::get_assertion::get_assertion_params::{
Extension as AssertExtension, GetAssertionArgsBuilder,
};
use ctap_hid_fido2::fidokey::make_credential::make_credential_params::{
Extension as CredExtension, MakeCredentialArgsBuilder,
};
use ctap_hid_fido2::{FidoKeyHid, FidoKeyHidFactory, LibCfg};
use super::fido2::{Fido2Authenticator, HMAC_SALT_LEN};
use crate::error::Error;
pub struct HwAuthenticator {
device: FidoKeyHid,
}
impl HwAuthenticator {
pub fn discover() -> Result<Self, Error> {
let cfg = LibCfg::init();
let device = FidoKeyHidFactory::create(&cfg).map_err(|e| {
Error::Fido2AssertionFailed(format!(
"no FIDO2 authenticator found on USB HID bus: {e}. \
plug in your security key and retry."
))
})?;
Ok(Self { device })
}
}
fn fresh_challenge() -> Vec<u8> {
use rand::RngCore;
let mut buf = vec![0u8; 32];
rand::rngs::OsRng.fill_bytes(&mut buf);
buf
}
impl Fido2Authenticator for HwAuthenticator {
fn make_credential(
&mut self,
relying_party_id: &str,
_relying_party_name: &str,
) -> Result<Vec<u8>, Error> {
let challenge = fresh_challenge();
let extensions = vec![CredExtension::HmacSecret(Some(true))];
let args = MakeCredentialArgsBuilder::new(relying_party_id, &challenge)
.extensions(&extensions)
.without_pin_and_uv()
.build();
let attestation = self.device.make_credential_with_args(&args).map_err(|e| {
Error::Fido2AssertionFailed(format!(
"make_credential failed: {e}. \
if your authenticator requires a PIN, this build does not yet \
support PIN entry — fall back to a key without PIN, or wait for \
the next release."
))
})?;
Ok(attestation.credential_descriptor.id)
}
fn assert_with_hmac(
&self,
credential_id: &[u8],
salt: &[u8; HMAC_SALT_LEN],
) -> Result<[u8; HMAC_SALT_LEN], Error> {
let challenge = fresh_challenge();
let extensions = vec![AssertExtension::HmacSecret(Some(*salt))];
let args = GetAssertionArgsBuilder::new(
crate::vault::keychain::fido2_unlock::RELYING_PARTY_ID,
&challenge,
)
.credential_id(credential_id)
.extensions(&extensions)
.without_pin_and_uv()
.build();
let assertions = self.device.get_assertion_with_args(&args).map_err(|e| {
Error::Fido2AssertionFailed(format!(
"get_assertion_with_hmac failed: {e}. \
confirm the same security key used for enrollment is plugged in \
and that the touch was registered within the device's timeout."
))
})?;
let assertion = assertions.into_iter().next().ok_or_else(|| {
Error::Fido2AssertionFailed(
"authenticator returned no assertion — likely user-presence \
timeout (no touch within ~30s)"
.to_string(),
)
})?;
for ext in &assertion.extensions {
if let AssertExtension::HmacSecret(Some(out)) = ext {
let mut result = [0u8; HMAC_SALT_LEN];
result.copy_from_slice(out);
return Ok(result);
}
}
Err(Error::Fido2AssertionFailed(
"authenticator did not include hmac-secret in its response — \
your security key may not support the hmac-secret extension. \
confirm with `ykman fido info` or equivalent."
.to_string(),
))
}
}