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 0172a9a commit 522733f
Show file tree
Hide file tree
Showing 13 changed files with 214 additions and 20 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
60 changes: 45 additions & 15 deletions webauthn-authenticator-rs/src/nfc/atr.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
//! ISO/IEC 7816-3 _Answer-to-Reset_ and 7816-4 _Historical Bytes_ parser.
use std::collections::{HashSet, VecDeque};

#[cfg(feature = "nfc")]
use pcsc::MAX_ATR_SIZE;

Expand Down Expand Up @@ -32,7 +34,7 @@ use super::tlv::*;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Atr {
/// Supported protocols (`T=`), specified in ISO/IEC 7816-3:2006 §8.2.3.
pub protocols: Vec<u8>,
pub protocols: HashSet<u8>,

/// Historical bytes (T<sub>1</sub> .. T<sub>k</sub>), as specified in
/// ISO/IEC 7816-4:2005 §8.1.1.
Expand Down Expand Up @@ -113,7 +115,7 @@ impl TryFrom<&[u8]> for Atr {
return Err(WebauthnCError::MessageTooShort);
}

let mut nibbles = Vec::with_capacity(MAX_ATR_SIZE);
let mut nibbles = VecDeque::with_capacity(MAX_ATR_SIZE);
// Byte 0 intentionally skipped

// Calculate checksum (TCK), present unless the only protocol is T=0:
Expand All @@ -129,30 +131,32 @@ impl TryFrom<&[u8]> for Atr {
let mut i: usize = 1;
loop {
let y = atr[i] >> 4;
nibbles.push(atr[i] & 0x0f);
nibbles.push_back(atr[i] & 0x0f);
i += 1;

// skip Ta, Tb, Tc fields
i += (y & 0x7) as usize;
i += (y & 0x7).count_ones() as usize;
if y & 0x8 == 0 {
/* Td = 0 */
break;
}
}

let t1_len = nibbles[0] as usize;
let protocols = if nibbles.len() > 1 {
&nibbles[1..]
let t1_len = nibbles.pop_front().unwrap_or_default() as usize;

let protocols = if nibbles.len() >= 1 {
HashSet::from_iter(nibbles.into_iter())
} else {
// If TD1 is absent, the only offer is T=0.
&PROTOCOL_T0
HashSet::from(PROTOCOL_T0)
};

let mut storage_card = false;
let mut command_chaining = None;
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 Expand Up @@ -199,7 +203,7 @@ impl TryFrom<&[u8]> for Atr {
}

Ok(Atr {
protocols: protocols.to_vec(),
protocols,
t1: t1.to_vec(),
storage_card,
command_chaining,
Expand Down Expand Up @@ -232,7 +236,7 @@ mod tests {
0x4b, 0x65, 0xff, 0x7f,
];
let expected = Atr {
protocols: [0, 1].to_vec(),
protocols: HashSet::from([0, 1]),
t1: [
0x80, 0x73, 0xc0, 0x21, 0xc0, 0x57, 0x59, 0x75, 0x62, 0x69, 0x4b, 0x65, 0xff,
]
Expand All @@ -256,7 +260,7 @@ mod tests {
0x4b, 0x65, 0x79, 0xf9,
];
let expected = Atr {
protocols: [0, 1].to_vec(),
protocols: HashSet::from([0, 1]),
t1: [
0x80, 0x73, 0xc0, 0x21, 0xc0, 0x57, 0x59, 0x75, 0x62, 0x69, 0x4b, 0x65, 0x79,
]
Expand All @@ -273,11 +277,37 @@ mod tests {
assert_eq!("YubiKey", actual.card_issuers_data_str().unwrap());
}

#[test]
fn yubico_yubikey_5c_usb_macos() {
let _ = tracing_subscriber::fmt().try_init();
let input = [
0x3b, 0xfd, 0x13, 0x00, 0x00, 0x81, 0x31, 0xfe, 0x15, 0x80, 0x73, 0xc0, 0x21, 0xc0,
0x57, 0x59, 0x75, 0x62, 0x69, 0x4b, 0x65, 0x79, 0x40,
];
let expected = Atr {
// T=1 repeated twice
protocols: HashSet::from([1]),
t1: [
0x80, 0x73, 0xc0, 0x21, 0xc0, 0x57, 0x59, 0x75, 0x62, 0x69, 0x4b, 0x65, 0x79,
]
.to_vec(),
storage_card: false,
// "YubiKey"
card_issuers_data: Some([0x59, 0x75, 0x62, 0x69, 0x4b, 0x65, 0x79].to_vec()),
command_chaining: Some(true),
extended_lc: Some(true),
};

let actual = Atr::try_from(&input[..]).expect("yubico_yubikey_5c_usb_macos ATR");
assert_eq!(expected, actual);
assert_eq!("YubiKey", actual.card_issuers_data_str().unwrap());
}

#[test]
fn desfire_storage_card() {
let input = [0x3b, 0x81, 0x80, 0x01, 0x80, 0x80];
let expected = Atr {
protocols: [0, 1].to_vec(),
protocols: HashSet::from([0, 1]),
t1: [0x80].to_vec(),
storage_card: false,
card_issuers_data: None,
Expand All @@ -297,7 +327,7 @@ mod tests {
0x3b, 0x00, 0x00, 0x00, 0x00, 0x42,
];
let expected = Atr {
protocols: [0, 1].to_vec(),
protocols: HashSet::from([0, 1]),
t1: [
0x80, 0x4f, 0x0c, 0xa0, 0x00, 0x00, 0x03, 0x06, 0x11, 0x00, 0x3b, 0x00, 0x00, 0x00,
0x00,
Expand All @@ -319,7 +349,7 @@ mod tests {
// These have a 1 and 2 byte tag 0x7X, so command chaining and extended
// lc support isn't available.
let i1 = [0x3b, 0x83, 0x80, 0x01, 0x80, 0x71, 0xc0, 0x33];
let expected_protocols = [0, 1].to_vec();
let expected_protocols = HashSet::from([0, 1]);
let a1 = Atr::try_from(&i1[..]).expect("short caps atr1");

assert_eq!(expected_protocols, a1.protocols);
Expand All @@ -339,7 +369,7 @@ mod tests {
#[test]
fn edge_cases() {
let expected = Atr {
protocols: [0].to_vec(),
protocols: HashSet::from([0]),
t1: [].to_vec(),
storage_card: false,
card_issuers_data: None,
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
Loading

0 comments on commit 522733f

Please sign in to comment.