pub mod ffi;
pub mod hid;
pub mod pairing;
#[cfg(all(unix, feature = "test-fixture"))]
pub mod socket;
use anyhow::{bail, Result};
use std::ffi::{c_int, CStr, CString};
use std::sync::Once;
use zeroize::Zeroizing;
pub const RP_ID: &str = "yb";
pub const YUBICO_VENDOR_ID: u16 = 0x1050;
pub const SALT_LEN: usize = 32;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum Fido2Error {
#[error("the YubiKey was not touched in time")]
NoTouch,
#[error("wrong FIDO2 PIN")]
PinInvalid,
#[error("3 wrong FIDO2 PINs in a row: unplug and replug the YubiKey")]
PinAuthBlocked,
#[error(
"the FIDO2 PIN is blocked: only a FIDO2 reset clears it, which destroys \
post-quantum stores"
)]
PinBlocked,
#[error("no FIDO2 PIN is set on this YubiKey")]
PinNotSet,
#[error("the FIDO2 credential is not on this YubiKey (FIDO2 reset?)")]
NoCredential,
#[error(
"only {0} FIDO2 PIN attempts left; a blocked FIDO2 PIN cannot be unblocked, and \
would destroy post-quantum stores. Make sure of your PIN, then retry with \
--allow-low-pin-attempts"
)]
FewAttempts(u32),
#[error(
"the FIDO2 side of the YubiKey was not found (is the FIDO application enabled, \
and /dev/hidraw accessible? see the documentation on udev rules)"
)]
NoDevice,
#[error("FIDO2 error: {0}")]
Other(String),
}
impl Fido2Error {
fn from_code(code: c_int) -> Self {
match code {
ffi::FIDO_ERR_OPERATION_DENIED
| ffi::FIDO_ERR_USER_ACTION_TIMEOUT
| ffi::FIDO_ERR_UP_REQUIRED
| ffi::FIDO_ERR_UNSUPPORTED_OPTION => Self::NoTouch,
ffi::FIDO_ERR_PIN_INVALID => Self::PinInvalid,
ffi::FIDO_ERR_PIN_AUTH_BLOCKED => Self::PinAuthBlocked,
ffi::FIDO_ERR_PIN_BLOCKED => Self::PinBlocked,
ffi::FIDO_ERR_PIN_NOT_SET => Self::PinNotSet,
ffi::FIDO_ERR_NO_CREDENTIALS => Self::NoCredential,
_ => Self::Other(strerr(code)),
}
}
}
fn strerr(code: c_int) -> String {
let s = unsafe { CStr::from_ptr(ffi::fido_strerr(code)) };
format!("{} (0x{code:02x})", s.to_string_lossy())
}
fn check(code: c_int) -> Result<()> {
if code == ffi::FIDO_OK {
Ok(())
} else {
Err(Fido2Error::from_code(code).into())
}
}
fn c_pin(pin: &str) -> Result<Zeroizing<Vec<u8>>> {
if pin.as_bytes().contains(&0) {
bail!("a PIN cannot contain a NUL byte");
}
let mut c = Zeroizing::new(Vec::with_capacity(pin.len() + 1));
c.extend_from_slice(pin.as_bytes());
c.push(0);
Ok(c)
}
fn init() {
static INIT: Once = Once::new();
INIT.call_once(|| unsafe { ffi::fido_init(0) });
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeviceInfo {
pub path: String,
pub vendor_id: u16,
pub product_id: u16,
}
pub fn list_devices() -> Result<Vec<DeviceInfo>> {
init();
const MAX: usize = 64;
let mut devices = Vec::new();
unsafe {
let mut list = ffi::fido_dev_info_new(MAX);
if list.is_null() {
bail!("libfido2: out of memory");
}
let mut n = 0usize;
let r = ffi::fido_dev_info_manifest(list, MAX, &mut n);
if r == ffi::FIDO_OK {
for i in 0..n {
let di = ffi::fido_dev_info_ptr(list, i);
devices.push(DeviceInfo {
path: CStr::from_ptr(ffi::fido_dev_info_path(di))
.to_string_lossy()
.into_owned(),
vendor_id: ffi::fido_dev_info_vendor(di) as u16,
product_id: ffi::fido_dev_info_product(di) as u16,
});
}
}
ffi::fido_dev_info_free(&mut list, MAX);
check(r)?;
}
Ok(devices)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Credential {
pub id: Vec<u8>,
}
pub struct Device {
dev: *mut ffi::FidoDev,
}
impl Device {
pub fn open(path: &str) -> Result<Self> {
Self::open_with(path, None)
}
#[cfg(any(test, feature = "test-utils", feature = "test-fixture"))]
pub fn open_with_io(io: &ffi::FidoDevIo) -> Result<Self> {
Self::open_with("emulator", Some(io))
}
#[cfg(feature = "test-fixture")]
pub fn open_with_io_path(path: &str, io: &ffi::FidoDevIo) -> Result<Self> {
Self::open_with(path, Some(io))
}
fn open_with(path: &str, io: Option<&ffi::FidoDevIo>) -> Result<Self> {
init();
let path = CString::new(path)?;
unsafe {
let mut dev = ffi::fido_dev_new();
if dev.is_null() {
bail!("libfido2: out of memory");
}
let mut r = ffi::FIDO_OK;
if let Some(io) = io {
r = ffi::fido_dev_set_io_functions(dev, io);
}
if r == ffi::FIDO_OK {
r = ffi::fido_dev_open(dev, path.as_ptr());
}
if r != ffi::FIDO_OK {
ffi::fido_dev_free(&mut dev);
return Err(Fido2Error::from_code(r).into());
}
Ok(Self { dev })
}
}
pub fn touch_begin(&self) -> Result<()> {
check(unsafe { ffi::fido_dev_get_touch_begin(self.dev) })
}
pub fn touched(&self, ms: i32) -> Result<bool> {
let mut touched: c_int = 0;
check(unsafe { ffi::fido_dev_get_touch_status(self.dev, &mut touched, ms) })?;
Ok(touched != 0)
}
pub fn cancel(&self) {
unsafe { ffi::fido_dev_cancel(self.dev) };
}
pub fn has_pin(&self) -> bool {
unsafe { ffi::fido_dev_has_pin(self.dev) }
}
pub fn pin_retries(&self) -> Result<u32> {
let mut n: c_int = -1;
check(unsafe { ffi::fido_dev_get_retry_count(self.dev, &mut n) })?;
Ok(n.max(0) as u32)
}
pub fn set_pin(&self, pin: &str) -> Result<()> {
let pin = c_pin(pin)?;
check(unsafe { ffi::fido_dev_set_pin(self.dev, pin.as_ptr().cast(), std::ptr::null()) })
}
pub fn make_credential(&self, pin: &str) -> Result<Credential> {
let rp = CString::new(RP_ID)?;
let user = CString::new("yb store")?;
let pin = c_pin(pin)?;
let user_id: [u8; 16] = rand::random();
let cdh: [u8; 32] = rand::random();
unsafe {
let mut cred = ffi::fido_cred_new();
if cred.is_null() {
bail!("libfido2: out of memory");
}
let mut r = ffi::fido_cred_set_type(cred, ffi::COSE_ES256);
for step in [
ffi::fido_cred_set_clientdata_hash(cred, cdh.as_ptr(), cdh.len()),
ffi::fido_cred_set_rp(cred, rp.as_ptr(), std::ptr::null()),
ffi::fido_cred_set_user(
cred,
user_id.as_ptr(),
user_id.len(),
user.as_ptr(),
std::ptr::null(),
std::ptr::null(),
),
ffi::fido_cred_set_rk(cred, ffi::FIDO_OPT_FALSE),
ffi::fido_cred_set_extensions(cred, ffi::FIDO_EXT_HMAC_SECRET),
ffi::fido_cred_set_prot(cred, ffi::FIDO_CRED_PROT_UV_REQUIRED),
] {
if r == ffi::FIDO_OK {
r = step;
}
}
if r == ffi::FIDO_OK {
r = ffi::fido_dev_make_cred(self.dev, cred, pin.as_ptr().cast());
}
let result = if r == ffi::FIDO_OK {
let id = std::slice::from_raw_parts(
ffi::fido_cred_id_ptr(cred),
ffi::fido_cred_id_len(cred),
)
.to_vec();
Ok(Credential { id })
} else {
Err(Fido2Error::from_code(r).into())
};
ffi::fido_cred_free(&mut cred);
result
}
}
pub fn find_credential(&self, candidates: &[&[u8]], pin: &str) -> Result<Option<Vec<u8>>> {
if candidates.is_empty() {
return Ok(None);
}
let rp = CString::new(RP_ID)?;
let pin = c_pin(pin)?;
let cdh: [u8; 32] = rand::random();
unsafe {
let mut a = ffi::fido_assert_new();
if a.is_null() {
bail!("libfido2: out of memory");
}
let mut r = ffi::fido_assert_set_rp(a, rp.as_ptr());
if r == ffi::FIDO_OK {
r = ffi::fido_assert_set_clientdata_hash(a, cdh.as_ptr(), cdh.len());
}
for id in candidates {
if r == ffi::FIDO_OK {
r = ffi::fido_assert_allow_cred(a, id.as_ptr(), id.len());
}
}
if r == ffi::FIDO_OK {
r = ffi::fido_assert_set_up(a, ffi::FIDO_OPT_FALSE);
}
if r == ffi::FIDO_OK {
r = ffi::fido_dev_get_assert(self.dev, a, pin.as_ptr().cast());
}
let result = match r {
ffi::FIDO_OK if ffi::fido_assert_count(a) == 1 => Ok(Some(
std::slice::from_raw_parts(
ffi::fido_assert_id_ptr(a, 0),
ffi::fido_assert_id_len(a, 0),
)
.to_vec(),
)),
ffi::FIDO_OK => Err(anyhow::anyhow!("libfido2: unexpected number of assertions")),
ffi::FIDO_ERR_NO_CREDENTIALS => Ok(None),
r => Err(Fido2Error::from_code(r).into()),
};
ffi::fido_assert_free(&mut a);
result
}
}
pub fn hmac_secret(
&self,
credential_id: &[u8],
salts: &[[u8; SALT_LEN]],
pin: &str,
) -> Result<Vec<Zeroizing<[u8; SALT_LEN]>>> {
if salts.is_empty() || salts.len() > 2 {
bail!("hmac-secret takes one or two salts");
}
let rp = CString::new(RP_ID)?;
let pin = c_pin(pin)?;
let joined: Vec<u8> = salts.concat();
let cdh: [u8; 32] = rand::random();
unsafe {
let mut a = ffi::fido_assert_new();
if a.is_null() {
bail!("libfido2: out of memory");
}
let mut r = ffi::fido_assert_set_rp(a, rp.as_ptr());
for step in [
ffi::fido_assert_set_clientdata_hash(a, cdh.as_ptr(), cdh.len()),
ffi::fido_assert_allow_cred(a, credential_id.as_ptr(), credential_id.len()),
ffi::fido_assert_set_extensions(a, ffi::FIDO_EXT_HMAC_SECRET),
ffi::fido_assert_set_hmac_salt(a, joined.as_ptr(), joined.len()),
ffi::fido_assert_set_up(a, ffi::FIDO_OPT_TRUE),
ffi::fido_assert_set_uv(a, ffi::FIDO_OPT_TRUE),
] {
if r == ffi::FIDO_OK {
r = step;
}
}
if r == ffi::FIDO_OK {
r = ffi::fido_dev_get_assert(self.dev, a, pin.as_ptr().cast());
}
let result = if r != ffi::FIDO_OK {
Err(Fido2Error::from_code(r).into())
} else if ffi::fido_assert_count(a) != 1 {
Err(anyhow::anyhow!("libfido2: unexpected number of assertions"))
} else {
let len = ffi::fido_assert_hmac_secret_len(a, 0);
if len != SALT_LEN * salts.len() {
Err(anyhow::anyhow!(
"hmac-secret: unexpected output length {len}"
))
} else {
let out =
std::slice::from_raw_parts(ffi::fido_assert_hmac_secret_ptr(a, 0), len);
Ok(out
.chunks(SALT_LEN)
.map(|c| {
let mut k = Zeroizing::new([0u8; SALT_LEN]);
k.copy_from_slice(c);
k
})
.collect())
}
};
ffi::fido_assert_free(&mut a);
result
}
}
}
impl Drop for Device {
fn drop(&mut self) {
unsafe {
ffi::fido_dev_close(self.dev);
ffi::fido_dev_free(&mut self.dev);
}
}
}