Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement some SoloKeys2/Trussed vendor commands #377

Merged
merged 12 commits into from
Oct 27, 2023
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ tokio = { version = "1.22.0", features = [
"sync",
"test-util",
"macros",
"net",
"rt-multi-thread",
"time",
] }
Expand Down
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
43 changes: 42 additions & 1 deletion fido-key-manager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
90 changes: 90 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,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)]
Expand Down Expand Up @@ -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<AnyToken, Cli> =
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));
}
}
}
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
4 changes: 2 additions & 2 deletions webauthn-authenticator-rs/src/bluetooth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
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
58 changes: 58 additions & 0 deletions webauthn-authenticator-rs/src/ctap2/solokey.rs
Original file line number Diff line number Diff line change
@@ -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<bool, WebauthnCError>;

/// 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<Uuid, WebauthnCError>;

/// Gets a SoloKey's firmware version.
async fn get_solokey_version(&mut self) -> Result<u32, WebauthnCError>;
}

#[async_trait]
impl<'a, T: Token + SoloKeyToken, U: UiCallback> SoloKeyAuthenticator
for Ctap20Authenticator<'a, T, U>
{
#[inline]
async fn get_solokey_lock(&mut self) -> Result<bool, WebauthnCError> {
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<Uuid, WebauthnCError> {
self.token.get_solokey_uuid().await
}

#[inline]
async fn get_solokey_version(&mut self) -> Result<u32, WebauthnCError> {
self.token.get_solokey_version().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
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
Loading
Loading