Skip to content

Commit

Permalink
Add support for cable authenticators (as an initiator and authenticat…
Browse files Browse the repository at this point in the history
…or).
  • Loading branch information
micolous committed Jan 12, 2023
1 parent f99ed1f commit 81df802
Show file tree
Hide file tree
Showing 18 changed files with 3,747 additions and 1 deletion.
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ jobs:
rust_version: [stable, 1.45.0]
features:
- ""
- --features cable
- --features nfc
- --features usb
- --features nfc,usb
- --features cable,nfc,usb
os:
- ubuntu-latest
- windows-latest
Expand All @@ -24,7 +26,7 @@ jobs:
features: --features win10
rust_version: stable
- os: windows-latest
features: --features nfc,usb,win10
features: --features cable,nfc,usb,win10
rust_version: stable
exclude:
- os: windows-latest
Expand All @@ -46,6 +48,8 @@ jobs:
run: sudo apt-get -y install libpcsclite-dev
- if: contains(matrix.features, 'usb') && runner.os != 'windows'
run: sudo apt-get -y install libusb-1.0-0-dev
- if: contains(matrix.features, 'cable') && runner.os != 'windows'
run: sudo apt-get -y install libdbus-1-dev

- if: runner.os == 'windows'
uses: johnwason/vcpkg-action@v4
Expand Down
21 changes: 21 additions & 0 deletions webauthn-authenticator-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ repository = "https://github.com/kanidm/webauthn-rs"
nfc_raw_transmit = ["nfc"]
u2fhid = ["authenticator"]

# BTLE authenticators are not yet supported - this is currently only for caBLE
bluetooth = ["btleplug"]

# caBLE / hybrid authenticator
cable = ["bluetooth", "hex", "tokio", "tokio-tungstenite", "qrcode"]
nfc = ["pcsc"]
usb = ["hidapi"]
win10 = ["windows"]
Expand Down Expand Up @@ -52,9 +57,25 @@ async-trait = "0.1.58"
futures = "0.3.25"

qrcode = { version = "^0.12.0", optional = true }
# btleplug pinned due to https://github.com/deviceplug/btleplug/issues/289
# Advertisements for the same device get dropped by bluez (Linux).
btleplug = { git = "https://github.com/deviceplug/btleplug.git", rev = "bb3212df36cfd00a1d5788de790240858847a5f1", optional = true }
tokio = { version = "1.22.0", features = ["sync", "test-util", "macros", "rt-multi-thread", "time"], optional = true }
tokio-tungstenite = { version = "0.18.0", features = ["native-tls"], optional = true }
hex = { version = "0.4.3", optional = true }

[dev-dependencies]
tracing-subscriber = { version = "0.3", features = ["env-filter", "std", "fmt"] }
clap = { version = "^3.2", features = ["derive", "env"] }
tokio = { version = "1.22.0", features = ["sync", "test-util", "macros", "rt-multi-thread", "time"] }
tempfile = { version = "3.3.0" }

# cable_tunnel - used for connecting to Bluetooth HCI controller over serial
serialport = { version = "4.2.0" }
serialport-hci = { git = "https://github.com/micolous/serialport-hci.git", rev = "7931ad32510ac162f9c4e1147bdd411e40cffa0e" }
bluetooth-hci = { git = "https://github.com/micolous/bluetooth-hci.git", rev = "04f98f734b0b9f0304e433335357307f63f6bc26" }
bardecoder = "0.4.0"
image = "0.23"

[build-dependencies]
openssl = "0.10"
10 changes: 10 additions & 0 deletions webauthn-authenticator-rs/examples/authenticate/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ enum Provider {
/// Requires administrative permissions on Windows.
Ctap,

#[cfg(feature = "cable")]
/// caBLE/Hybrid authenticator, using a QR code, BTLE and Websockets.
Cable,

#[cfg(feature = "u2fhid")]
/// Mozilla webauthn-authenticator-rs provider, supporting USB HID only.
Mozilla,
Expand Down Expand Up @@ -94,6 +98,12 @@ impl Provider {
}
}
Provider::Ctap => Box::new(select_transport(ui)),
#[cfg(feature = "cable")]
Provider::Cable => Box::new(
webauthn_authenticator_rs::cable::connect_cable_authenticator(request_type, ui)
.await
.unwrap(),
),
#[cfg(feature = "u2fhid")]
Provider::Mozilla => Box::new(webauthn_authenticator_rs::u2fhid::U2FHid::default()),
#[cfg(feature = "win10")]
Expand Down
243 changes: 243 additions & 0 deletions webauthn-authenticator-rs/examples/cable_tunnel/core.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
use bluetooth_hci::{
host::{
uart::{CommandHeader, Hci as UartHci, Packet},
AdvertisingFilterPolicy, AdvertisingParameters, Channels, Hci, OwnAddressType,
},
types::{Advertisement, AdvertisingInterval, AdvertisingType},
BdAddr, BdAddrType,
};
use clap::{ArgGroup, Parser};
use openssl::rand::rand_bytes;
use serialport::FlowControl;
use serialport_hci::{
vendor::none::{Event, Vendor},
SerialController,
};
use std::{fmt::Debug, fs::OpenOptions, time::Duration};

use webauthn_authenticator_rs::{
cable::{share_cable_authenticator, Advertiser},
ctap2::CtapAuthenticator,
error::WebauthnCError,
softtoken::SoftTokenFile,
transport::{AnyTransport, Transport},
ui::Cli,
};

#[derive(Debug, clap::Parser)]
#[clap(about = "caBLE tunneler tool")]
#[clap(group(
ArgGroup::new("url")
.required(true)
.args(&["cable-url", "qr-image"])
))]
pub struct CliParser {
/// Serial port where Bluetooth HCI controller is connected to.
///
/// This has primarily been tested using a Nordic nRF52 microcontroller
/// running [Apache Mynewt's NimBLE HCI demo][0].
///
/// [0]: https://mynewt.apache.org/latest/tutorials/ble/blehci_project.html
#[clap(short, long)]
pub serial_port: String,

/// Baud rate for communication with Bluetooth HCI controller.
///
/// By default, Apache Mynewt's NimBLE HCI demo runs at 1000000 baud.
#[clap(short, long, default_value = "1000000")]
pub baud_rate: u32,

/// Tunnel server ID to use. 0 = Google.
#[clap(short, long, default_value = "0")]
pub tunnel_server_id: u16,

/// `FIDO:/` URL from the initiator's QR code. Either this option or
/// --qr-image is required.
#[clap(short, long)]
pub cable_url: Option<String>,

/// Screenshot of the initator's QR code. Either this option or --cable-url
/// is required.
///
/// Use `adb` to screenshot an Android device with USB debugging:
///
/// adb exec-out screencap -p > screenshot.png
///
/// Use Xcode to screenshot an iOS device with debugging. There is no need
/// to open an Xcode project: from the `Window` menu, select
/// `Devices and Simulators`, then select the device, and press
/// `Take Screenshot`. The screenshot will be saved to `~/Desktop`.
///
/// Note: this *will not* work in the Android emulator or iOS simulator,
/// because they do not have access to a physical Bluetooth controller.
#[clap(short, long)]
pub qr_image: Option<String>,

/// Path to saved SoftToken.
///
/// You can create a new SoftToken with the `softtoken` example:
///
/// cargo run --example softtoken -- create /tmp/softtoken.dat
///
/// If this option is not specified, `cable_tunnel` will attempt to connect
/// to the first supported physical token using AnyTransport. Most initators
/// (browsers) will *also* attempt to connect directly to *all* physical
/// tokens, so only use this if your initator is running on another device!
#[clap(long)]
pub softtoken_path: Option<String>,
}

struct SerialHciAdvertiser {
hci: SerialController<CommandHeader, Vendor>,
}

impl SerialHciAdvertiser {
fn new(serial_port: &str, baud_rate: u32) -> Self {
let port = serialport::new(serial_port, baud_rate)
.timeout(Duration::from_secs(2))
.flow_control(FlowControl::None)
.open()
.unwrap();
Self {
hci: SerialController::new(port),
}
}

fn read(&mut self) -> Packet<Event> {
let r = self.hci.read().unwrap();
trace!("<<< {:?}", r);
r
}
}

impl Advertiser for SerialHciAdvertiser {
fn stop_advertising(&mut self) -> Result<(), WebauthnCError> {
trace!("sending reset...");
self.hci.reset().unwrap();
let _ = self.read();

self.hci.le_set_advertise_enable(false).unwrap();
let _ = self.read();
Ok(())
}

fn start_advertising(
&mut self,
service_uuid: u16,
payload: &[u8],
) -> Result<(), WebauthnCError> {
self.stop_advertising()?;
let advert = Advertisement::ServiceData16BitUuid(service_uuid, payload);
let mut service_data = [0; 31];
let len = advert.copy_into_slice(&mut service_data);

let p = AdvertisingParameters {
advertising_interval: AdvertisingInterval::for_type(
AdvertisingType::NonConnectableUndirected,
)
.with_range(Duration::from_millis(100), Duration::from_millis(500))
.unwrap(),
own_address_type: OwnAddressType::Random,
peer_address: BdAddrType::Random(bluetooth_hci::BdAddr([0xc0; 6])),
advertising_channel_map: Channels::all(),
advertising_filter_policy: AdvertisingFilterPolicy::WhiteListConnectionAllowScan,
};
let mut addr = [0u8; 6];
addr[5] = 0xc0;
rand_bytes(&mut addr[..5])?;

self.hci.le_set_random_address(BdAddr(addr)).unwrap();
let _ = self.read();

self.hci.le_set_advertising_parameters(&p).unwrap();
let _ = self.read();

self.hci
.le_set_advertising_data(&service_data[..len])
.unwrap();
let _ = self.read();

self.hci.le_set_advertise_enable(true).unwrap();
let _ = self.read();
Ok(())
}
}

#[tokio::main]
pub(super) async fn main() {
let opt = CliParser::parse();
let cable_url = if let Some(u) = opt.cable_url {
u
} else if let Some(img) = opt.qr_image {
let img = image::open(img).unwrap();
// Optimised for screenshots from the device.
let img = img.adjust_contrast(9000.0);

let decoder = bardecoder::default_decoder();
let fido_url = decoder
.decode(&img)
.into_iter()
.filter_map(|r| {
trace!(?r);
r.ok()
})
.find(|u| {
trace!("Found QR code: {:?}", u);
let u = u.to_ascii_uppercase();
u.starts_with("FIDO:/")
});
match fido_url {
Some(u) => u,
None => {
panic!("Could not find any FIDO URLs in the image");
}
}
} else {
unreachable!();
};

let mut advertiser = SerialHciAdvertiser::new(&opt.serial_port, opt.baud_rate);
let ui = Cli {};

if let Some(p) = opt.softtoken_path {
// Use a SoftToken
let f = OpenOptions::new()
.read(true)
.write(true)
.create(false)
.open(p)
.unwrap();
let mut softtoken = SoftTokenFile::open(f).unwrap();
let info = softtoken.as_ref().get_info();

share_cable_authenticator(
&mut softtoken,
info,
cable_url.trim(),
opt.tunnel_server_id,
&mut advertiser,
&ui,
true,
)
.await
.unwrap();
} else {
// Use a physical authenticator
let mut transport = AnyTransport::new().unwrap();
let token = transport.tokens().unwrap().pop().unwrap();
let mut authenticator = CtapAuthenticator::new(token, &ui).await.unwrap();
let info = authenticator.get_info().to_owned();

share_cable_authenticator(
&mut authenticator,
info,
cable_url.trim(),
opt.tunnel_server_id,
&mut advertiser,
&ui,
true,
)
.await
.unwrap();
};
}
17 changes: 17 additions & 0 deletions webauthn-authenticator-rs/examples/cable_tunnel/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//! `cable_tunnel` shares a [Token] over a caBLE connection.

#[macro_use]
extern crate tracing;

#[cfg(feature = "cable")]
mod core;

fn main() {
let _ = tracing_subscriber::fmt::try_init();

#[cfg(feature = "cable")]
core::main();

#[cfg(not(feature = "cable"))]
error!("This example requires the feature \"cable\" to be enabled.");
}
Loading

0 comments on commit 81df802

Please sign in to comment.