From f3fa8df697a7e269393d2296d11749acc2827de2 Mon Sep 17 00:00:00 2001 From: Michael Farrell Date: Wed, 29 Mar 2023 23:21:15 +1000 Subject: [PATCH] Add `cable-localhost-tunnel` feature. This makes the library connect to a tunnel server running at `ws://localhost:8080`, rather than the proper tunnel server URL. --- webauthn-authenticator-rs/Cargo.toml | 5 + .../src/cable/discovery.rs | 12 +-- webauthn-authenticator-rs/src/cable/mod.rs | 4 + webauthn-authenticator-rs/src/cable/tunnel.rs | 92 ++++++++++++------- webauthn-authenticator-rs/src/stubs.rs | 3 + 5 files changed, 76 insertions(+), 40 deletions(-) diff --git a/webauthn-authenticator-rs/Cargo.toml b/webauthn-authenticator-rs/Cargo.toml index 3d306726..5efae315 100644 --- a/webauthn-authenticator-rs/Cargo.toml +++ b/webauthn-authenticator-rs/Cargo.toml @@ -17,6 +17,11 @@ bluetooth = ["btleplug"] # caBLE / hybrid authenticator cable = ["bluetooth", "hex", "tokio", "tokio-tungstenite", "qrcode"] + +# Always connect to a caBLE tunnel server at ws://localhost:8080, rather than +# the true tunnel server URL. Only useful for testing. +cable-localhost-tunnel = [] + nfc = ["pcsc"] usb = ["hidapi"] win10 = ["windows"] diff --git a/webauthn-authenticator-rs/src/cable/discovery.rs b/webauthn-authenticator-rs/src/cable/discovery.rs index 928b1365..9a59fa2d 100644 --- a/webauthn-authenticator-rs/src/cable/discovery.rs +++ b/webauthn-authenticator-rs/src/cable/discovery.rs @@ -11,7 +11,7 @@ use openssl::{ sign::Signer, }; use std::mem::size_of; -use tokio_tungstenite::tungstenite::http::Uri; +use tokio_tungstenite::tungstenite::http::{uri::Builder, Uri}; use crate::{ cable::{btle::*, handshake::*, tunnel::get_domain, CableRequestType, Psk}, @@ -176,9 +176,7 @@ impl Discovery { get_domain(domain_id) .and_then(|domain| { let tunnel_id = hex::encode_upper(self.derive_tunnel_id().ok()?); - Uri::builder() - .scheme("wss") - .authority(domain) + domain .path_and_query(format!("/cable/new/{}", tunnel_id)) .build() .ok() @@ -339,7 +337,7 @@ impl Eid { } /// Gets the tunnel server domain for this [Eid]. - fn get_domain(&self) -> Option { + fn get_domain(&self) -> Option { get_domain(self.tunnel_server_id) } @@ -353,9 +351,7 @@ impl Eid { let routing_id = hex::encode_upper(self.routing_id); let tunnel_id = hex::encode_upper(tunnel_id); - Uri::builder() - .scheme("wss") - .authority(domain) + domain .path_and_query(format!("/cable/connect/{}/{}", routing_id, tunnel_id)) .build() .ok() diff --git a/webauthn-authenticator-rs/src/cable/mod.rs b/webauthn-authenticator-rs/src/cable/mod.rs index 41e9d15a..54013125 100644 --- a/webauthn-authenticator-rs/src/cable/mod.rs +++ b/webauthn-authenticator-rs/src/cable/mod.rs @@ -19,6 +19,10 @@ //! operated by Apple (`wss://cable.auth.com`) and Google //! (`wss://cable.ua5v.com`), and an algorithm to generate tunnel server //! domain names from a hash to allow for future expansion. +//! +//! You can also build the library with the `cable-localhost-tunnel` feature, +//! which causes it to always connect to `ws://localhost:8080` (over HTTP) +//! instead of the *proper* tunnel server over HTTPS. //! //! This module implements both the [initator][connect_cable_authenticator] and //! [authenticator][share_cable_authenticator] side of caBLE, provided diff --git a/webauthn-authenticator-rs/src/cable/tunnel.rs b/webauthn-authenticator-rs/src/cable/tunnel.rs index bd1bc46a..9c9d7a6c 100644 --- a/webauthn-authenticator-rs/src/cable/tunnel.rs +++ b/webauthn-authenticator-rs/src/cable/tunnel.rs @@ -19,7 +19,10 @@ use tokio_tungstenite::{ connect_async, tungstenite::{client::IntoClientRequest, http::HeaderValue, Message}, }; -use tokio_tungstenite::{tungstenite::http::Uri, MaybeTlsStream, WebSocketStream}; +use tokio_tungstenite::{ + tungstenite::http::{uri::Builder, Uri}, + MaybeTlsStream, WebSocketStream, +}; use webauthn_rs_proto::AuthenticatorTransport; use crate::{ @@ -59,37 +62,48 @@ const TUNNEL_SERVER_ID_OFFSET: usize = TUNNEL_SERVER_SALT.len() - 3; const TUNNEL_SERVER_TLDS: [&str; 4] = [".com", ".org", ".net", ".info"]; const BASE32_CHARS: &[u8] = b"abcdefghijklmnopqrstuvwxyz234567"; +#[cfg(feature = "cable-localhost-tunnel")] +const CABLE_LOCAL_TUNNEL: Option<&str> = Some("localhost:8080"); +#[cfg(not(feature = "cable-localhost-tunnel"))] +const CABLE_LOCAL_TUNNEL: Option<&str> = None; + /// Decodes a `domain_id` into an actual domain name. /// /// See Chromium's `tunnelserver::DecodeDomain`. -pub fn get_domain(domain_id: u16) -> Option { - if domain_id < 256 { - return match ASSIGNED_DOMAINS.get(usize::from(domain_id)) { - Some(d) => Some(d.to_string()), +pub fn get_domain(domain_id: u16) -> Option { + let domain = if domain_id < 256 { + match ASSIGNED_DOMAINS.get(usize::from(domain_id)) { + Some(d) => d.to_string(), None => { warn!("Invalid tunnel server ID {:04x}", domain_id); - None + return None; } - }; - } - - let mut buf = TUNNEL_SERVER_SALT.to_vec(); - buf[TUNNEL_SERVER_ID_OFFSET..TUNNEL_SERVER_ID_OFFSET + 2] - .copy_from_slice(&domain_id.to_le_bytes()); - let digest = compute_sha256(&buf); - let mut result = u64::from_le_bytes(digest[..8].try_into().ok()?); - - let tld = TUNNEL_SERVER_TLDS[(result & 3) as usize]; + } + } else { + let mut buf = TUNNEL_SERVER_SALT.to_vec(); + buf[TUNNEL_SERVER_ID_OFFSET..TUNNEL_SERVER_ID_OFFSET + 2] + .copy_from_slice(&domain_id.to_le_bytes()); + let digest = compute_sha256(&buf); + let mut result = u64::from_le_bytes(digest[..8].try_into().ok()?); + + let tld = TUNNEL_SERVER_TLDS[(result & 3) as usize]; + + let mut domain = String::from("cable."); + result >>= 2; + while result != 0 { + domain.push(char::from_u32(BASE32_CHARS[(result & 31) as usize].into())?); + result >>= 5; + } + domain.push_str(tld); + domain + }; - let mut o = String::from("cable."); - result >>= 2; - while result != 0 { - o.push(char::from_u32(BASE32_CHARS[(result & 31) as usize].into())?); - result >>= 5; + if let Some(t) = CABLE_LOCAL_TUNNEL { + warn!("Using ws://{t} as caBLE tunnel server rather than wss://{domain}"); + return Some(Uri::builder().scheme("ws").authority(t)); } - o.push_str(tld); - Some(o) + Some(Uri::builder().scheme("wss").authority(domain)) } /// Websocket tunnel to a caBLE authenticator. @@ -124,7 +138,7 @@ impl Tunnel { trace!(?request); let (stream, response) = connect_async(request).await.map_err(|e| { - error!("websocket error: {:?}", e); + error!("websocket error: {uri}: {e:?}"); WebauthnCError::Internal })?; @@ -459,26 +473,40 @@ mod test { #[test] fn check_known_tunnel_server_domains() { - assert_eq!(get_domain(0), Some(String::from("cable.ua5v.com"))); - assert_eq!(get_domain(1), Some(String::from("cable.auth.com"))); assert_eq!( - get_domain(266), - Some(String::from("cable.wufkweyy3uaxb.com")) + get_domain(0).unwrap().path_and_query("/").build().unwrap(), + "wss://cable.ua5v.com/" + ); + assert_eq!( + get_domain(1).unwrap().path_and_query("/").build().unwrap(), + "wss://cable.auth.com/" + ); + assert_eq!( + get_domain(266) + .unwrap() + .path_and_query("/") + .build() + .unwrap(), + "wss://cable.wufkweyy3uaxb.com/" ); - assert_eq!(get_domain(255), None); + assert!(get_domain(255).is_none()); // 🦀 = \u{1f980} assert_eq!( - get_domain(0xf980), - Some(String::from("cable.my4kstlhndi4c.net")) + get_domain(0xf980) + .unwrap() + .path_and_query("/") + .build() + .unwrap(), + "wss://cable.my4kstlhndi4c.net" ) } #[test] fn check_all_hashed_tunnel_servers() { for x in 256..u16::MAX { - assert_ne!(get_domain(x), None); + assert!(get_domain(x).is_some()); } } } diff --git a/webauthn-authenticator-rs/src/stubs.rs b/webauthn-authenticator-rs/src/stubs.rs index 4c0773ca..4df32062 100644 --- a/webauthn-authenticator-rs/src/stubs.rs +++ b/webauthn-authenticator-rs/src/stubs.rs @@ -49,6 +49,9 @@ pub mod tokio_tungstenite { pub mod tungstenite { pub mod http { pub struct Uri {} + pub mod uri { + pub struct Builder {} + } } } pub struct MaybeTlsStream {}