diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 30a1a028..81f263a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,6 +72,8 @@ jobs: # handle. - if: runner.os != 'windows' run: cargo run --bin fido-key-manager -- --help + - if: runner.os != 'windows' + run: cargo run --bin fido-key-manager --features solokey -- --help - run: cargo run --bin fido-mds-tool -- --help authenticator: @@ -88,7 +90,7 @@ jobs: - softtoken - usb - bluetooth,nfc,usb,ctap2-management - - bluetooth,cable,cable-override-tunnel,ctap2-management,nfc,softpasskey,softtoken,usb + - bluetooth,cable,cable-override-tunnel,ctap2-management,nfc,softpasskey,softtoken,usb,vendor-solokey os: - ubuntu-latest - windows-latest diff --git a/Cargo.toml b/Cargo.toml index d1939adf..a0409da0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,6 +78,7 @@ tokio = { version = "1.22.0", features = [ "sync", "test-util", "macros", + "net", "rt-multi-thread", "time", ] } diff --git a/fido-key-manager/Cargo.toml b/fido-key-manager/Cargo.toml index 1926126c..202783c3 100644 --- a/fido-key-manager/Cargo.toml +++ b/fido-key-manager/Cargo.toml @@ -23,6 +23,7 @@ test = false bluetooth = ["webauthn-authenticator-rs/bluetooth"] nfc = ["webauthn-authenticator-rs/nfc"] usb = ["webauthn-authenticator-rs/usb"] +solokey = ["webauthn-authenticator-rs/vendor-solokey"] default = ["nfc", "usb"] diff --git a/fido-key-manager/README.md b/fido-key-manager/README.md index 1bb4121c..f259bb11 100644 --- a/fido-key-manager/README.md +++ b/fido-key-manager/README.md @@ -38,6 +38,10 @@ Start-Process -FilePath "powershell" -Verb RunAs .\target\debug\fido-key-manager.exe --help ``` +By default, Cargo will build `fido-key-manager` with the `nfc` and `usb` +[features][]. Additional features are described in `Cargo.toml` and in the +remainder of this document. + ## Commands Most `fido-key-manager` commands (except `info` and `factory-reset`) will @@ -80,10 +84,42 @@ Command | Description | Requirements [Enterprise Attestation]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#sctn-feature-descriptions-enterp-attstn [Minimum PIN Length]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#sctn-feature-descriptions-minPinLength +## Vendor-specific commands + +**Warning:** for safety, ensure that you **only** have security key(s) from that +vendor connected to your computer when using **any** vendor-specific command, +**even benign ones**. + +In the CTAP 2 protocol, vendor-specific command IDs can (and do!) have different +meanings on different vendors – one vendor may use a certain ID as a safe +operation (such as "get info"), but another vendor might use the same ID to +start firmware updates, change the key's operating mode or perform some +potentially-destructive operation. + +For operations that require multiple commands be sent to a security key, this +tool will attempt to stop early if a key reports that it does not support one +of the commands, or returns an unexpected value. + +### SoloKey 2 / Trussed + +> **Tip:** this functionality is only available when `fido-key-manager` is +> built with `--features solokey`. + +SoloKey 2 / Trussed commands are currently **only** supported over USB HID. NFC +support may be added in future, but we have encountered many problems +communicating with SoloKey and Trussed devices *at all* over NFC, which has made +things difficult. + +Command | Description +------- | ----------- +`solo-key-info` | get all connected SoloKeys' unique ID, firmware version and secure boot status +`solo-key-random` | get some random bytes from a SoloKey + ## Platform-specific notes Bluetooth is currently disabled by default, as it's not particularly reliable on -anything but macOS, and can easily accidentally select nearby devices. +anything but macOS, and can easily accidentally select nearby devices. It can be +enabled with `--features bluetooth`. ### Linux @@ -145,6 +181,10 @@ anything but macOS, and can easily accidentally select nearby devices. * NFC should "just work", provided you've installed a PC/SC initiator (driver) for your transciever (if it is not supported by `libccid`). + macOS tends to "butt in" on exclusive connections by selecting the PIV applet, + which can cause issues for some keys' firmware, especially if they support + PIV. + * USB should "just work". ### Windows @@ -187,3 +227,4 @@ As long as you're running `fido-key-manager` as Administrator: * USB support should "just work". [1]: https://learn.microsoft.com/en-us/previous-versions/bb756929(v=msdn.10) +[features]: https://doc.rust-lang.org/cargo/reference/features.html diff --git a/fido-key-manager/src/main.rs b/fido-key-manager/src/main.rs index 1a7cb61a..d8f41276 100644 --- a/fido-key-manager/src/main.rs +++ b/fido-key-manager/src/main.rs @@ -11,6 +11,9 @@ use hex::{FromHex, FromHexError}; use std::io::{stdin, stdout, Write}; use std::time::Duration; use tokio_stream::StreamExt; +#[cfg(feature = "solokey")] +use webauthn_authenticator_rs::ctap2::SoloKeyAuthenticator; +use webauthn_authenticator_rs::prelude::WebauthnCError; use webauthn_authenticator_rs::{ ctap2::{ commands::UserCM, select_one_device, select_one_device_predicate, @@ -197,6 +200,12 @@ pub enum Opt { DeleteCredential(DeleteCredentialOpt), /// Updates user information for a discoverable credential on this token. UpdateCredentialUser(UpdateCredentialUserOpt), + #[cfg(feature = "solokey")] + /// Gets info about a connected SoloKey 2 or Trussed device. + SoloKeyInfo(InfoOpt), + #[cfg(feature = "solokey")] + /// Gets some random bytes from a connected SoloKey 2 or Trussed device. + SoloKeyRandom, } #[derive(Debug, clap::Parser)] @@ -680,5 +689,86 @@ async fn main() { .await .expect("Error updating credential"); } + + #[cfg(feature = "solokey")] + Opt::SoloKeyInfo(o) => { + println!("Looking for SoloKey 2 or Trussed devices..."); + while let Some(event) = stream.next().await { + match event { + TokenEvent::Added(t) => { + let mut authenticator = match CtapAuthenticator::new(t, &ui).await { + Some(a) => a, + None => continue, + }; + + // TODO: filter this to just SoloKey devices in a safe way + let uuid = match authenticator.get_solokey_uuid().await { + Ok(v) => v, + Err(WebauthnCError::NotSupported) + | Err(WebauthnCError::U2F(_)) + | Err(WebauthnCError::InvalidMessageLength) => { + println!("Device is not a SoloKey!"); + continue; + } + Err(e) => panic!("could not get SoloKey UUID: {e:?}"), + }; + + let version = match authenticator.get_solokey_version().await { + Ok(v) => v, + Err(WebauthnCError::NotSupported) + | Err(WebauthnCError::U2F(_)) + | Err(WebauthnCError::InvalidMessageLength) => { + println!("Device is not a SoloKey!"); + continue; + } + Err(e) => panic!("could not get SoloKey version: {e:?}"), + }; + + let secure_boot = if match authenticator.get_solokey_lock().await { + Ok(v) => v, + Err(WebauthnCError::NotSupported) + | Err(WebauthnCError::U2F(_)) + | Err(WebauthnCError::InvalidMessageLength) => { + println!("Device is not a SoloKey!"); + continue; + } + Err(e) => panic!("could not get SoloKey lock state: {e:?}"), + } { + "enabled" + } else { + "disabled" + }; + + println!("SoloKey info:"); + println!(" Device UUID: {uuid}"); + println!(" Version: {version:#x}"); + println!(" Secure boot: {secure_boot}"); + } + TokenEvent::EnumerationComplete => { + if o.watch { + println!("Initial enumeration completed, watching for more devices..."); + println!("Press Ctrl + C to stop watching."); + } else { + break; + } + } + _ => (), + } + } + } + + #[cfg(feature = "solokey")] + Opt::SoloKeyRandom => { + // TODO: filter this to just SoloKey devices in a safe way + println!("Insert a SoloKey 2 or Trussed device..."); + let mut token: CtapAuthenticator = + select_one_device(stream, &ui).await.unwrap(); + + let r = token + .get_solokey_random() + .await + .expect("Error getting random data"); + println!("Random bytes: {}", hex::encode(r)); + } } } diff --git a/webauthn-authenticator-rs/Cargo.toml b/webauthn-authenticator-rs/Cargo.toml index daf7bc39..6bfb2d06 100644 --- a/webauthn-authenticator-rs/Cargo.toml +++ b/webauthn-authenticator-rs/Cargo.toml @@ -50,6 +50,8 @@ ctap2 = [ "dep:tokio-stream", ] ctap2-management = ["ctap2"] +# Support for SoloKey's vendor commands +vendor-solokey = [] nfc = ["ctap2", "dep:pcsc"] # TODO: allow running softpasskey without softtoken softpasskey = ["crypto", "softtoken"] diff --git a/webauthn-authenticator-rs/src/bluetooth/mod.rs b/webauthn-authenticator-rs/src/bluetooth/mod.rs index b75ed707..985aeefd 100644 --- a/webauthn-authenticator-rs/src/bluetooth/mod.rs +++ b/webauthn-authenticator-rs/src/bluetooth/mod.rs @@ -75,9 +75,9 @@ use crate::{ transport::{ types::{ CBORResponse, KeepAliveStatus, Response, U2FError, BTLE_CANCEL, BTLE_KEEPALIVE, - TYPE_INIT, U2FHID_ERROR, U2FHID_MSG, U2FHID_PING, + U2FHID_ERROR, U2FHID_MSG, U2FHID_PING, }, - Token, TokenEvent, Transport, + Token, TokenEvent, Transport, TYPE_INIT, }, ui::UiCallback, }; diff --git a/webauthn-authenticator-rs/src/ctap2/mod.rs b/webauthn-authenticator-rs/src/ctap2/mod.rs index f194bda8..ac45ea49 100644 --- a/webauthn-authenticator-rs/src/ctap2/mod.rs +++ b/webauthn-authenticator-rs/src/ctap2/mod.rs @@ -128,6 +128,9 @@ mod ctap21_cred; mod ctap21pre; mod internal; mod pin_uv; +#[cfg(any(all(doc, not(doctest)), feature = "vendor-solokey"))] +#[doc(hidden)] +mod solokey; use std::ops::{Deref, DerefMut}; use std::pin::Pin; @@ -159,6 +162,10 @@ pub use self::{ ctap21_bio::BiometricAuthenticator, ctap21_cred::CredentialManagementAuthenticator, }; +#[cfg(any(all(doc, not(doctest)), feature = "vendor-solokey"))] +#[doc(inline)] +pub use self::solokey::SoloKeyAuthenticator; + /// Abstraction for different versions of the CTAP2 protocol. /// /// All tokens can [Deref] into [Ctap20Authenticator]. diff --git a/webauthn-authenticator-rs/src/ctap2/solokey.rs b/webauthn-authenticator-rs/src/ctap2/solokey.rs new file mode 100644 index 00000000..d3bee9f2 --- /dev/null +++ b/webauthn-authenticator-rs/src/ctap2/solokey.rs @@ -0,0 +1,58 @@ +use async_trait::async_trait; +use uuid::Uuid; + +use crate::{ + prelude::WebauthnCError, transport::solokey::SoloKeyToken, transport::Token, ui::UiCallback, +}; + +use super::Ctap20Authenticator; + +/// SoloKey (Trussed) vendor-specific commands. +/// +/// ## Warning +/// +/// These commands currently operate on *any* [`Ctap20Authenticator`][], and do +/// not filter to just SoloKey/Trussed devices. Due to the nature of CTAP +/// vendor-specific commands, this may cause unexpected or undesirable behaviour +/// on other vendors' keys. +/// +/// Protocol notes are in [`crate::transport::solokey`]. +#[async_trait] +pub trait SoloKeyAuthenticator { + /// Gets a SoloKey's lock (secure boot) status. + async fn get_solokey_lock(&mut self) -> Result; + + /// Gets some random bytes from a SoloKey. + async fn get_solokey_random(&mut self) -> Result<[u8; 57], WebauthnCError>; + + /// Gets a SoloKey's UUID. + async fn get_solokey_uuid(&mut self) -> Result; + + /// Gets a SoloKey's firmware version. + async fn get_solokey_version(&mut self) -> Result; +} + +#[async_trait] +impl<'a, T: Token + SoloKeyToken, U: UiCallback> SoloKeyAuthenticator + for Ctap20Authenticator<'a, T, U> +{ + #[inline] + async fn get_solokey_lock(&mut self) -> Result { + self.token.get_solokey_lock().await + } + + #[inline] + async fn get_solokey_random(&mut self) -> Result<[u8; 57], WebauthnCError> { + self.token.get_solokey_random().await + } + + #[inline] + async fn get_solokey_uuid(&mut self) -> Result { + self.token.get_solokey_uuid().await + } + + #[inline] + async fn get_solokey_version(&mut self) -> Result { + self.token.get_solokey_version().await + } +} diff --git a/webauthn-authenticator-rs/src/error.rs b/webauthn-authenticator-rs/src/error.rs index 082b0aad..1b5a14cd 100644 --- a/webauthn-authenticator-rs/src/error.rs +++ b/webauthn-authenticator-rs/src/error.rs @@ -69,6 +69,8 @@ pub enum WebauthnCError { /// something has not been initialised correctly, or that the authenticator /// is sending unexpected messages. UnexpectedState, + #[cfg(feature = "usb")] + U2F(crate::transport::types::U2FError), } #[cfg(feature = "nfc")] @@ -141,6 +143,13 @@ impl From for WebauthnCError { } } +#[cfg(feature = "usb")] +impl From for WebauthnCError { + fn from(value: crate::transport::types::U2FError) -> Self { + Self::U2F(value) + } +} + /// #[derive(Debug, PartialEq, Eq)] pub enum CtapError { diff --git a/webauthn-authenticator-rs/src/transport/mod.rs b/webauthn-authenticator-rs/src/transport/mod.rs index c5b403f7..c1d8a3d4 100644 --- a/webauthn-authenticator-rs/src/transport/mod.rs +++ b/webauthn-authenticator-rs/src/transport/mod.rs @@ -3,6 +3,8 @@ //! See [crate::ctap2] for a higher-level abstraction over this API. mod any; pub mod iso7816; +#[cfg(any(all(doc, not(doctest)), feature = "vendor-solokey"))] +pub(crate) mod solokey; #[cfg(any(doc, feature = "bluetooth", feature = "usb"))] pub(crate) mod types; @@ -15,6 +17,9 @@ use webauthn_rs_proto::AuthenticatorTransport; use crate::{ctap2::*, error::WebauthnCError, ui::UiCallback}; +#[cfg(any(doc, feature = "bluetooth", feature = "usb"))] +pub(crate) const TYPE_INIT: u8 = 0x80; + #[derive(Debug)] pub enum TokenEvent { Added(T), diff --git a/webauthn-authenticator-rs/src/transport/solokey.rs b/webauthn-authenticator-rs/src/transport/solokey.rs new file mode 100644 index 00000000..4fb30edd --- /dev/null +++ b/webauthn-authenticator-rs/src/transport/solokey.rs @@ -0,0 +1,113 @@ +//! SoloKey (Trussed) vendor-specific commands. +//! +//! ## USB HID +//! +//! Commands are sent on a `U2FHIDFrame` level, and values are bitwise-OR'd +//! with `transport::TYPE_INIT` (0x80). +//! +//! Command | Description | Request | Response +//! ------- | ----------- | ------- | -------- +//! `0x51` | Update | _none_ to reboot into update mode, `01` to be "destructive" | _none_ +//! `0x53` | Reboot | _none_ | _none_ +//! `0x60` | Get random bytes | _none_ | 57 bytes of randomness +//! `0x61` | Get version | _none_ | Version ID as `u32` +//! `0x62` | Get device UUID | _none_ | Big-endian UUID (16 bytes) +//! `0x63` | Get lock state | _none_ | `0` for unlocked "hacker edition" devices, `1` if locked +//! +//! ## NFC +//! +//! Admin app AID: `A0 00 00 08 47 00 00 00 01` +//! +//! ## References +//! +//! * [`solo2-cli` Admin commands][0] +//! * [SoloKeys `admin-app` commands][1] +//! +//! [0]: https://github.com/solokeys/solo2-cli/blob/main/src/apps/admin.rs +//! [1]: https://github.com/solokeys/admin-app/blob/main/src/admin.rs +use async_trait::async_trait; +use uuid::Uuid; + +use crate::prelude::WebauthnCError; + +use super::AnyToken; + +#[cfg(all(feature = "usb", feature = "vendor-solokey"))] +pub(crate) const CMD_RANDOM: u8 = super::TYPE_INIT | 0x60; + +#[cfg(all(feature = "usb", feature = "vendor-solokey"))] +pub(crate) const CMD_VERSION: u8 = super::TYPE_INIT | 0x61; + +#[cfg(all(feature = "usb", feature = "vendor-solokey"))] +pub(crate) const CMD_UUID: u8 = super::TYPE_INIT | 0x62; + +#[cfg(all(feature = "usb", feature = "vendor-solokey"))] +pub(crate) const CMD_LOCK: u8 = super::TYPE_INIT | 0x63; + +/// See [`SoloKeyAuthenticator`](crate::ctap2::SoloKeyAuthenticator). +#[async_trait] +pub trait SoloKeyToken { + /// See [`SoloKeyAuthenticator::get_solokey_lock()`](crate::ctap2::SoloKeyAuthenticator::get_solokey_lock). + async fn get_solokey_lock(&mut self) -> Result; + + /// See [`SoloKeyAuthenticator::get_solokey_random()`](crate::ctap2::SoloKeyAuthenticator::get_solokey_random). + async fn get_solokey_random(&mut self) -> Result<[u8; 57], WebauthnCError>; + + /// See [`SoloKeyAuthenticator::get_solokey_version()`](crate::ctap2::SoloKeyAuthenticator::get_solokey_version). + async fn get_solokey_version(&mut self) -> Result; + + /// See [`SoloKeyAuthenticator::get_solokey_uuid()`](crate::ctap2::SoloKeyAuthenticator::get_solokey_uuid). + async fn get_solokey_uuid(&mut self) -> Result; +} + +#[async_trait] +#[allow(clippy::unimplemented)] +impl SoloKeyToken for AnyToken { + async fn get_solokey_lock(&mut self) -> Result { + match self { + AnyToken::Stub => unimplemented!(), + #[cfg(feature = "bluetooth")] + AnyToken::Bluetooth(_) => Err(WebauthnCError::NotSupported), + #[cfg(feature = "nfc")] + AnyToken::Nfc(_) => Err(WebauthnCError::NotSupported), + #[cfg(feature = "usb")] + AnyToken::Usb(u) => u.get_solokey_lock().await, + } + } + + async fn get_solokey_random(&mut self) -> Result<[u8; 57], WebauthnCError> { + match self { + AnyToken::Stub => unimplemented!(), + #[cfg(feature = "bluetooth")] + AnyToken::Bluetooth(_) => Err(WebauthnCError::NotSupported), + #[cfg(feature = "nfc")] + AnyToken::Nfc(_) => Err(WebauthnCError::NotSupported), + #[cfg(feature = "usb")] + AnyToken::Usb(u) => u.get_solokey_random().await, + } + } + + async fn get_solokey_version(&mut self) -> Result { + match self { + AnyToken::Stub => unimplemented!(), + #[cfg(feature = "bluetooth")] + AnyToken::Bluetooth(_) => Err(WebauthnCError::NotSupported), + #[cfg(feature = "nfc")] + AnyToken::Nfc(_) => Err(WebauthnCError::NotSupported), + #[cfg(feature = "usb")] + AnyToken::Usb(u) => u.get_solokey_version().await, + } + } + + async fn get_solokey_uuid(&mut self) -> Result { + match self { + AnyToken::Stub => unimplemented!(), + #[cfg(feature = "bluetooth")] + AnyToken::Bluetooth(_) => Err(WebauthnCError::NotSupported), + #[cfg(feature = "nfc")] + AnyToken::Nfc(_) => Err(WebauthnCError::NotSupported), + #[cfg(feature = "usb")] + AnyToken::Usb(u) => u.get_solokey_uuid().await, + } + } +} diff --git a/webauthn-authenticator-rs/src/transport/types.rs b/webauthn-authenticator-rs/src/transport/types.rs index c0df4d29..c48a05d2 100644 --- a/webauthn-authenticator-rs/src/transport/types.rs +++ b/webauthn-authenticator-rs/src/transport/types.rs @@ -1,9 +1,9 @@ +use super::TYPE_INIT; use crate::error::{CtapError, WebauthnCError}; #[cfg(any(all(doc, not(doctest)), feature = "usb"))] use super::iso7816::ISO7816ResponseAPDU; -pub const TYPE_INIT: u8 = 0x80; pub const U2FHID_PING: u8 = TYPE_INIT | 0x01; #[cfg(any(doc, feature = "bluetooth"))] pub const BTLE_KEEPALIVE: u8 = TYPE_INIT | 0x02; diff --git a/webauthn-authenticator-rs/src/usb/mod.rs b/webauthn-authenticator-rs/src/usb/mod.rs index 76f709dc..8fdf2388 100644 --- a/webauthn-authenticator-rs/src/usb/mod.rs +++ b/webauthn-authenticator-rs/src/usb/mod.rs @@ -13,6 +13,8 @@ //! Windows instead. mod framing; mod responses; +#[cfg(any(all(doc, not(doctest)), feature = "vendor-solokey"))] +mod solokey; use fido_hid_rs::{ HidReportBytes, HidSendReportBytes, USBDevice, USBDeviceImpl, USBDeviceInfo, USBDeviceInfoImpl, diff --git a/webauthn-authenticator-rs/src/usb/responses.rs b/webauthn-authenticator-rs/src/usb/responses.rs index a8e20505..28b55420 100644 --- a/webauthn-authenticator-rs/src/usb/responses.rs +++ b/webauthn-authenticator-rs/src/usb/responses.rs @@ -1,9 +1,12 @@ //! All [Response] frame types, used by FIDO tokens over USB HID. use crate::error::WebauthnCError; -use crate::transport::iso7816::ISO7816ResponseAPDU; -use crate::transport::types::{ - CBORResponse, U2FError, TYPE_INIT, U2FHID_CBOR, U2FHID_ERROR, U2FHID_KEEPALIVE, U2FHID_MSG, - U2FHID_PING, +use crate::transport::{ + iso7816::ISO7816ResponseAPDU, + types::{ + CBORResponse, U2FError, U2FHID_CBOR, U2FHID_ERROR, U2FHID_KEEPALIVE, U2FHID_MSG, + U2FHID_PING, + }, + TYPE_INIT, }; use crate::usb::framing::U2FHIDFrame; use crate::usb::*; diff --git a/webauthn-authenticator-rs/src/usb/solokey.rs b/webauthn-authenticator-rs/src/usb/solokey.rs new file mode 100644 index 00000000..323061bf --- /dev/null +++ b/webauthn-authenticator-rs/src/usb/solokey.rs @@ -0,0 +1,122 @@ +use async_trait::async_trait; +use uuid::Uuid; + +#[cfg(all(feature = "usb", feature = "vendor-solokey"))] +use crate::transport::solokey::{CMD_LOCK, CMD_RANDOM, CMD_UUID, CMD_VERSION}; + +use crate::{ + prelude::WebauthnCError, + transport::{ + solokey::SoloKeyToken, + types::{U2FError, U2FHID_ERROR}, + }, + usb::{framing::U2FHIDFrame, USBToken}, +}; + +#[async_trait] +impl SoloKeyToken for USBToken { + async fn get_solokey_lock(&mut self) -> Result { + let cmd = U2FHIDFrame { + cid: self.cid, + cmd: CMD_LOCK, + len: 0, + data: vec![], + }; + self.send_one(&cmd).await?; + + let r = self.recv_one().await?; + match r.cmd { + CMD_LOCK => { + if r.len != 1 || r.data.len() != 1 { + return Err(WebauthnCError::InvalidMessageLength); + } + + Ok(r.data[0] != 0) + } + + U2FHID_ERROR => Err(U2FError::from(r.data.as_slice()).into()), + + _ => Err(WebauthnCError::UnexpectedState), + } + } + + async fn get_solokey_random(&mut self) -> Result<[u8; 57], WebauthnCError> { + let cmd = U2FHIDFrame { + cid: self.cid, + cmd: CMD_RANDOM, + len: 0, + data: vec![], + }; + self.send_one(&cmd).await?; + + let r = self.recv_one().await?; + match r.cmd { + CMD_RANDOM => r + .data + .try_into() + .map_err(|_| WebauthnCError::InvalidMessageLength), + + U2FHID_ERROR => Err(U2FError::from(r.data.as_slice()).into()), + + _ => Err(WebauthnCError::UnexpectedState), + } + } + + async fn get_solokey_version(&mut self) -> Result { + let cmd = U2FHIDFrame { + cid: self.cid, + cmd: CMD_VERSION, + len: 0, + data: vec![], + }; + self.send_one(&cmd).await?; + + let r = self.recv_one().await?; + match r.cmd { + CMD_VERSION => { + let u = u32::from_be_bytes( + r.data + .try_into() + .map_err(|_| WebauthnCError::InvalidMessageLength)?, + ); + + Ok(u) + } + + U2FHID_ERROR => Err(U2FError::from(r.data.as_slice()).into()), + + _ => Err(WebauthnCError::UnexpectedState), + } + } + + async fn get_solokey_uuid(&mut self) -> Result { + let cmd = U2FHIDFrame { + cid: self.cid, + cmd: CMD_UUID, + len: 0, + data: vec![], + }; + self.send_one(&cmd).await?; + + let r = self.recv_one().await?; + match r.cmd { + CMD_UUID => { + if r.len != 16 || r.data.len() != 16 { + return Err(WebauthnCError::InvalidMessageLength); + } + + let u = Uuid::from_bytes( + r.data + .try_into() + .map_err(|_| WebauthnCError::InvalidMessageLength)?, + ); + + Ok(u) + } + + U2FHID_ERROR => Err(U2FError::from(r.data.as_slice()).into()), + + _ => Err(WebauthnCError::UnexpectedState), + } + } +}