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/src/main.rs b/fido-key-manager/src/main.rs index 1a7cb61a..cd3aa745 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,9 @@ pub enum Opt { DeleteCredential(DeleteCredentialOpt), /// Updates user information for a discoverable credential on this token. UpdateCredentialUser(UpdateCredentialUserOpt), + #[cfg(feature = "solokey")] + /// Gets a SoloKey's UUID. + SoloKeyUuid(InfoOpt), } #[derive(Debug, clap::Parser)] @@ -680,5 +686,37 @@ async fn main() { .await .expect("Error updating credential"); } + + #[cfg(feature = "solokey")] + Opt::SoloKeyUuid(o) => { + 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, + }; + + match authenticator.get_uuid().await { + Ok(uuid) => println!("SoloKey UUID: {uuid}"), + Err(WebauthnCError::NotSupported) + | Err(WebauthnCError::InvalidMessageLength) => { + println!("Device is not a SoloKey!") + } + Err(e) => panic!("could not get SoloKey UUID: {e:?}"), + } + } + TokenEvent::EnumerationComplete => { + if o.watch { + println!("Initial enumeration completed, watching for more devices..."); + println!("Press Ctrl + C to stop watching."); + } else { + break; + } + } + _ => (), + } + } + } } } 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/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..c42cfc10 --- /dev/null +++ b/webauthn-authenticator-rs/src/ctap2/solokey.rs @@ -0,0 +1,22 @@ +use async_trait::async_trait; +use uuid::Uuid; + +use crate::{ + prelude::WebauthnCError, transport::solokey::SoloKeyToken, transport::Token, ui::UiCallback, +}; + +use super::Ctap20Authenticator; + +#[async_trait] +pub trait SoloKeyAuthenticator { + async fn get_uuid(&mut self) -> Result; +} + +#[async_trait] +impl<'a, T: Token + SoloKeyToken, U: UiCallback> SoloKeyAuthenticator + for Ctap20Authenticator<'a, T, U> +{ + async fn get_uuid(&mut self) -> Result { + self.token.get_solokey_uuid().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/nfc/atr.rs b/webauthn-authenticator-rs/src/nfc/atr.rs index 4c11610a..c8f7761b 100644 --- a/webauthn-authenticator-rs/src/nfc/atr.rs +++ b/webauthn-authenticator-rs/src/nfc/atr.rs @@ -156,6 +156,7 @@ impl TryFrom<&[u8]> for Atr { let mut extended_lc = None; let mut card_issuers_data = None; if i + t1_len > atr.len() { + error!(?i, ?t1_len, "atr.len = {}", atr.len()); return Err(WebauthnCError::MessageTooShort); } let t1 = &atr[i..i + t1_len]; 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..09746015 --- /dev/null +++ b/webauthn-authenticator-rs/src/transport/solokey.rs @@ -0,0 +1,29 @@ +use async_trait::async_trait; +use uuid::Uuid; + +use crate::prelude::WebauthnCError; + +use super::AnyToken; + +#[cfg(all(feature = "usb", feature = "vendor-solokey"))] +pub const CMD_UUID: u8 = super::TYPE_INIT | 0x62; + +#[async_trait] +pub trait SoloKeyToken { + async fn get_solokey_uuid(&mut self) -> Result; +} + +#[async_trait] +impl SoloKeyToken for AnyToken { + 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..ad287fc5 100644 --- a/webauthn-authenticator-rs/src/transport/types.rs +++ b/webauthn-authenticator-rs/src/transport/types.rs @@ -3,7 +3,6 @@ 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/solokey.rs b/webauthn-authenticator-rs/src/usb/solokey.rs new file mode 100644 index 00000000..e82c12e7 --- /dev/null +++ b/webauthn-authenticator-rs/src/usb/solokey.rs @@ -0,0 +1,47 @@ +use async_trait::async_trait; +use uuid::Uuid; + +use crate::{ + prelude::WebauthnCError, + transport::{ + solokey::{SoloKeyToken, CMD_UUID}, + types::{U2FError, U2FHID_ERROR}, + }, + usb::framing::U2FHIDFrame, +}; + +use super::{USBToken, USBTransport}; + +#[async_trait] +impl SoloKeyToken for USBToken { + 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), + } + } +}