|
| 1 | +//! caBLE / CTAP 2.3 hybrid: the QR advertises both the WebSocket tunnel and |
| 2 | +//! the BLE L2CAP channel so the authenticator can pick either one. Transient |
| 3 | +//! MakeCredential only. |
| 4 | +use std::error::Error; |
| 5 | + |
| 6 | +use libwebauthn::transport::cable::is_available; |
| 7 | +use libwebauthn::transport::cable::qr_code_device::{ |
| 8 | + CableQrCodeDevice, CableTransports, QrCodeOperationHint, |
| 9 | +}; |
| 10 | +use qrcode::render::unicode; |
| 11 | +use qrcode::QrCode; |
| 12 | + |
| 13 | +use libwebauthn::ops::webauthn::{ |
| 14 | + DatFilePublicSuffixList, JsonFormat, MakeCredentialRequest, RequestOrigin, WebAuthnIDL as _, |
| 15 | + WebAuthnIDLResponse as _, |
| 16 | +}; |
| 17 | +use libwebauthn::transport::{Channel as _, Device}; |
| 18 | +use libwebauthn::webauthn::WebAuthn; |
| 19 | + |
| 20 | +#[path = "../common/mod.rs"] |
| 21 | +mod common; |
| 22 | + |
| 23 | +const MAKE_CREDENTIAL_REQUEST: &str = r#" |
| 24 | +{ |
| 25 | + "rp": { |
| 26 | + "id": "example.org", |
| 27 | + "name": "Example Relying Party" |
| 28 | + }, |
| 29 | + "user": { |
| 30 | + "id": "MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2Nzg", |
| 31 | + "name": "Mario Rossi", |
| 32 | + "displayName": "Mario Rossi" |
| 33 | + }, |
| 34 | + "challenge": "MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2Nzg", |
| 35 | + "pubKeyCredParams": [ |
| 36 | + {"type": "public-key", "alg": -7} |
| 37 | + ], |
| 38 | + "timeout": 120000, |
| 39 | + "excludeCredentials": [], |
| 40 | + "authenticatorSelection": { |
| 41 | + "residentKey": "discouraged", |
| 42 | + "userVerification": "preferred" |
| 43 | + }, |
| 44 | + "attestation": "none" |
| 45 | +} |
| 46 | +"#; |
| 47 | + |
| 48 | +#[tokio::main] |
| 49 | +pub async fn main() -> Result<(), Box<dyn Error>> { |
| 50 | + common::setup_logging(); |
| 51 | + |
| 52 | + if !is_available().await { |
| 53 | + eprintln!("No Bluetooth adapter found. Cable/Hybrid transport is unavailable."); |
| 54 | + return Err("Cable transport not available".into()); |
| 55 | + } |
| 56 | + |
| 57 | + let request_origin: RequestOrigin = "https://example.org".try_into().expect("Invalid origin"); |
| 58 | + let psl = DatFilePublicSuffixList::from_system_file().expect( |
| 59 | + "PSL not available; install the publicsuffix-list package or pass an explicit path", |
| 60 | + ); |
| 61 | + |
| 62 | + let mut device: CableQrCodeDevice = CableQrCodeDevice::new_transient( |
| 63 | + QrCodeOperationHint::MakeCredential, |
| 64 | + CableTransports::CloudAssistedOrLocal, |
| 65 | + )?; |
| 66 | + |
| 67 | + println!("Created QR code, awaiting for advertisement."); |
| 68 | + let qr_code = QrCode::new(device.qr_code.to_string()).unwrap(); |
| 69 | + let image = qr_code |
| 70 | + .render::<unicode::Dense1x2>() |
| 71 | + .dark_color(unicode::Dense1x2::Light) |
| 72 | + .light_color(unicode::Dense1x2::Dark) |
| 73 | + .build(); |
| 74 | + println!("{}", image); |
| 75 | + |
| 76 | + let mut channel = device.channel().await.unwrap(); |
| 77 | + println!("Channel established {:?}", channel); |
| 78 | + |
| 79 | + let state_recv = channel.get_ux_update_receiver(); |
| 80 | + tokio::spawn(common::handle_cable_updates(state_recv)); |
| 81 | + |
| 82 | + let request = MakeCredentialRequest::from_json(&request_origin, &psl, MAKE_CREDENTIAL_REQUEST) |
| 83 | + .expect("Failed to parse request JSON"); |
| 84 | + |
| 85 | + let response = retry_user_errors!(channel.webauthn_make_credential(&request)).unwrap(); |
| 86 | + let response_json = response |
| 87 | + .to_json_string(&request, JsonFormat::Prettified) |
| 88 | + .expect("Failed to serialize MakeCredential response"); |
| 89 | + println!("WebAuthn MakeCredential response (JSON):\n{response_json}"); |
| 90 | + |
| 91 | + Ok(()) |
| 92 | +} |
0 commit comments