Skip to content

Commit e2789ad

Browse files
Hybrid transport: Add state-assisted transactions (#100)
## Changes * Updates internals of CableKnownDeviceInfoStore, to make it easier to use and thread-safe. * Adds parsing of caBLE update messages. This includes a Google-specific key (999). Due to a limitation in serde-indexed, parsing of this structure has to be done field-by-field with `serde_cbor`. To be fixed properly with #66. * Connect to known device from saved state. * Verifies handshake signature. Note this currently only works for linking information received on QR-initiated transactions. The protocol doesn't seen to support subsequent updates, as the ephemeral QR-code identity key is needed for verification.
1 parent 7397062 commit e2789ad

File tree

11 files changed

+728
-328
lines changed

11 files changed

+728
-328
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@ _Looking for the D-Bus API proposal?_ Check out [platform-api][linux-credentials
3636
- [Passkey Authentication][passkeys]
3737
- 🟢 Discoverable credentials (resident keys)
3838
- 🟢 Hybrid transport (caBLE v2): QR-initiated transactions
39-
- 🟠 Hybrid transport (caBLE v2): State-assisted transactions ([#31][#31]: planned)
39+
- 🟢 Hybrid transport (caBLE v2): State-assisted transactions (remember this phone)
4040

4141
## Transports
4242

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

4848
## Example programs
4949

libwebauthn/examples/webauthn_cable.rs

Lines changed: 73 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use std::error::Error;
22
use std::io::{self, Write};
3+
use std::sync::Arc;
34
use std::time::Duration;
45

56
use libwebauthn::pin::PinRequestReason;
67
use libwebauthn::transport::cable::known_devices::{
7-
CableKnownDeviceInfoStore, EphemeralDeviceInfoStore,
8+
CableKnownDevice, ClientPayloadHint, EphemeralDeviceInfoStore,
89
};
910
use libwebauthn::transport::cable::qr_code_device::{CableQrCodeDevice, QrCodeOperationHint};
1011
use libwebauthn::UxUpdate;
@@ -13,6 +14,7 @@ use qrcode::QrCode;
1314
use rand::{thread_rng, Rng};
1415
use text_io::read;
1516
use tokio::sync::mpsc::Receiver;
17+
use tokio::time::sleep;
1618
use tracing_subscriber::{self, EnvFilter};
1719

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

81-
let _device_info_store: Box<dyn CableKnownDeviceInfoStore> =
82-
Box::new(EphemeralDeviceInfoStore::default());
83-
84-
// Create QR code
85-
let mut device: CableQrCodeDevice<'_> =
86-
CableQrCodeDevice::new_transient(QrCodeOperationHint::MakeCredential);
87-
88-
println!("Created QR code, awaiting for advertisement.");
89-
let qr_code = QrCode::new(device.qr_code.to_string()).unwrap();
90-
let image = qr_code
91-
.render::<unicode::Dense1x2>()
92-
.dark_color(unicode::Dense1x2::Light)
93-
.light_color(unicode::Dense1x2::Dark)
94-
.build();
95-
println!("{}", image);
96-
97-
// Connect to a known device
98-
let (mut channel, state_recv) = device.channel().await.unwrap();
99-
println!("Tunnel established {:?}", channel);
100-
101-
tokio::spawn(handle_updates(state_recv));
102-
83+
let device_info_store = Arc::new(EphemeralDeviceInfoStore::default());
10384
let user_id: [u8; 32] = thread_rng().gen();
10485
let challenge: [u8; 32] = thread_rng().gen();
10586

106-
// Make Credentials ceremony
107-
let make_credentials_request = MakeCredentialRequest {
108-
origin: "example.org".to_owned(),
109-
hash: Vec::from(challenge),
110-
relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"),
111-
user: Ctap2PublicKeyCredentialUserEntity::new(&user_id, "mario.rossi", "Mario Rossi"),
112-
require_resident_key: false,
113-
user_verification: UserVerificationRequirement::Preferred,
114-
algorithms: vec![Ctap2CredentialType::default()],
115-
exclude: None,
116-
extensions: None,
117-
timeout: TIMEOUT,
118-
};
87+
let credential: Ctap2PublicKeyCredentialDescriptor = {
88+
// Create QR code
89+
let mut device: CableQrCodeDevice = CableQrCodeDevice::new_persistent(
90+
QrCodeOperationHint::MakeCredential,
91+
device_info_store.clone(),
92+
);
93+
94+
println!("Created QR code, awaiting for advertisement.");
95+
let qr_code = QrCode::new(device.qr_code.to_string()).unwrap();
96+
let image = qr_code
97+
.render::<unicode::Dense1x2>()
98+
.dark_color(unicode::Dense1x2::Light)
99+
.light_color(unicode::Dense1x2::Dark)
100+
.build();
101+
println!("{}", image);
102+
103+
// Connect to a known device
104+
let (mut channel, state_recv) = device.channel().await.unwrap();
105+
println!("Tunnel established {:?}", channel);
106+
107+
tokio::spawn(handle_updates(state_recv));
108+
109+
// Make Credentials ceremony
110+
let make_credentials_request = MakeCredentialRequest {
111+
origin: "example.org".to_owned(),
112+
hash: Vec::from(challenge),
113+
relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"),
114+
user: Ctap2PublicKeyCredentialUserEntity::new(&user_id, "mario.rossi", "Mario Rossi"),
115+
require_resident_key: false,
116+
user_verification: UserVerificationRequirement::Preferred,
117+
algorithms: vec![Ctap2CredentialType::default()],
118+
exclude: None,
119+
extensions: None,
120+
timeout: TIMEOUT,
121+
};
119122

120-
let response = loop {
121-
match channel
122-
.webauthn_make_credential(&make_credentials_request)
123-
.await
124-
{
125-
Ok(response) => break Ok(response),
126-
Err(WebAuthnError::Ctap(ctap_error)) => {
127-
if ctap_error.is_retryable_user_error() {
128-
println!("Oops, try again! Error: {}", ctap_error);
129-
continue;
123+
let response = loop {
124+
match channel
125+
.webauthn_make_credential(&make_credentials_request)
126+
.await
127+
{
128+
Ok(response) => break Ok(response),
129+
Err(WebAuthnError::Ctap(ctap_error)) => {
130+
if ctap_error.is_retryable_user_error() {
131+
println!("Oops, try again! Error: {}", ctap_error);
132+
continue;
133+
}
134+
break Err(WebAuthnError::Ctap(ctap_error));
130135
}
131-
break Err(WebAuthnError::Ctap(ctap_error));
132-
}
133-
Err(err) => break Err(err),
134-
};
135-
}
136-
.unwrap();
137-
println!("WebAuthn MakeCredential response: {:?}", response);
136+
Err(err) => break Err(err),
137+
};
138+
}
139+
.unwrap();
140+
println!("WebAuthn MakeCredential response: {:?}", response);
141+
142+
(&response.authenticator_data).try_into().unwrap()
143+
};
144+
145+
println!("Waiting for 5 seconds before contacting the device...");
146+
sleep(Duration::from_secs(5)).await;
138147

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

150-
// Create QR code
151-
let mut device: CableQrCodeDevice<'_> =
152-
CableQrCodeDevice::new_transient(QrCodeOperationHint::GetAssertionRequest);
157+
let all_devices = device_info_store.list_all().await;
158+
let (_known_device_id, known_device_info) =
159+
all_devices.first().expect("No known devices found");
153160

154-
println!("Created QR code, awaiting for advertisement.");
155-
let qr_code = QrCode::new(device.qr_code.to_string()).unwrap();
156-
let image = qr_code
157-
.render::<unicode::Dense1x2>()
158-
.dark_color(unicode::Dense1x2::Light)
159-
.light_color(unicode::Dense1x2::Dark)
160-
.build();
161-
println!("{}", image);
161+
let mut known_device: CableKnownDevice = CableKnownDevice::new(
162+
ClientPayloadHint::GetAssertion,
163+
known_device_info,
164+
device_info_store.clone(),
165+
)
166+
.await
167+
.unwrap();
162168

163169
// Connect to a known device
164-
println!("Tunnel established {:?}", channel);
165-
let (mut channel, state_recv) = device.channel().await.unwrap();
170+
let (mut channel, state_recv) = known_device.channel().await.unwrap();
166171
println!("Tunnel established {:?}", channel);
167172

168173
tokio::spawn(handle_updates(state_recv));

libwebauthn/src/transport/ble/btleplug/manager.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ async fn on_peripheral_service_data(
5656
id: &PeripheralId,
5757
uuids: &[Uuid],
5858
service_data: HashMap<Uuid, Vec<u8>>,
59-
) -> Option<(Peripheral, Vec<u8>)> {
59+
) -> Option<(Adapter, Peripheral, Vec<u8>)> {
6060
for uuid in uuids {
6161
if let Some(service_data) = service_data.get(uuid) {
6262
trace!(?id, ?service_data, "Found service data");
@@ -66,7 +66,7 @@ async fn on_peripheral_service_data(
6666
};
6767

6868
debug!({ ?id, ?service_data }, "Found service data for peripheral");
69-
return Some((peripheral, service_data.to_owned()));
69+
return Some((adapter.clone(), peripheral, service_data.to_owned()));
7070
}
7171
}
7272

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

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
use ::btleplug::api::Central;
2+
use futures::StreamExt;
3+
use std::pin::pin;
4+
use tracing::{debug, trace, warn};
5+
use uuid::Uuid;
6+
7+
use crate::transport::ble::btleplug::{self, FidoDevice};
8+
use crate::transport::cable::crypto::trial_decrypt_advert;
9+
use crate::webauthn::{Error, TransportError};
10+
11+
const CABLE_UUID_FIDO: &str = "0000fff9-0000-1000-8000-00805f9b34fb";
12+
const CABLE_UUID_GOOGLE: &str = "0000fde2-0000-1000-8000-00805f9b34fb";
13+
14+
#[derive(Debug)]
15+
pub(crate) struct DecryptedAdvert {
16+
pub plaintext: [u8; 16],
17+
pub nonce: [u8; 10],
18+
pub routing_id: [u8; 3],
19+
pub encoded_tunnel_server_domain: u16,
20+
}
21+
22+
impl From<[u8; 16]> for DecryptedAdvert {
23+
fn from(plaintext: [u8; 16]) -> Self {
24+
let mut nonce = [0u8; 10];
25+
nonce.copy_from_slice(&plaintext[1..11]);
26+
let mut routing_id = [0u8; 3];
27+
routing_id.copy_from_slice(&plaintext[11..14]);
28+
let encoded_tunnel_server_domain = u16::from_le_bytes([plaintext[14], plaintext[15]]);
29+
let mut plaintext_fixed = [0u8; 16];
30+
plaintext_fixed.copy_from_slice(&plaintext[..16]);
31+
Self {
32+
plaintext: plaintext_fixed,
33+
nonce,
34+
routing_id,
35+
encoded_tunnel_server_domain,
36+
}
37+
}
38+
}
39+
40+
pub(crate) async fn await_advertisement(
41+
eid_key: &[u8],
42+
) -> Result<(FidoDevice, DecryptedAdvert), Error> {
43+
let uuids = &[
44+
Uuid::parse_str(CABLE_UUID_FIDO).unwrap(),
45+
Uuid::parse_str(CABLE_UUID_GOOGLE).unwrap(), // Deprecated, but may still be in use.
46+
];
47+
let stream = btleplug::manager::start_discovery_for_service_data(uuids)
48+
.await
49+
.or(Err(Error::Transport(TransportError::TransportUnavailable)))?;
50+
51+
let mut stream = pin!(stream);
52+
while let Some((adapter, peripheral, data)) = stream.as_mut().next().await {
53+
debug!({ ?peripheral, ?data }, "Found device with service data");
54+
55+
let Some(device) = btleplug::manager::get_device(peripheral.clone())
56+
.await
57+
.or(Err(Error::Transport(TransportError::TransportUnavailable)))?
58+
else {
59+
warn!(
60+
?peripheral,
61+
"Unable to fetch peripheral properties, ignoring"
62+
);
63+
continue;
64+
};
65+
66+
trace!(?device, ?data, ?eid_key);
67+
let Some(decrypted) = trial_decrypt_advert(&eid_key, &data) else {
68+
warn!(?device, "Trial decrypt failed, ignoring");
69+
continue;
70+
};
71+
trace!(?decrypted);
72+
73+
let advert = DecryptedAdvert::from(decrypted);
74+
debug!(
75+
?device,
76+
?decrypted,
77+
"Successfully decrypted advertisement from device"
78+
);
79+
80+
adapter
81+
.stop_scan()
82+
.await
83+
.or(Err(Error::Transport(TransportError::TransportUnavailable)))?;
84+
85+
return Ok((device, advert));
86+
}
87+
88+
warn!("BLE advertisement discovery stream terminated");
89+
Err(Error::Transport(TransportError::TransportUnavailable))
90+
}

libwebauthn/src/transport/cable/channel.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,35 +22,39 @@ use super::qr_code_device::CableQrCodeDevice;
2222

2323
#[derive(Debug)]
2424
pub enum CableChannelDevice<'d> {
25-
QrCode(&'d CableQrCodeDevice<'d>),
26-
Known(&'d CableKnownDevice<'d>),
25+
QrCode(&'d CableQrCodeDevice),
26+
Known(&'d CableKnownDevice),
2727
}
2828

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

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

3737
/// The device that this channel is connected to.
38-
pub device: CableChannelDevice<'d>,
39-
4038
pub(crate) handle_connection: task::JoinHandle<()>,
4139
pub(crate) cbor_sender: mpsc::Sender<CborRequest>,
4240
pub(crate) cbor_receiver: mpsc::Receiver<CborResponse>,
4341
pub(crate) tx: mpsc::Sender<UxUpdate>,
4442
}
4543

46-
impl Display for CableChannel<'_> {
44+
impl Display for CableChannel {
4745
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
4846
write!(f, "CableChannel")
4947
}
5048
}
5149

50+
impl Drop for CableChannel {
51+
fn drop(&mut self) {
52+
self.handle_connection.abort();
53+
}
54+
}
55+
5256
#[async_trait]
53-
impl<'d> Channel for CableChannel<'d> {
57+
impl<'d> Channel for CableChannel {
5458
async fn supported_protocols(&self) -> Result<SupportedProtocols, Error> {
5559
Ok(SupportedProtocols::fido2_only())
5660
}
@@ -111,7 +115,7 @@ impl<'d> Channel for CableChannel<'d> {
111115
}
112116
}
113117

114-
impl<'d> Ctap2AuthTokenStore for CableChannel<'d> {
118+
impl<'d> Ctap2AuthTokenStore for CableChannel {
115119
fn store_auth_data(&mut self, _auth_token_data: AuthTokenData) {}
116120

117121
fn get_auth_data(&self) -> Option<&AuthTokenData> {

0 commit comments

Comments
 (0)