Skip to content

Commit 625fd50

Browse files
Known device advertisement
1 parent 8c5f281 commit 625fd50

File tree

10 files changed

+375
-253
lines changed

10 files changed

+375
-253
lines changed

libwebauthn/examples/webauthn_cable.rs

Lines changed: 70 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::time::Duration;
55

66
use libwebauthn::pin::PinRequestReason;
77
use libwebauthn::transport::cable::known_devices::{
8-
CableKnownDeviceInfoStore, EphemeralDeviceInfoStore,
8+
self, CableKnownDevice, CableKnownDeviceId, CableKnownDeviceInfoStore, EphemeralDeviceInfoStore,
99
};
1010
use libwebauthn::transport::cable::qr_code_device::{CableQrCodeDevice, QrCodeOperationHint};
1111
use libwebauthn::UxUpdate;
@@ -14,6 +14,7 @@ use qrcode::QrCode;
1414
use rand::{thread_rng, Rng};
1515
use text_io::read;
1616
use tokio::sync::mpsc::Receiver;
17+
use tokio::time::sleep;
1718
use tracing_subscriber::{self, EnvFilter};
1819

1920
use libwebauthn::ops::webauthn::{
@@ -79,68 +80,71 @@ async fn handle_updates(mut state_recv: Receiver<UxUpdate>) {
7980
pub async fn main() -> Result<(), Box<dyn Error>> {
8081
setup_logging();
8182

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

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-
};
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+
};
122122

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;
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));
133135
}
134-
break Err(WebAuthnError::Ctap(ctap_error));
135-
}
136-
Err(err) => break Err(err),
137-
};
138-
}
139-
.unwrap();
140-
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 10 seconds before contacting the device...");
146+
sleep(Duration::from_secs(30)).await;
141147

142-
let credential: Ctap2PublicKeyCredentialDescriptor =
143-
(&response.authenticator_data).try_into().unwrap();
144148
let get_assertion = GetAssertionRequest {
145149
relying_party_id: "example.org".to_owned(),
146150
hash: Vec::from(challenge),
@@ -150,24 +154,17 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
150154
timeout: TIMEOUT,
151155
};
152156

153-
// Create QR code
154-
let mut device: CableQrCodeDevice = CableQrCodeDevice::new_persistent(
155-
QrCodeOperationHint::GetAssertionRequest,
156-
device_info_store.clone(),
157-
);
158-
159-
println!("Created QR code, awaiting for advertisement.");
160-
let qr_code = QrCode::new(device.qr_code.to_string()).unwrap();
161-
let image = qr_code
162-
.render::<unicode::Dense1x2>()
163-
.dark_color(unicode::Dense1x2::Light)
164-
.light_color(unicode::Dense1x2::Dark)
165-
.build();
166-
println!("{}", image);
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");
160+
161+
let mut known_device: CableKnownDevice =
162+
CableKnownDevice::new(known_device_info, device_info_store.clone())
163+
.await
164+
.unwrap();
167165

168166
// Connect to a known device
169-
println!("Tunnel established {:?}", channel);
170-
let (mut channel, state_recv) = device.channel().await.unwrap();
167+
let (mut channel, state_recv) = known_device.channel().await.unwrap();
171168
println!("Tunnel established {:?}", channel);
172169

173170
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]> for DecryptedAdvert {
23+
fn from(plaintext: &[u8]) -> 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.as_slice());
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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ impl Display for CableChannel {
4747
}
4848
}
4949

50+
impl Drop for CableChannel {
51+
fn drop(&mut self) {
52+
self.handle_connection.abort();
53+
}
54+
}
55+
5056
#[async_trait]
5157
impl<'d> Channel for CableChannel {
5258
async fn supported_protocols(&self) -> Result<SupportedProtocols, Error> {

libwebauthn/src/transport/cable/crypto.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ pub enum KeyPurpose {
1313
}
1414

1515

16-
pub fn derive(secret: &[u8; 16], salt: Option<&[u8]>, purpose: KeyPurpose) -> Vec<u8> {
16+
pub fn derive(secret: &[u8], salt: Option<&[u8]>, purpose: KeyPurpose) -> [u8; 64] {
1717
let mut purpose32 = [0u8; 4];
1818
purpose32[0] = purpose as u8;
1919

2020
let hkdf = Hkdf::<Sha256>::new(salt, secret);
21-
let mut output = vec![0u8; 64];
21+
let mut output = [0u8; 64];
2222
hkdf.expand(&purpose32, &mut output).unwrap();
2323
output
2424
}
@@ -66,7 +66,7 @@ mod tests {
6666
#[test]
6767
fn derive_eidkey_nosalt() {
6868
let input: [u8; 16] = hex::decode("00112233445566778899aabbccddeeff").unwrap().try_into().unwrap();
69-
let output = derive(&input, None, KeyPurpose::EIDKey);
69+
let output = derive(&input, None, KeyPurpose::EIDKey).to_vec();
7070
let expected = hex::decode("efafab5b2c84a11c80e3ad0770353138b414a859ccd3afcc99e3d3250dba65084ede8e38e75432617c0ccae1ffe5d8143df0db0cd6d296f489419cd6411ee505").unwrap();
7171
assert_eq!(output, expected);
7272
}
@@ -75,7 +75,7 @@ mod tests {
7575
fn derive_eidkey_salt() {
7676
let input: [u8; 16] = hex::decode("00112233445566778899aabbccddeeff").unwrap().try_into().unwrap();
7777
let salt = hex::decode("ffeeddccbbaa998877665544332211").unwrap();
78-
let output = derive(&input, Some(&salt), KeyPurpose::EIDKey);
78+
let output = derive(&input, Some(&salt), KeyPurpose::EIDKey).to_vec();
7979
let expected = hex::decode("168cf3dd220a7907f8bac30f559be92a3b6d937fe5594beeaf1e50e35976b7d654dd550e22ae4c801b9d1cdbf0d2b1472daa1328661eb889acae3023b7ffa509").unwrap();
8080
assert_eq!(output, expected);
8181
}

0 commit comments

Comments
 (0)