Skip to content

Commit

Permalink
add CLI tool to derive caBLE tunnel server domain (#305)
Browse files Browse the repository at this point in the history
* Add caBLE domain getter

* add DNS resolution

* update docs

* fix formatting
  • Loading branch information
micolous authored Apr 17, 2023
1 parent f2cf354 commit 410a9aa
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 5 deletions.
4 changes: 4 additions & 0 deletions webauthn-authenticator-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,7 @@ image = ">= 0.23.14, < 0.24"

[build-dependencies]
openssl.workspace = true

[[example]]
name = "cable_domain"
required-features = ["cable"]
118 changes: 118 additions & 0 deletions webauthn-authenticator-rs/examples/cable_domain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//! Derives caBLE tunnel server hostnames.
//!
//! ```sh
//! cargo run --example cable_domain --features cable -- --help
//! ```
use clap::{ArgGroup, CommandFactory, Parser, ValueHint};
use std::net::ToSocketAddrs;
use webauthn_authenticator_rs::cable::get_domain;

#[derive(Debug, clap::Parser)]
#[clap(
about = "Derives caBLE tunnel server hostnames",
after_help = "\
When deriving exactly one tunnel server hostname, only the hostname will be \
printed, and an unknown ID will result in an error.
When deriving multiple (or all) tunnel server hostnames, or when using `--csv` \
or `--resolve`, the output will switch to CSV format.
When outputting CSV, entries for unknown tunnel IDs will be an empty string, \
rather than an error."
)]
#[clap(group(
ArgGroup::new("ids")
.required(true)
.args(&["tunnel-server-ids", "all"])
))]
pub struct CliParser {
/// One or more tunnel server IDs.
#[clap(value_name = "ID", value_hint = ValueHint::Other)]
pub tunnel_server_ids: Vec<u16>,

/// Derives all 65,536 possible tunnel server hostnames.
#[clap(short, long)]
pub all: bool,

/// Enable CSV output mode.
///
/// This is automatically enabled when requesting multiple (or all) tunnel
/// server IDs, or when using `--resolve`.
#[clap(short, long)]
pub csv: bool,

/// Resolve tunnel server hostnames to IP addresses.
///
/// This generally results in network traffic, so will be slow.
#[clap(short, long)]
pub resolve: bool,
}

/// Resolves a hostname to (an) IP address(es) using the system resolver,
/// and return it as a comma-separated list.
///
/// Returns [None] on resolution failure or no results.
fn resolver(hostname: &str) -> Option<String> {
(hostname, 443).to_socket_addrs().ok().and_then(|addrs| {
let mut o: String = addrs
.map(|addr| format!("{},", addr.ip().to_string()))
.collect();
o.pop();

if o.is_empty() {
return None;
}

Some(o)
})
}

/// Prints caBLE tunnel server hostnames in CSV format.
fn print_hostnames(i: impl Iterator<Item = u16>, resolve: bool) {
for domain_id in i {
let domain = get_domain(domain_id);
let addrs = if resolve {
domain.as_deref().and_then(resolver)
} else {
None
}
.unwrap_or_default();
let domain = domain.unwrap_or_default();

if resolve {
println!("{domain_id},{domain:?},{addrs:?}",);
} else {
println!("{domain_id},{domain:?}",);
}
}
}

fn main() {
let opt = CliParser::parse();
tracing_subscriber::fmt::init();

if opt.tunnel_server_ids.len() == 1 && !(opt.resolve || opt.csv) {
let domain_id = opt.tunnel_server_ids[0];
match get_domain(domain_id) {
Some(d) => println!("{d}"),
None => CliParser::command()
.error(
clap::ErrorKind::ValueValidation,
format!("unknown tunnel server ID: {domain_id}"),
)
.exit(),
}
} else {
if opt.resolve {
println!("tunnel_server_id,hostname,addrs");
} else {
println!("tunnel_server_id,hostname");
}

if opt.all {
print_hostnames(u16::MIN..=u16::MAX, opt.resolve);
} else {
print_hostnames(opt.tunnel_server_ids.into_iter(), opt.resolve);
}
}
}
3 changes: 1 addition & 2 deletions webauthn-authenticator-rs/src/cable/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,7 @@ mod tunnel;

use std::collections::BTreeMap;

pub use base10::DecodeError;
pub use btle::Advertiser;
pub use self::{base10::DecodeError, btle::Advertiser, tunnel::get_domain};
use tokio_tungstenite::tungstenite::http::uri::Builder;

use crate::{
Expand Down
33 changes: 30 additions & 3 deletions webauthn-authenticator-rs/src/cable/tunnel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,36 @@ 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";

/// Decodes a `domain_id` into an actual domain name.
/// Derives a caBLE tunnel server hostname from a `domain_id`.
///
/// See Chromium's `tunnelserver::DecodeDomain`.
/// Returns a hostname (eg: `cable.ua5v.com`) on success, or `None` if
/// `domain_id` is unknown.
///
/// **Reference:** [Chromium's `tunnelserver::DecodeDomain`][0]
///
/// ## Example
///
/// ```
/// use webauthn_authenticator_rs::cable::get_domain;
///
/// // Known static assignment
/// assert_eq!(get_domain(0).unwrap(), "cable.ua5v.com");
///
/// // Unknown static assignment
/// assert_eq!(get_domain(255), None);
///
/// // Hostname derived from checksum
/// assert_eq!(get_domain(266).unwrap(), "cable.wufkweyy3uaxb.com");
/// ```
///
/// **Tip:** The `cable_domain` example derives tunnel server hostnames at the
/// command line. For more information, run:
///
/// ```sh
/// cargo run --example cable_domain --features cable -- --help
/// ```
///
/// [0]: https://source.chromium.org/chromium/chromium/src/+/main:device/fido/cable/v2_handshake.cc?q=symbol%3A%5Cbdevice%3A%3Acablev2%3A%3Atunnelserver%3A%3ADecodeDomain%5Cb%20case%3Ayes
pub fn get_domain(domain_id: u16) -> Option<String> {
if domain_id < 256 {
return match ASSIGNED_DOMAINS.get(usize::from(domain_id)) {
Expand Down Expand Up @@ -477,7 +504,7 @@ mod test {

#[test]
fn check_all_hashed_tunnel_servers() {
for x in 256..u16::MAX {
for x in 256..=u16::MAX {
assert_ne!(get_domain(x), None);
}
}
Expand Down

0 comments on commit 410a9aa

Please sign in to comment.