Skip to content

Commit

Permalink
wip: implementing SoloKey vendor commands
Browse files Browse the repository at this point in the history
  • Loading branch information
micolous committed Oct 25, 2023
1 parent 8878c0f commit 0ec75c7
Show file tree
Hide file tree
Showing 13 changed files with 170 additions and 5 deletions.
1 change: 1 addition & 0 deletions fido-key-manager/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down
38 changes: 38 additions & 0 deletions fido-key-manager/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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;
}
}
_ => (),
}
}
}
}
}
2 changes: 2 additions & 0 deletions webauthn-authenticator-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
7 changes: 7 additions & 0 deletions webauthn-authenticator-rs/src/ctap2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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].
Expand Down
22 changes: 22 additions & 0 deletions webauthn-authenticator-rs/src/ctap2/solokey.rs
Original file line number Diff line number Diff line change
@@ -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<Uuid, WebauthnCError>;
}

#[async_trait]
impl<'a, T: Token + SoloKeyToken, U: UiCallback> SoloKeyAuthenticator
for Ctap20Authenticator<'a, T, U>
{
async fn get_uuid(&mut self) -> Result<Uuid, WebauthnCError> {
self.token.get_solokey_uuid().await
}
}
9 changes: 9 additions & 0 deletions webauthn-authenticator-rs/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -141,6 +143,13 @@ impl From<btleplug::Error> for WebauthnCError {
}
}

#[cfg(feature = "usb")]
impl From<crate::transport::types::U2FError> for WebauthnCError {
fn from(value: crate::transport::types::U2FError) -> Self {
Self::U2F(value)
}
}

/// <https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#error-responses>
#[derive(Debug, PartialEq, Eq)]
pub enum CtapError {
Expand Down
1 change: 1 addition & 0 deletions webauthn-authenticator-rs/src/nfc/atr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
11 changes: 7 additions & 4 deletions webauthn-authenticator-rs/src/nfc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,13 @@ pub const APPLET_DF: [u8; 8] = [
/// indicate we should ignore it.
///
/// **See:** [`ignored_reader()`]
const IGNORED_READERS: [&str; 2] = [
// Nitrokey 3 exposes a CCID interface, which we can select the FIDO applet
// on, but it doesn't actually work.
"Nitrokey",
const IGNORED_READERS: [&str; 3] = [
// Trussed (used by Nitrokey 3 and SoloKeys Solo 2) expose a USB CCID
// interface which allows U2F applet selection, but then returns 0x6985
// (conditions of use not satisfied) to every command sent thereafter:
// https://github.com/trussed-dev/fido-authenticator/blob/7bd0c3bc5105a122fa11d9b354457746f391c4fb/src/dispatch/apdu.rs#L44-L48
// https://github.com/trussed-dev/fido-authenticator/issues/38
"Nitrokey", "SoloKey",
// YubiKey exposes a CCID interface when OpenGPG or PIV support is enabled,
// and this interface doesn't support FIDO.
"YubiKey",
Expand Down
5 changes: 5 additions & 0 deletions webauthn-authenticator-rs/src/transport/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<T: Token> {
Added(T),
Expand Down
29 changes: 29 additions & 0 deletions webauthn-authenticator-rs/src/transport/solokey.rs
Original file line number Diff line number Diff line change
@@ -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<Uuid, WebauthnCError>;
}

#[async_trait]
impl SoloKeyToken for AnyToken {
async fn get_solokey_uuid(&mut self) -> Result<Uuid, 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_uuid().await,
}
}
}
1 change: 0 additions & 1 deletion webauthn-authenticator-rs/src/transport/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions webauthn-authenticator-rs/src/usb/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
47 changes: 47 additions & 0 deletions webauthn-authenticator-rs/src/usb/solokey.rs
Original file line number Diff line number Diff line change
@@ -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<Uuid, WebauthnCError> {
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),
}
}
}

0 comments on commit 0ec75c7

Please sign in to comment.