Skip to content
Merged
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ _Looking for the D-Bus API proposal?_ Check out [platform-api][linux-credentials
- [Passkey Authentication][passkeys]
- 🟢 Discoverable credentials (resident keys)
- 🟢 Hybrid transport (caBLE v2): QR-initiated transactions
- 🟠 Hybrid transport (caBLE v2): State-assisted transactions ([#31][#31]: planned)
- 🟢 Hybrid transport (caBLE v2): State-assisted transactions (remember this phone)

## Transports

| | USB (HID) | Bluetooth Low Energy (BLE) | NFC | TPM 2.0 (Platform) | Hybrid (caBLEv2) |
| -------------------- | ------------------------- | -------------------------- | --------------------- | --------------------- | ---------------------------------- |
| **FIDO U2F** | 🟢 Supported (via hidapi) | 🟢 Supported (via bluez) | 🟠 Planned ([#5][#5]) | 🟠 Planned ([#4][#4]) | N/A |
| **WebAuthn (FIDO2)** | 🟢 Supported (via hidapi) | 🟢 Supported (via bluez) | 🟠 Planned ([#5][#5]) | 🟠 Planned ([#4][#4]) | 🟠 Partly implemented ([#31][#31]) |
| **WebAuthn (FIDO2)** | 🟢 Supported (via hidapi) | 🟢 Supported (via bluez) | 🟠 Planned ([#5][#5]) | 🟠 Planned ([#4][#4]) | 🟢 Supported |

## Example programs

Expand Down
141 changes: 73 additions & 68 deletions libwebauthn/examples/webauthn_cable.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use std::error::Error;
use std::io::{self, Write};
use std::sync::Arc;
use std::time::Duration;

use libwebauthn::pin::PinRequestReason;
use libwebauthn::transport::cable::known_devices::{
CableKnownDeviceInfoStore, EphemeralDeviceInfoStore,
CableKnownDevice, ClientPayloadHint, EphemeralDeviceInfoStore,
};
use libwebauthn::transport::cable::qr_code_device::{CableQrCodeDevice, QrCodeOperationHint};
use libwebauthn::UxUpdate;
Expand All @@ -13,6 +14,7 @@ use qrcode::QrCode;
use rand::{thread_rng, Rng};
use text_io::read;
use tokio::sync::mpsc::Receiver;
use tokio::time::sleep;
use tracing_subscriber::{self, EnvFilter};

use libwebauthn::ops::webauthn::{
Expand Down Expand Up @@ -78,66 +80,71 @@ async fn handle_updates(mut state_recv: Receiver<UxUpdate>) {
pub async fn main() -> Result<(), Box<dyn Error>> {
setup_logging();

let _device_info_store: Box<dyn CableKnownDeviceInfoStore> =
Box::new(EphemeralDeviceInfoStore::default());

// Create QR code
let mut device: CableQrCodeDevice<'_> =
CableQrCodeDevice::new_transient(QrCodeOperationHint::MakeCredential);

println!("Created QR code, awaiting for advertisement.");
let qr_code = QrCode::new(device.qr_code.to_string()).unwrap();
let image = qr_code
.render::<unicode::Dense1x2>()
.dark_color(unicode::Dense1x2::Light)
.light_color(unicode::Dense1x2::Dark)
.build();
println!("{}", image);

// Connect to a known device
let (mut channel, state_recv) = device.channel().await.unwrap();
println!("Tunnel established {:?}", channel);

tokio::spawn(handle_updates(state_recv));

let device_info_store = Arc::new(EphemeralDeviceInfoStore::default());
let user_id: [u8; 32] = thread_rng().gen();
let challenge: [u8; 32] = thread_rng().gen();

// Make Credentials ceremony
let make_credentials_request = MakeCredentialRequest {
origin: "example.org".to_owned(),
hash: Vec::from(challenge),
relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"),
user: Ctap2PublicKeyCredentialUserEntity::new(&user_id, "mario.rossi", "Mario Rossi"),
require_resident_key: false,
user_verification: UserVerificationRequirement::Preferred,
algorithms: vec![Ctap2CredentialType::default()],
exclude: None,
extensions: None,
timeout: TIMEOUT,
};
let credential: Ctap2PublicKeyCredentialDescriptor = {
// Create QR code
let mut device: CableQrCodeDevice = CableQrCodeDevice::new_persistent(
QrCodeOperationHint::MakeCredential,
device_info_store.clone(),
);

println!("Created QR code, awaiting for advertisement.");
let qr_code = QrCode::new(device.qr_code.to_string()).unwrap();
let image = qr_code
.render::<unicode::Dense1x2>()
.dark_color(unicode::Dense1x2::Light)
.light_color(unicode::Dense1x2::Dark)
.build();
println!("{}", image);

// Connect to a known device
let (mut channel, state_recv) = device.channel().await.unwrap();
println!("Tunnel established {:?}", channel);

tokio::spawn(handle_updates(state_recv));

// Make Credentials ceremony
let make_credentials_request = MakeCredentialRequest {
origin: "example.org".to_owned(),
hash: Vec::from(challenge),
relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"),
user: Ctap2PublicKeyCredentialUserEntity::new(&user_id, "mario.rossi", "Mario Rossi"),
require_resident_key: false,
user_verification: UserVerificationRequirement::Preferred,
algorithms: vec![Ctap2CredentialType::default()],
exclude: None,
extensions: None,
timeout: TIMEOUT,
};

let response = loop {
match channel
.webauthn_make_credential(&make_credentials_request)
.await
{
Ok(response) => break Ok(response),
Err(WebAuthnError::Ctap(ctap_error)) => {
if ctap_error.is_retryable_user_error() {
println!("Oops, try again! Error: {}", ctap_error);
continue;
let response = loop {
match channel
.webauthn_make_credential(&make_credentials_request)
.await
{
Ok(response) => break Ok(response),
Err(WebAuthnError::Ctap(ctap_error)) => {
if ctap_error.is_retryable_user_error() {
println!("Oops, try again! Error: {}", ctap_error);
continue;
}
break Err(WebAuthnError::Ctap(ctap_error));
}
break Err(WebAuthnError::Ctap(ctap_error));
}
Err(err) => break Err(err),
};
}
.unwrap();
println!("WebAuthn MakeCredential response: {:?}", response);
Err(err) => break Err(err),
};
}
.unwrap();
println!("WebAuthn MakeCredential response: {:?}", response);

(&response.authenticator_data).try_into().unwrap()
};

println!("Waiting for 5 seconds before contacting the device...");
sleep(Duration::from_secs(5)).await;

let credential: Ctap2PublicKeyCredentialDescriptor =
(&response.authenticator_data).try_into().unwrap();
let get_assertion = GetAssertionRequest {
relying_party_id: "example.org".to_owned(),
hash: Vec::from(challenge),
Expand All @@ -147,22 +154,20 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
timeout: TIMEOUT,
};

// Create QR code
let mut device: CableQrCodeDevice<'_> =
CableQrCodeDevice::new_transient(QrCodeOperationHint::GetAssertionRequest);
let all_devices = device_info_store.list_all().await;
let (_known_device_id, known_device_info) =
all_devices.first().expect("No known devices found");

println!("Created QR code, awaiting for advertisement.");
let qr_code = QrCode::new(device.qr_code.to_string()).unwrap();
let image = qr_code
.render::<unicode::Dense1x2>()
.dark_color(unicode::Dense1x2::Light)
.light_color(unicode::Dense1x2::Dark)
.build();
println!("{}", image);
let mut known_device: CableKnownDevice = CableKnownDevice::new(
ClientPayloadHint::GetAssertion,
known_device_info,
device_info_store.clone(),
)
.await
.unwrap();

// Connect to a known device
println!("Tunnel established {:?}", channel);
let (mut channel, state_recv) = device.channel().await.unwrap();
let (mut channel, state_recv) = known_device.channel().await.unwrap();
println!("Tunnel established {:?}", channel);

tokio::spawn(handle_updates(state_recv));
Expand Down
6 changes: 3 additions & 3 deletions libwebauthn/src/transport/ble/btleplug/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ async fn on_peripheral_service_data(
id: &PeripheralId,
uuids: &[Uuid],
service_data: HashMap<Uuid, Vec<u8>>,
) -> Option<(Peripheral, Vec<u8>)> {
) -> Option<(Adapter, Peripheral, Vec<u8>)> {
for uuid in uuids {
if let Some(service_data) = service_data.get(uuid) {
trace!(?id, ?service_data, "Found service data");
Expand All @@ -66,7 +66,7 @@ async fn on_peripheral_service_data(
};

debug!({ ?id, ?service_data }, "Found service data for peripheral");
return Some((peripheral, service_data.to_owned()));
return Some((adapter.clone(), peripheral, service_data.to_owned()));
}
}

Expand All @@ -81,7 +81,7 @@ async fn on_peripheral_service_data(
/// Starts a discovery for devices advertising service data on any of the provided UUIDs
pub async fn start_discovery_for_service_data(
uuids: &[Uuid],
) -> Result<impl Stream<Item = (Peripheral, Vec<u8>)> + use<'_>, Error> {
) -> Result<impl Stream<Item = (Adapter, Peripheral, Vec<u8>)> + use<'_>, Error> {
let adapter = get_adapter().await?;
let scan_filter = ScanFilter::default();

Expand Down
90 changes: 90 additions & 0 deletions libwebauthn/src/transport/cable/advertisement.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use ::btleplug::api::Central;
use futures::StreamExt;
use std::pin::pin;
use tracing::{debug, trace, warn};
use uuid::Uuid;

use crate::transport::ble::btleplug::{self, FidoDevice};
use crate::transport::cable::crypto::trial_decrypt_advert;
use crate::webauthn::{Error, TransportError};

const CABLE_UUID_FIDO: &str = "0000fff9-0000-1000-8000-00805f9b34fb";
const CABLE_UUID_GOOGLE: &str = "0000fde2-0000-1000-8000-00805f9b34fb";

#[derive(Debug)]
pub(crate) struct DecryptedAdvert {
pub plaintext: [u8; 16],
pub nonce: [u8; 10],
pub routing_id: [u8; 3],
pub encoded_tunnel_server_domain: u16,
}

impl From<[u8; 16]> for DecryptedAdvert {
fn from(plaintext: [u8; 16]) -> Self {
let mut nonce = [0u8; 10];
nonce.copy_from_slice(&plaintext[1..11]);
let mut routing_id = [0u8; 3];
routing_id.copy_from_slice(&plaintext[11..14]);
let encoded_tunnel_server_domain = u16::from_le_bytes([plaintext[14], plaintext[15]]);
let mut plaintext_fixed = [0u8; 16];
plaintext_fixed.copy_from_slice(&plaintext[..16]);
Self {
plaintext: plaintext_fixed,
nonce,
routing_id,
encoded_tunnel_server_domain,
}
}
}

pub(crate) async fn await_advertisement(
eid_key: &[u8],
) -> Result<(FidoDevice, DecryptedAdvert), Error> {
let uuids = &[
Uuid::parse_str(CABLE_UUID_FIDO).unwrap(),
Uuid::parse_str(CABLE_UUID_GOOGLE).unwrap(), // Deprecated, but may still be in use.
];
let stream = btleplug::manager::start_discovery_for_service_data(uuids)
.await
.or(Err(Error::Transport(TransportError::TransportUnavailable)))?;

let mut stream = pin!(stream);
while let Some((adapter, peripheral, data)) = stream.as_mut().next().await {
debug!({ ?peripheral, ?data }, "Found device with service data");

let Some(device) = btleplug::manager::get_device(peripheral.clone())
.await
.or(Err(Error::Transport(TransportError::TransportUnavailable)))?
else {
warn!(
?peripheral,
"Unable to fetch peripheral properties, ignoring"
);
continue;
};

trace!(?device, ?data, ?eid_key);
let Some(decrypted) = trial_decrypt_advert(&eid_key, &data) else {
warn!(?device, "Trial decrypt failed, ignoring");
continue;
};
trace!(?decrypted);

let advert = DecryptedAdvert::from(decrypted);
debug!(
?device,
?decrypted,
"Successfully decrypted advertisement from device"
);

adapter
.stop_scan()
.await
.or(Err(Error::Transport(TransportError::TransportUnavailable)))?;

return Ok((device, advert));
}

warn!("BLE advertisement discovery stream terminated");
Err(Error::Transport(TransportError::TransportUnavailable))
}
20 changes: 12 additions & 8 deletions libwebauthn/src/transport/cable/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,35 +22,39 @@ use super::qr_code_device::CableQrCodeDevice;

#[derive(Debug)]
pub enum CableChannelDevice<'d> {
QrCode(&'d CableQrCodeDevice<'d>),
Known(&'d CableKnownDevice<'d>),
QrCode(&'d CableQrCodeDevice),
Known(&'d CableKnownDevice),
}

#[derive(Debug)]
pub struct CableChannel<'d> {
pub struct CableChannel {
/// The WebSocket stream used for communication.
// pub(crate) ws_stream: WebSocketStream<MaybeTlsStream<TcpStream>>,

/// The noise state used for encryption over the WebSocket stream.
// pub(crate) noise_state: TransportState,

/// The device that this channel is connected to.
pub device: CableChannelDevice<'d>,

pub(crate) handle_connection: task::JoinHandle<()>,
pub(crate) cbor_sender: mpsc::Sender<CborRequest>,
pub(crate) cbor_receiver: mpsc::Receiver<CborResponse>,
pub(crate) tx: mpsc::Sender<UxUpdate>,
}

impl Display for CableChannel<'_> {
impl Display for CableChannel {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "CableChannel")
}
}

impl Drop for CableChannel {
fn drop(&mut self) {
self.handle_connection.abort();
}
}

#[async_trait]
impl<'d> Channel for CableChannel<'d> {
impl<'d> Channel for CableChannel {
async fn supported_protocols(&self) -> Result<SupportedProtocols, Error> {
Ok(SupportedProtocols::fido2_only())
}
Expand Down Expand Up @@ -111,7 +115,7 @@ impl<'d> Channel for CableChannel<'d> {
}
}

impl<'d> Ctap2AuthTokenStore for CableChannel<'d> {
impl<'d> Ctap2AuthTokenStore for CableChannel {
fn store_auth_data(&mut self, _auth_token_data: AuthTokenData) {}

fn get_auth_data(&self) -> Option<&AuthTokenData> {
Expand Down
Loading
Loading