├── .github └── workflows │ ├── rust-clippy.yml │ └── rust.yml ├── .gitignore ├── .gitmodules ├── COPYING ├── Cargo.lock ├── Cargo.toml ├── README.md ├── libwebauthn ├── Cargo.toml ├── examples │ ├── authenticator_config_hid.rs │ ├── bio_enrollment_hid.rs │ ├── change_pin_hid.rs │ ├── cred_management.rs │ ├── prf_test.rs │ ├── u2f_ble.rs │ ├── u2f_hid.rs │ ├── webauthn_cable.rs │ ├── webauthn_extensions_hid.rs │ ├── webauthn_hid.rs │ ├── webauthn_preflight_hid.rs │ └── webauthn_prf_hid.rs └── src │ ├── fido.rs │ ├── lib.rs │ ├── management.rs │ ├── management │ ├── authenticator_config.rs │ ├── bio_enrollment.rs │ └── credential_management.rs │ ├── ops │ ├── mod.rs │ ├── u2f.rs │ └── webauthn.rs │ ├── pin.rs │ ├── proto │ ├── ctap1 │ │ ├── apdu │ │ │ ├── mod.rs │ │ │ ├── request.rs │ │ │ └── response.rs │ │ ├── mod.rs │ │ ├── model.rs │ │ └── protocol.rs │ ├── ctap2 │ │ ├── cbor │ │ │ ├── mod.rs │ │ │ ├── request.rs │ │ │ └── response.rs │ │ ├── mod.rs │ │ ├── model.rs │ │ ├── model │ │ │ ├── authenticator_config.rs │ │ │ ├── bio_enrollment.rs │ │ │ ├── client_pin.rs │ │ │ ├── credential_management.rs │ │ │ ├── get_assertion.rs │ │ │ ├── get_info.rs │ │ │ └── make_credential.rs │ │ ├── preflight.rs │ │ └── protocol.rs │ ├── error.rs │ └── mod.rs │ ├── transport │ ├── ble │ │ ├── btleplug │ │ │ ├── connection.rs │ │ │ ├── device.rs │ │ │ ├── error.rs │ │ │ ├── gatt.rs │ │ │ ├── manager.rs │ │ │ └── mod.rs │ │ ├── channel.rs │ │ ├── device.rs │ │ ├── framing.rs │ │ └── mod.rs │ ├── cable │ │ ├── advertisement.rs │ │ ├── channel.rs │ │ ├── crypto.rs │ │ ├── digit_encode.rs │ │ ├── known_devices.rs │ │ ├── mod.rs │ │ ├── qr_code_device.rs │ │ └── tunnel.rs │ ├── channel.rs │ ├── device.rs │ ├── error.rs │ ├── hid │ │ ├── channel.rs │ │ ├── device.rs │ │ ├── framing.rs │ │ ├── init.rs │ │ └── mod.rs │ ├── mod.rs │ └── transport.rs │ ├── u2f.rs │ ├── ui.rs │ └── webauthn.rs └── solo ├── Cargo.toml ├── build.rs └── src └── lib.rs /.github/workflows/rust-clippy.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # rust-clippy is a tool that runs a bunch of lints to catch common 6 | # mistakes in your Rust code and help improve your Rust code. 7 | # More details at https://github.com/rust-lang/rust-clippy 8 | # and https://rust-lang.github.io/rust-clippy/ 9 | 10 | name: rust-clippy analyze 11 | 12 | on: 13 | push: 14 | branches: [ "master" ] 15 | pull_request: 16 | # The branches below must be a subset of the branches above 17 | branches: [ "master" ] 18 | schedule: 19 | - cron: '37 10 * * 6' 20 | 21 | jobs: 22 | rust-clippy-analyze: 23 | name: Run rust-clippy analyzing 24 | runs-on: ubuntu-latest 25 | permissions: 26 | contents: read 27 | security-events: write 28 | actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status 29 | steps: 30 | - name: Checkout code 31 | uses: actions/checkout@v2 32 | 33 | - name: Install Rust toolchain 34 | uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af #@v1 35 | with: 36 | profile: minimal 37 | toolchain: stable 38 | components: clippy 39 | override: true 40 | 41 | - name: Install required cargo 42 | run: cargo install clippy-sarif sarif-fmt 43 | 44 | - name: Run rust-clippy 45 | run: 46 | cargo clippy 47 | --all-features 48 | --message-format=json | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt 49 | continue-on-error: true 50 | 51 | - name: Upload analysis results to GitHub 52 | uses: github/codeql-action/upload-sarif@v1 53 | with: 54 | sarif_file: rust-clippy-results.sarif 55 | wait-for-processing: true 56 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Build and run tests 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | env: 7 | RUST_LOG: debug 8 | name: Build and run tests 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Checkout submodules 13 | run: git submodule update --init --recursive 14 | - name: Update apt cache 15 | run: sudo apt-get update 16 | - name: Install system dependencies 17 | run: sudo apt-get install libudev-dev libdbus-1-dev libsodium-dev 18 | - name: Build 19 | run: cargo build 20 | - name: Run tests 21 | run: cargo test --verbose --features hid-device-tests 22 | - name: Run u2f_hid example (virtual key) 23 | run: cargo run --example u2f_hid --features virtual-hid-device 24 | # - name: Run webauthn_hid example (virtual key) 25 | # run: cargo run --example webauthn_hid --features virtual-hid-device 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | */target 2 | target 3 | 4 | # Virtual key state 5 | solo/resident_keys.bin 6 | solo/authenticator_state.bin 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "solo/src/ext"] 2 | path = solo/src/ext 3 | url = https://github.com/AlfioEmanueleFresta/solo.git 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | resolver = "2" 4 | members = [ 5 | "libwebauthn", 6 | "solo", 7 | ] 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libwebauthn 2 | 3 | A Linux-native implementation of FIDO2 and FIDO U2F Platform API, fully written in Rust. 4 | 5 | This library supports multiple transports (see [Transports](#Transports) for a list) via a pluggable interface, making it easy to add additional backends. 6 | 7 | ## Credentials for Linux Project 8 | 9 | This repository is now part of the [Credentials for Linux][linux-credentials] project, and was previously known as **xdg-credentials-portal**. 10 | 11 | The [Credentials for Linux][linux-credentials] project aims to offer FIDO2 platform functionality (FIDO U2F, and WebAuthn) on Linux, over a [D-Bus Portal interface][xdg-portal]. 12 | 13 | _Looking for the D-Bus API proposal?_ Check out [platform-api][linux-credentials]. 14 | 15 | ## Features 16 | 17 | - FIDO U2F 18 | - 🟢 Registration (U2F_REGISTER) 19 | - 🟢 Authentication (U2F_AUTHENTICATE) 20 | - 🟢 Version (U2F_VERSION) 21 | - FIDO2 22 | - 🟢 Create credential 23 | - 🟢 Verify assertion 24 | - 🟢 Biometric user verification 25 | - 🟢 Discoverable credentials (resident keys) 26 | - FIDO2 to FIDO U2F downgrade 27 | - 🟢 Basic functionality 28 | - 🟢 Support for excludeList and pre-flight requests 29 | - PIN/UV Protocols 30 | - 🟢 PIN/UV Auth Protocol One 31 | - 🟢 PIN/UV Auth Protocol Two 32 | - PIN/UV Operations 33 | - 🟢 GetPinToken 34 | - 🟢 GetPinUvAuthTokenUsingPinWithPermissions 35 | - 🟢 GetPinUvAuthTokenUsingUvWithPermissions 36 | - [Passkey Authentication][passkeys] 37 | - 🟢 Discoverable credentials (resident keys) 38 | - 🟢 Hybrid transport (caBLE v2): QR-initiated transactions 39 | - 🟢 Hybrid transport (caBLE v2): State-assisted transactions (remember this phone) 40 | 41 | ## Transports 42 | 43 | | | USB (HID) | Bluetooth Low Energy (BLE) | NFC | TPM 2.0 (Platform) | Hybrid (caBLEv2) | 44 | | -------------------- | ------------------------- | -------------------------- | --------------------- | --------------------- | ---------------------------------- | 45 | | **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]) | 🟢 Supported | 47 | 48 | ## Example programs 49 | 50 | After cloning, you can try out [one of the libwebauthn examples](libwebauthn/examples): 51 | ``` 52 | $ cd libwebauthn 53 | $ git submodule update --init 54 | $ cargo run --example webauthn_hid 55 | $ cargo run --example webauthn_cable 56 | $ cargo run --example u2f_hid 57 | ``` 58 | 59 | ## Contributing 60 | 61 | We welcome contributions! 62 | 63 | If you'd like to contribute but you don't know where to start, check out the _Issues_ tab. 64 | 65 | [xdg-portal]: https://flatpak.github.io/xdg-desktop-portal/portal-docs.html 66 | [linux-credentials]: https://github.com/linux-credentials 67 | [webauthn]: https://www.w3.org/TR/webauthn/ 68 | [passkeys]: https://fidoalliance.org/passkeys/ 69 | [#10]: https://github.com/AlfioEmanueleFresta/xdg-credentials-portal/issues/10 70 | [#3]: https://github.com/AlfioEmanueleFresta/xdg-credentials-portal/issues/3 71 | [#4]: https://github.com/AlfioEmanueleFresta/xdg-credentials-portal/issues/4 72 | [#5]: https://github.com/AlfioEmanueleFresta/xdg-credentials-portal/issues/5 73 | [#17]: https://github.com/AlfioEmanueleFresta/xdg-credentials-portal/issues/17 74 | [#18]: https://github.com/AlfioEmanueleFresta/xdg-credentials-portal/issues/18 75 | [#31]: https://github.com/AlfioEmanueleFresta/xdg-credentials-portal/issues/31 76 | -------------------------------------------------------------------------------- /libwebauthn/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "libwebauthn" 3 | description = "FIDO2 (WebAuthn) and FIDO U2F platform library for Linux written in Rust " 4 | version = "0.1.2" 5 | authors = ["Alfie Fresta "] 6 | edition = "2021" 7 | license-file = "../COPYING" 8 | homepage = "https://github.com/linux-credentials" 9 | repository = "https://github.com/linux-credentials/libwebauthn" 10 | 11 | [lib] 12 | name = "libwebauthn" 13 | path = "src/lib.rs" 14 | 15 | [features] 16 | default = [] 17 | hid-device-tests = ["virtual-hid-device"] 18 | virtual-hid-device = ["solo"] 19 | 20 | [dependencies] 21 | base64-url = "3.0.0" 22 | dbus = "0.9.5" 23 | tracing = "0.1.29" 24 | maplit = "1.0.2" 25 | sha2 = "0.10.2" 26 | uuid = { version = "1.5.0", features = ["serde", "v4"] } 27 | async-trait = "0.1.36" 28 | futures = "0.3.5" 29 | tokio = { version = "1.45.0", features = ["full"] } 30 | serde = "1.0.110" 31 | serde_cbor = "0.11.2" 32 | serde-indexed = "0.1.1" 33 | serde_derive = "1.0.123" 34 | serde_repr = "0.1.6" 35 | serde_bytes = "0.11.5" 36 | num-traits = "0.2" 37 | num-derive = "0.4.1" 38 | byteorder = "1.3.4" 39 | num_enum = "0.7.1" 40 | x509-parser = "0.17.0" 41 | time = "0.3.35" 42 | curve25519-dalek = "4.1.3" 43 | hex = "0.4.3" 44 | mockall = "0.13.1" 45 | hidapi = { version = "2.4.1", default-features = false, features = [ 46 | "linux-static-hidraw", 47 | ] } 48 | bitflags = "2.4.1" 49 | rand = "0.8.5" 50 | p256 = { version = "0.13.2", features = ["ecdh", "arithmetic", "serde"] } 51 | heapless = "0.7" 52 | cosey = "0.3.2" 53 | aes = "0.8.2" 54 | hmac = "0.12.1" 55 | cbc = { version = "0.1", features = ["alloc"] } 56 | hkdf = "0.12" 57 | solo = { path = "../solo", optional = true } 58 | text_io = "0.1" 59 | tungstenite = { version = "0.26.2" } 60 | tokio-tungstenite = { version = "0.26.2", features = [ 61 | "rustls-tls-native-roots", 62 | ] } 63 | tokio-stream = "0.1.4" 64 | snow = { version = "0.10.0-alpha.1", features = ["use-p256"] } 65 | ctap-types = { version = "0.4.0" } 66 | btleplug = "0.11.7" 67 | thiserror = "2.0.12" 68 | 69 | 70 | [dev-dependencies] 71 | tracing-subscriber = { version = "0.3.3", features = ["env-filter"] } 72 | qrcode = "0.14.1" 73 | -------------------------------------------------------------------------------- /libwebauthn/examples/authenticator_config_hid.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt::Display; 3 | use std::time::Duration; 4 | 5 | use libwebauthn::management::AuthenticatorConfig; 6 | use libwebauthn::pin::PinRequestReason; 7 | use libwebauthn::proto::ctap2::{Ctap2, Ctap2GetInfoResponse}; 8 | use libwebauthn::transport::hid::list_devices; 9 | use libwebauthn::transport::Device; 10 | use libwebauthn::webauthn::Error as WebAuthnError; 11 | use libwebauthn::UxUpdate; 12 | use std::io::{self, Write}; 13 | use text_io::read; 14 | use tokio::sync::mpsc::Receiver; 15 | use tracing_subscriber::{self, EnvFilter}; 16 | 17 | const TIMEOUT: Duration = Duration::from_secs(10); 18 | 19 | fn setup_logging() { 20 | tracing_subscriber::fmt() 21 | .with_env_filter(EnvFilter::from_default_env()) 22 | .without_time() 23 | .init(); 24 | } 25 | 26 | async fn handle_updates(mut state_recv: Receiver) { 27 | while let Some(update) = state_recv.recv().await { 28 | match update { 29 | UxUpdate::PresenceRequired => println!("Please touch your device!"), 30 | UxUpdate::UvRetry { attempts_left } => { 31 | print!("UV failed."); 32 | if let Some(attempts_left) = attempts_left { 33 | print!(" You have {attempts_left} attempts left."); 34 | } 35 | } 36 | UxUpdate::PinRequired(update) => { 37 | let mut attempts_str = String::new(); 38 | if let Some(attempts) = update.attempts_left { 39 | attempts_str = format!(". You have {attempts} attempts left!"); 40 | }; 41 | 42 | match update.reason { 43 | PinRequestReason::RelyingPartyRequest => println!("RP required a PIN."), 44 | PinRequestReason::AuthenticatorPolicy => { 45 | println!("Your device requires a PIN.") 46 | } 47 | PinRequestReason::FallbackFromUV => { 48 | println!("UV failed too often and is blocked. Falling back to PIN.") 49 | } 50 | } 51 | print!("PIN: Please enter the PIN for your authenticator{attempts_str}: "); 52 | io::stdout().flush().unwrap(); 53 | let pin_raw: String = read!("{}\n"); 54 | 55 | if pin_raw.is_empty() { 56 | println!("PIN: No PIN provided, cancelling operation."); 57 | update.cancel(); 58 | } else { 59 | let _ = update.send_pin(&pin_raw); 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 67 | enum Operation { 68 | ToggleAlwaysUv, 69 | EnableForceChangePin, 70 | DisableForceChangePin, 71 | SetMinPinLength(Option), 72 | SetMinPinLengthRpids, 73 | EnableEnterpriseAttestation, 74 | } 75 | 76 | impl Display for Operation { 77 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 78 | match self { 79 | Operation::ToggleAlwaysUv => f.write_str("Toggle AlwaysUV"), 80 | Operation::EnableForceChangePin => f.write_str("Enable force change pin"), 81 | Operation::DisableForceChangePin => f.write_str("Disable force change pin"), 82 | Operation::SetMinPinLength(l) => { 83 | if let Some(length) = l { 84 | f.write_fmt(format_args!("Set min PIN length. Current length: {length}")) 85 | } else { 86 | f.write_str("Set min PIN length.") 87 | } 88 | } 89 | Operation::SetMinPinLengthRpids => f.write_str("Set min PIN length RPIDs"), 90 | Operation::EnableEnterpriseAttestation => f.write_str("Enable enterprise attestation"), 91 | } 92 | } 93 | } 94 | 95 | fn get_supported_options(info: &Ctap2GetInfoResponse) -> Vec { 96 | let mut configure_ops = vec![]; 97 | if let Some(options) = &info.options { 98 | if options.get("authnrCfg") == Some(&true) && options.get("alwaysUv").is_some() { 99 | configure_ops.push(Operation::ToggleAlwaysUv); 100 | } 101 | if options.get("authnrCfg") == Some(&true) && options.get("setMinPINLength").is_some() { 102 | if info.force_pin_change == Some(true) { 103 | configure_ops.push(Operation::DisableForceChangePin); 104 | } else { 105 | configure_ops.push(Operation::EnableForceChangePin); 106 | } 107 | configure_ops.push(Operation::SetMinPinLength(info.min_pin_length)); 108 | configure_ops.push(Operation::SetMinPinLengthRpids); 109 | } 110 | if options.get("ep").is_some() { 111 | configure_ops.push(Operation::EnableEnterpriseAttestation); 112 | } 113 | } 114 | configure_ops 115 | } 116 | 117 | #[tokio::main] 118 | pub async fn main() -> Result<(), Box> { 119 | setup_logging(); 120 | 121 | let devices = list_devices().await.unwrap(); 122 | println!("Devices found: {:?}", devices); 123 | 124 | for mut device in devices { 125 | println!("Selected HID authenticator: {}", &device); 126 | let (mut channel, state_recv) = device.channel().await?; 127 | channel.wink(TIMEOUT).await?; 128 | 129 | tokio::spawn(handle_updates(state_recv)); 130 | 131 | let info = channel.ctap2_get_info().await?; 132 | let options = get_supported_options(&info); 133 | 134 | println!("What do you want to do?"); 135 | println!(); 136 | for (idx, op) in options.iter().enumerate() { 137 | println!("({idx}) {op}"); 138 | } 139 | 140 | let idx = loop { 141 | print!("Your choice: "); 142 | io::stdout().flush().expect("Failed to flush stdout!"); 143 | let input: String = read!("{}\n"); 144 | if let Ok(idx) = input.trim().parse::() { 145 | if idx < options.len() { 146 | println!(); 147 | break idx; 148 | } 149 | } 150 | }; 151 | 152 | let mut min_pin_length_rpids = Vec::new(); 153 | if options[idx] == Operation::SetMinPinLengthRpids { 154 | loop { 155 | print!("Add RPIDs to list (enter empty string once finished): "); 156 | io::stdout().flush().expect("Failed to flush stdout!"); 157 | let input: String = read!("{}\n"); 158 | let trimmed = input.trim().to_string(); 159 | if trimmed.is_empty() { 160 | break; 161 | } else { 162 | min_pin_length_rpids.push(trimmed); 163 | } 164 | } 165 | }; 166 | 167 | let new_pin_length = if matches!(options[idx], Operation::SetMinPinLength(..)) { 168 | loop { 169 | print!("New minimum PIN length: "); 170 | io::stdout().flush().expect("Failed to flush stdout!"); 171 | let input: String = read!("{}\n"); 172 | match input.trim().parse::() { 173 | Ok(l) => { 174 | break l; 175 | } 176 | Err(_) => continue, 177 | } 178 | } 179 | } else { 180 | 0 181 | }; 182 | 183 | loop { 184 | let action = match options[idx] { 185 | Operation::ToggleAlwaysUv => channel.toggle_always_uv(TIMEOUT), 186 | Operation::SetMinPinLengthRpids => { 187 | channel.set_min_pin_length_rpids(min_pin_length_rpids.clone(), TIMEOUT) 188 | } 189 | Operation::SetMinPinLength(..) => { 190 | channel.set_min_pin_length(new_pin_length, TIMEOUT) 191 | } 192 | Operation::EnableEnterpriseAttestation => { 193 | channel.enable_enterprise_attestation(TIMEOUT) 194 | } 195 | Operation::EnableForceChangePin => channel.force_change_pin(true, TIMEOUT), 196 | Operation::DisableForceChangePin => channel.force_change_pin(false, TIMEOUT), 197 | }; 198 | match action.await { 199 | Ok(_) => break Ok(()), 200 | Err(WebAuthnError::Ctap(ctap_error)) => { 201 | if ctap_error.is_retryable_user_error() { 202 | println!("Oops, try again! Error: {}", ctap_error); 203 | continue; 204 | } 205 | break Err(WebAuthnError::Ctap(ctap_error)); 206 | } 207 | Err(err) => break Err(err), 208 | }; 209 | } 210 | .unwrap(); 211 | println!("Authenticator config done!"); 212 | } 213 | 214 | Ok(()) 215 | } 216 | -------------------------------------------------------------------------------- /libwebauthn/examples/change_pin_hid.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::time::Duration; 3 | 4 | use libwebauthn::{ 5 | pin::{PinManagement, PinRequestReason}, 6 | UxUpdate, 7 | }; 8 | use tokio::sync::mpsc::Receiver; 9 | use tracing_subscriber::{self, EnvFilter}; 10 | 11 | use libwebauthn::transport::hid::list_devices; 12 | use libwebauthn::transport::Device; 13 | use libwebauthn::webauthn::Error as WebAuthnError; 14 | use std::io::{self, Write}; 15 | use text_io::read; 16 | 17 | const TIMEOUT: Duration = Duration::from_secs(10); 18 | 19 | fn setup_logging() { 20 | tracing_subscriber::fmt() 21 | .with_env_filter(EnvFilter::from_default_env()) 22 | .without_time() 23 | .init(); 24 | } 25 | 26 | async fn handle_updates(mut state_recv: Receiver) { 27 | while let Some(update) = state_recv.recv().await { 28 | match update { 29 | UxUpdate::PresenceRequired => println!("Please touch your device!"), 30 | UxUpdate::UvRetry { attempts_left } => { 31 | print!("UV failed."); 32 | if let Some(attempts_left) = attempts_left { 33 | print!(" You have {attempts_left} attempts left."); 34 | } 35 | } 36 | UxUpdate::PinRequired(update) => { 37 | let mut attempts_str = String::new(); 38 | if let Some(attempts) = update.attempts_left { 39 | attempts_str = format!(". You have {attempts} attempts left!"); 40 | }; 41 | 42 | match update.reason { 43 | PinRequestReason::RelyingPartyRequest => println!("RP required a PIN."), 44 | PinRequestReason::AuthenticatorPolicy => { 45 | println!("Your device requires a PIN.") 46 | } 47 | PinRequestReason::FallbackFromUV => { 48 | println!("UV failed too often and is blocked. Falling back to PIN.") 49 | } 50 | } 51 | print!("PIN: Please enter the PIN for your authenticator{attempts_str}: "); 52 | io::stdout().flush().unwrap(); 53 | let pin_raw: String = read!("{}\n"); 54 | 55 | if pin_raw.is_empty() { 56 | println!("PIN: No PIN provided, cancelling operation."); 57 | update.cancel(); 58 | } else { 59 | let _ = update.send_pin(&pin_raw); 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | #[tokio::main] 67 | pub async fn main() -> Result<(), Box> { 68 | setup_logging(); 69 | 70 | let devices = list_devices().await.unwrap(); 71 | println!("Devices found: {:?}", devices); 72 | 73 | for mut device in devices { 74 | println!("Selected HID authenticator: {}", &device); 75 | let (mut channel, state_recv) = device.channel().await?; 76 | channel.wink(TIMEOUT).await?; 77 | 78 | print!("PIN: Please enter the _new_ PIN: "); 79 | io::stdout().flush().unwrap(); 80 | let new_pin: String = read!("{}\n"); 81 | 82 | if &new_pin == "" { 83 | println!("PIN: No PIN provided, cancelling operation."); 84 | return Ok(()); 85 | } 86 | 87 | tokio::spawn(handle_updates(state_recv)); 88 | 89 | let response = loop { 90 | match channel.change_pin(new_pin.clone(), TIMEOUT).await { 91 | Ok(response) => break Ok(response), 92 | Err(WebAuthnError::Ctap(ctap_error)) => { 93 | if ctap_error.is_retryable_user_error() { 94 | println!("Oops, try again! Error: {}", ctap_error); 95 | continue; 96 | } 97 | break Err(WebAuthnError::Ctap(ctap_error)); 98 | } 99 | Err(err) => break Err(err), 100 | }; 101 | } 102 | .unwrap(); 103 | println!("WebAuthn response: {:?}", response); 104 | } 105 | 106 | Ok(()) 107 | } 108 | -------------------------------------------------------------------------------- /libwebauthn/examples/cred_management.rs: -------------------------------------------------------------------------------- 1 | use libwebauthn::management::CredentialManagement; 2 | use libwebauthn::pin::PinRequestReason; 3 | use libwebauthn::proto::ctap2::{ 4 | Ctap2, Ctap2CredentialData, Ctap2PublicKeyCredentialRpEntity, Ctap2RPData, 5 | }; 6 | use libwebauthn::proto::CtapError; 7 | use libwebauthn::transport::hid::list_devices; 8 | use libwebauthn::transport::Device; 9 | use libwebauthn::webauthn::Error as WebAuthnError; 10 | use libwebauthn::UxUpdate; 11 | use std::fmt::Display; 12 | use std::io::{self, Write}; 13 | use std::time::Duration; 14 | use text_io::read; 15 | use tokio::sync::mpsc::Receiver; 16 | use tracing_subscriber::{self, EnvFilter}; 17 | 18 | const TIMEOUT: Duration = Duration::from_secs(10); 19 | 20 | fn setup_logging() { 21 | tracing_subscriber::fmt() 22 | .with_env_filter(EnvFilter::from_default_env()) 23 | .without_time() 24 | .init(); 25 | } 26 | 27 | async fn handle_updates(mut state_recv: Receiver) { 28 | while let Some(update) = state_recv.recv().await { 29 | match update { 30 | UxUpdate::PresenceRequired => println!("Please touch your device!"), 31 | UxUpdate::UvRetry { attempts_left } => { 32 | print!("UV failed."); 33 | if let Some(attempts_left) = attempts_left { 34 | print!(" You have {attempts_left} attempts left."); 35 | } 36 | } 37 | UxUpdate::PinRequired(update) => { 38 | let mut attempts_str = String::new(); 39 | if let Some(attempts) = update.attempts_left { 40 | attempts_str = format!(". You have {attempts} attempts left!"); 41 | }; 42 | 43 | match update.reason { 44 | PinRequestReason::RelyingPartyRequest => println!("RP required a PIN."), 45 | PinRequestReason::AuthenticatorPolicy => { 46 | println!("Your device requires a PIN.") 47 | } 48 | PinRequestReason::FallbackFromUV => { 49 | println!("UV failed too often and is blocked. Falling back to PIN.") 50 | } 51 | } 52 | print!("PIN: Please enter the PIN for your authenticator{attempts_str}: "); 53 | io::stdout().flush().unwrap(); 54 | let pin_raw: String = read!("{}\n"); 55 | 56 | if pin_raw.is_empty() { 57 | println!("PIN: No PIN provided, cancelling operation."); 58 | update.cancel(); 59 | } else { 60 | let _ = update.send_pin(&pin_raw); 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | macro_rules! handle_retries { 68 | ($res:expr) => { 69 | loop { 70 | match $res.await { 71 | Ok(r) => break r, 72 | Err(WebAuthnError::Ctap(ctap_error)) => { 73 | if ctap_error.is_retryable_user_error() { 74 | println!("Oops, try again! Error: {}", ctap_error); 75 | continue; 76 | } 77 | return Err(WebAuthnError::Ctap(ctap_error)); 78 | } 79 | Err(err) => return Err(err), 80 | } 81 | } 82 | }; 83 | } 84 | 85 | fn format_rp(rp: &Ctap2PublicKeyCredentialRpEntity) -> String { 86 | rp.name.clone().unwrap_or(rp.id.clone()) 87 | } 88 | 89 | fn format_credential(cred: &Ctap2CredentialData) -> String { 90 | cred.user 91 | .display_name 92 | .clone() 93 | .unwrap_or(cred.user.name.clone().unwrap_or("".into())) 94 | .to_string() 95 | } 96 | 97 | async fn enumerate_rps( 98 | channel: &mut T, 99 | ) -> Result, WebAuthnError> { 100 | let (rp, total_rps) = handle_retries!(channel.enumerate_rps_begin(TIMEOUT)); 101 | let mut rps = vec![rp]; 102 | // Starting at 1, as we already have one from the begin-call. 103 | for _ in 1..total_rps { 104 | let rp = handle_retries!(channel.enumerate_rps_next_rp(TIMEOUT)); 105 | rps.push(rp); 106 | } 107 | Ok(rps) 108 | } 109 | 110 | async fn enumerate_credentials_for_rp( 111 | channel: &mut T, 112 | rp_id_hash: &[u8], 113 | ) -> Result, WebAuthnError> { 114 | let (credential, num_of_creds) = 115 | handle_retries!(channel.enumerate_credentials_begin(rp_id_hash, TIMEOUT)); 116 | let mut credentials = vec![credential]; 117 | // Starting at 1, as we already have one from the begin-call. 118 | for _ in 1..num_of_creds { 119 | let credential = handle_retries!(channel.enumerate_credentials_next(TIMEOUT)); 120 | credentials.push(credential); 121 | } 122 | Ok(credentials) 123 | } 124 | 125 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 126 | enum Operation { 127 | GetMetadata, 128 | EnumerateRPs, 129 | EnumerateCredentials, 130 | RemoveCredential, 131 | UpdateUserInfo, 132 | } 133 | 134 | impl Display for Operation { 135 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 136 | match self { 137 | Operation::GetMetadata => f.write_str("Get metadata"), 138 | Operation::EnumerateRPs => f.write_str("Enumerate relying parties"), 139 | Operation::EnumerateCredentials => f.write_str("Enumerate credentials"), 140 | Operation::RemoveCredential => f.write_str("Remove credential"), 141 | Operation::UpdateUserInfo => f.write_str("Update user info"), 142 | } 143 | } 144 | } 145 | 146 | fn ask_for_user_input(num_of_items: usize) -> usize { 147 | let idx = loop { 148 | print!("Your choice: "); 149 | io::stdout().flush().expect("Failed to flush stdout!"); 150 | let input: String = read!("{}\n"); 151 | if let Ok(idx) = input.trim().parse::() { 152 | if idx < num_of_items { 153 | println!(); 154 | break idx; 155 | } 156 | } 157 | }; 158 | idx 159 | } 160 | 161 | #[tokio::main] 162 | pub async fn main() -> Result<(), WebAuthnError> { 163 | setup_logging(); 164 | 165 | let devices = list_devices().await.unwrap(); 166 | println!("Devices found: {:?}", devices); 167 | 168 | for mut device in devices { 169 | println!("Selected HID authenticator: {}", &device); 170 | let (mut channel, state_recv) = device.channel().await?; 171 | channel.wink(TIMEOUT).await?; 172 | 173 | tokio::spawn(handle_updates(state_recv)); 174 | 175 | let info = channel.ctap2_get_info().await?; 176 | 177 | if !info.supports_credential_management() { 178 | println!("Your token does not support credential management."); 179 | return Err(WebAuthnError::Ctap(CtapError::InvalidCommand)); 180 | } 181 | 182 | let options = [ 183 | Operation::GetMetadata, 184 | Operation::EnumerateRPs, 185 | Operation::EnumerateCredentials, 186 | Operation::RemoveCredential, 187 | Operation::UpdateUserInfo, 188 | ]; 189 | 190 | println!("What do you want to do?"); 191 | println!(); 192 | for (idx, op) in options.iter().enumerate() { 193 | println!("({idx}) {op}"); 194 | } 195 | 196 | let idx = ask_for_user_input(options.len()); 197 | let metadata = handle_retries!(channel.get_credential_metadata(TIMEOUT)); 198 | if options[idx] == Operation::GetMetadata { 199 | println!("Metadata: {metadata:#?}"); 200 | return Ok(()); 201 | } 202 | 203 | let rps = enumerate_rps(&mut channel).await?; 204 | if options[idx] == Operation::EnumerateRPs { 205 | println!("RPs:"); 206 | for rp in &rps { 207 | println!("{}", format_rp(&rp.rp)); 208 | } 209 | return Ok(()); 210 | } 211 | 212 | let mut credlist = Vec::new(); 213 | for rp in &rps { 214 | let creds = enumerate_credentials_for_rp(&mut channel, &rp.rp_id_hash).await?; 215 | for cred in creds { 216 | credlist.push((rp.rp.clone(), cred)); 217 | } 218 | } 219 | if options[idx] == Operation::EnumerateCredentials { 220 | println!("Credentials:"); 221 | for (rp, cred) in &credlist { 222 | println!("{}: {}", format_rp(rp), format_credential(cred)); 223 | } 224 | return Ok(()); 225 | } 226 | 227 | // For all remaining operations, we need to enumerate the found creds 228 | for (idx, (rp, cred)) in credlist.iter().enumerate() { 229 | println!("({idx}) {}: {}", format_rp(rp), format_credential(cred)); 230 | } 231 | 232 | let cred_idx = ask_for_user_input(options.len()); 233 | 234 | if options[idx] == Operation::RemoveCredential { 235 | let (_, cred) = &credlist[cred_idx]; 236 | handle_retries!(channel.delete_credential(&cred.credential_id, TIMEOUT)); 237 | println!("Done"); 238 | return Ok(()); 239 | } 240 | 241 | if options[idx] == Operation::UpdateUserInfo { 242 | let name = loop { 243 | print!("New user name: "); 244 | io::stdout().flush().expect("Failed to flush stdout!"); 245 | let input: String = read!("{}\n"); 246 | let input = input.trim(); 247 | if !input.is_empty() { 248 | println!(); 249 | break input.to_string(); 250 | } 251 | }; 252 | let (_rp, cred) = &credlist[cred_idx]; 253 | let mut user = cred.user.clone(); 254 | user.name = Some(name); 255 | handle_retries!(channel.update_user_info(&cred.credential_id, &user, TIMEOUT)); 256 | println!("Done"); 257 | return Ok(()); 258 | } 259 | } 260 | 261 | Ok(()) 262 | } 263 | -------------------------------------------------------------------------------- /libwebauthn/examples/prf_test.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::convert::TryInto; 3 | use std::error::Error; 4 | use std::io::{self, Write}; 5 | use std::time::Duration; 6 | 7 | use libwebauthn::transport::hid::channel::HidChannel; 8 | use libwebauthn::UxUpdate; 9 | use rand::{thread_rng, Rng}; 10 | use serde_bytes::ByteBuf; 11 | use text_io::read; 12 | use tokio::sync::mpsc::Receiver; 13 | use tracing_subscriber::{self, EnvFilter}; 14 | 15 | use libwebauthn::ops::webauthn::{ 16 | GetAssertionHmacOrPrfInput, GetAssertionRequest, GetAssertionRequestExtensions, PRFValue, 17 | UserVerificationRequirement, 18 | }; 19 | use libwebauthn::pin::PinRequestReason; 20 | use libwebauthn::proto::ctap2::{Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialType}; 21 | use libwebauthn::transport::hid::list_devices; 22 | use libwebauthn::transport::Device; 23 | use libwebauthn::webauthn::{Error as WebAuthnError, WebAuthn}; 24 | 25 | const TIMEOUT: Duration = Duration::from_secs(10); 26 | 27 | fn setup_logging() { 28 | tracing_subscriber::fmt() 29 | .with_env_filter(EnvFilter::from_default_env()) 30 | .without_time() 31 | .init(); 32 | } 33 | 34 | async fn handle_updates(mut state_recv: Receiver) { 35 | while let Some(update) = state_recv.recv().await { 36 | match update { 37 | UxUpdate::PresenceRequired => println!("Please touch your device!"), 38 | UxUpdate::UvRetry { attempts_left } => { 39 | print!("UV failed."); 40 | if let Some(attempts_left) = attempts_left { 41 | print!(" You have {attempts_left} attempts left."); 42 | } 43 | } 44 | UxUpdate::PinRequired(update) => { 45 | let mut attempts_str = String::new(); 46 | if let Some(attempts) = update.attempts_left { 47 | attempts_str = format!(". You have {attempts} attempts left!"); 48 | }; 49 | 50 | match update.reason { 51 | PinRequestReason::RelyingPartyRequest => println!("RP required a PIN."), 52 | PinRequestReason::AuthenticatorPolicy => { 53 | println!("Your device requires a PIN.") 54 | } 55 | PinRequestReason::FallbackFromUV => { 56 | println!("UV failed too often and is blocked. Falling back to PIN.") 57 | } 58 | } 59 | print!("PIN: Please enter the PIN for your authenticator{attempts_str}: "); 60 | io::stdout().flush().unwrap(); 61 | let pin_raw: String = read!("{}\n"); 62 | 63 | if pin_raw.is_empty() { 64 | println!("PIN: No PIN provided, cancelling operation."); 65 | update.cancel(); 66 | } else { 67 | let _ = update.send_pin(&pin_raw); 68 | } 69 | } 70 | } 71 | } 72 | } 73 | 74 | #[tokio::main] 75 | pub async fn main() -> Result<(), Box> { 76 | setup_logging(); 77 | 78 | let argv: Vec<_> = std::env::args().collect(); 79 | if argv.len() != 3 { 80 | println!("Usage: cargo run --example prf_test -- CREDENTIAL_ID FIRST_PRF_INPUT"); 81 | println!(); 82 | println!("CREDENTIAL_ID: Credential ID to be used to sign against, as a hexstring (like 5830c80ae90f7865c631626573f1fdc7..)"); 83 | println!( 84 | "FIRST_PRF_INPUT: PRF input to be used as a hexstring. Needs to be 32 bytes long!" 85 | ); 86 | // println!("EXPECTED_RESULT: PRF output from the demo-webpage, that should be reproduced with this crate."); 87 | println!(); 88 | println!("How to use:"); 89 | println!("1. Go to https://demo.yubico.com/webauthn-developers"); 90 | println!("2. Register there with PRF extension enabled, using your favorite browser"); 91 | println!("3. Sign in, with FIRST_PRF_INPUT set"); 92 | println!("4. Copy out the used hexstrings for credential_id and PRF input, and use them with this example"); 93 | println!("5. Hope the outputs match"); 94 | return Ok(()); 95 | } 96 | let credential_id = 97 | hex::decode(argv[1].clone()).expect("CREDENTIAL_ID is not a valid hex code"); 98 | let first_prf_input = hex::decode(argv[2].clone()) 99 | .expect("FIRST_PRF_INPUT is not a valid hex code") 100 | .try_into() 101 | .expect("FIRST_PRF_INPUT is not exactly 32 bytes long"); 102 | 103 | let devices = list_devices().await.unwrap(); 104 | println!("Devices found: {:?}", devices); 105 | 106 | let challenge: [u8; 32] = thread_rng().gen(); 107 | 108 | for mut device in devices { 109 | println!("Selected HID authenticator: {}", &device); 110 | let (mut channel, state_recv) = device.channel().await?; 111 | channel.wink(TIMEOUT).await?; 112 | 113 | tokio::spawn(handle_updates(state_recv)); 114 | 115 | let credential = Ctap2PublicKeyCredentialDescriptor { 116 | r#type: Ctap2PublicKeyCredentialType::PublicKey, 117 | id: ByteBuf::from(credential_id.as_slice()), 118 | transports: None, 119 | }; 120 | 121 | // eval only 122 | let eval = Some(PRFValue { 123 | first: first_prf_input, 124 | second: None, 125 | }); 126 | 127 | let eval_by_credential = HashMap::new(); 128 | let hmac_or_prf = GetAssertionHmacOrPrfInput::Prf { 129 | eval, 130 | eval_by_credential, 131 | }; 132 | run_success_test( 133 | &mut channel, 134 | &credential, 135 | &challenge, 136 | hmac_or_prf, 137 | "PRF output: ", 138 | ) 139 | .await; 140 | } 141 | Ok(()) 142 | } 143 | 144 | async fn run_success_test( 145 | channel: &mut HidChannel<'_>, 146 | credential: &Ctap2PublicKeyCredentialDescriptor, 147 | challenge: &[u8; 32], 148 | hmac_or_prf: GetAssertionHmacOrPrfInput, 149 | printoutput: &str, 150 | ) { 151 | let get_assertion = GetAssertionRequest { 152 | relying_party_id: "demo.yubico.com".to_owned(), 153 | hash: Vec::from(challenge), 154 | allow: vec![credential.clone()], 155 | user_verification: UserVerificationRequirement::Preferred, 156 | extensions: Some(GetAssertionRequestExtensions { 157 | hmac_or_prf, 158 | ..Default::default() 159 | }), 160 | timeout: TIMEOUT, 161 | }; 162 | 163 | let response = loop { 164 | match channel.webauthn_get_assertion(&get_assertion).await { 165 | Ok(response) => break Ok(response), 166 | Err(WebAuthnError::Ctap(ctap_error)) => { 167 | if ctap_error.is_retryable_user_error() { 168 | println!("Oops, try again! Error: {}", ctap_error); 169 | continue; 170 | } 171 | break Err(WebAuthnError::Ctap(ctap_error)); 172 | } 173 | Err(err) => break Err(err), 174 | }; 175 | } 176 | .unwrap(); 177 | for (num, assertion) in response.assertions.iter().enumerate() { 178 | println!( 179 | "{num}. result of {printoutput}: {:?}", 180 | assertion 181 | .unsigned_extensions_output 182 | .as_ref() 183 | .map(|e| if let Some(prf) = &e.prf { 184 | let results = prf.results.as_ref().map(|r| hex::encode(r.first)).unwrap(); 185 | format!("Found PRF results: {}", results) 186 | } else if e.hmac_get_secret.is_some() { 187 | String::from("ERROR: Got HMAC instead of PRF output") 188 | } else { 189 | String::from("ERROR: No PRF output") 190 | }) 191 | .unwrap_or(String::from("ERROR: No extensions returned")) 192 | ); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /libwebauthn/examples/u2f_ble.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::time::Duration; 3 | 4 | use libwebauthn::UxUpdate; 5 | use tokio::sync::mpsc::Receiver; 6 | use tracing_subscriber::{self, EnvFilter}; 7 | 8 | use libwebauthn::ops::u2f::{RegisterRequest, SignRequest}; 9 | use libwebauthn::transport::ble::list_devices; 10 | use libwebauthn::transport::Device; 11 | use libwebauthn::u2f::U2F; 12 | 13 | const TIMEOUT: Duration = Duration::from_secs(10); 14 | 15 | fn setup_logging() { 16 | tracing_subscriber::fmt() 17 | .with_env_filter(EnvFilter::from_default_env()) 18 | .without_time() 19 | .init(); 20 | } 21 | 22 | async fn handle_updates(mut state_recv: Receiver) { 23 | while let Some(update) = state_recv.recv().await { 24 | match update { 25 | UxUpdate::PresenceRequired => println!("Please touch your device!"), 26 | _ => { /* U2F doesn't use other state updates */ } 27 | } 28 | } 29 | } 30 | 31 | #[tokio::main] 32 | pub async fn main() -> Result<(), Box> { 33 | setup_logging(); 34 | 35 | let devices = list_devices().await?; 36 | println!("Found {} devices.", devices.len()); 37 | 38 | for mut device in devices { 39 | let (mut channel, state_recv) = device.channel().await?; 40 | 41 | const APP_ID: &str = "https://foo.example.org"; 42 | let challenge: &[u8] = 43 | &base64_url::decode("1vQ9mxionq0ngCnjD-wTsv1zUSrGRtFqG2xP09SbZ70").unwrap(); 44 | // Registration ceremony 45 | println!("Registration request sent (timeout: {:?}).", TIMEOUT); 46 | let register_request = 47 | RegisterRequest::new_u2f_v2(&APP_ID, &challenge, vec![], TIMEOUT, false); 48 | 49 | tokio::spawn(handle_updates(state_recv)); 50 | let response = channel.u2f_register(®ister_request).await?; 51 | println!("Response: {:?}", response); 52 | 53 | // Signature ceremony 54 | println!("Signature request sent (timeout: {:?} seconds).", TIMEOUT); 55 | let new_key = response.as_registered_key()?; 56 | let sign_request = 57 | SignRequest::new(&APP_ID, &challenge, &new_key.key_handle, TIMEOUT, true); 58 | let response = channel.u2f_sign(&sign_request).await?; 59 | println!("Response: {:?}", response); 60 | } 61 | 62 | Ok(()) 63 | } 64 | -------------------------------------------------------------------------------- /libwebauthn/examples/u2f_hid.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::time::Duration; 3 | 4 | use libwebauthn::UxUpdate; 5 | use tokio::sync::mpsc::Receiver; 6 | use tracing_subscriber::{self, EnvFilter}; 7 | 8 | use libwebauthn::ops::u2f::{RegisterRequest, SignRequest}; 9 | use libwebauthn::transport::hid::list_devices; 10 | use libwebauthn::transport::Device; 11 | use libwebauthn::u2f::U2F; 12 | 13 | const TIMEOUT: Duration = Duration::from_secs(10); 14 | 15 | fn setup_logging() { 16 | tracing_subscriber::fmt() 17 | .pretty() 18 | .with_env_filter(EnvFilter::from_default_env()) 19 | // .without_time() 20 | .init(); 21 | } 22 | 23 | async fn handle_updates(mut state_recv: Receiver) { 24 | while let Some(update) = state_recv.recv().await { 25 | match update { 26 | UxUpdate::PresenceRequired => println!("Please touch your device!"), 27 | _ => { /* U2F doesn't use other state updates */ } 28 | } 29 | } 30 | } 31 | 32 | #[tokio::main] 33 | pub async fn main() -> Result<(), Box> { 34 | setup_logging(); 35 | 36 | let devices = list_devices().await?; 37 | 38 | println!("Found {} devices.", devices.len()); 39 | for mut device in devices { 40 | println!("Winking device: {}", device); 41 | let (mut channel, state_recv) = device.channel().await?; 42 | channel.wink(TIMEOUT).await?; 43 | 44 | const APP_ID: &str = "https://foo.example.org"; 45 | let challenge: &[u8] = 46 | &base64_url::decode("1vQ9mxionq0ngCnjD-wTsv1zUSrGRtFqG2xP09SbZ70").unwrap(); 47 | // Registration ceremony 48 | println!("Registration request sent (timeout: {:?}).", TIMEOUT); 49 | let register_request = 50 | RegisterRequest::new_u2f_v2(&APP_ID, &challenge, vec![], TIMEOUT, false); 51 | 52 | tokio::spawn(handle_updates(state_recv)); 53 | let response = channel.u2f_register(®ister_request).await?; 54 | println!("Response: {:?}", response); 55 | 56 | // Signature ceremony 57 | println!("Signature request sent (timeout: {:?} seconds).", TIMEOUT); 58 | let new_key = response.as_registered_key()?; 59 | let sign_request = 60 | SignRequest::new(&APP_ID, &challenge, &new_key.key_handle, TIMEOUT, true); 61 | let response = channel.u2f_sign(&sign_request).await?; 62 | println!("Response: {:?}", response); 63 | } 64 | 65 | Ok(()) 66 | } 67 | -------------------------------------------------------------------------------- /libwebauthn/examples/webauthn_cable.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::io::{self, Write}; 3 | use std::sync::Arc; 4 | use std::time::Duration; 5 | 6 | use libwebauthn::pin::PinRequestReason; 7 | use libwebauthn::transport::cable::known_devices::{ 8 | CableKnownDevice, ClientPayloadHint, EphemeralDeviceInfoStore, 9 | }; 10 | use libwebauthn::transport::cable::qr_code_device::{CableQrCodeDevice, QrCodeOperationHint}; 11 | use libwebauthn::UxUpdate; 12 | use qrcode::render::unicode; 13 | use qrcode::QrCode; 14 | use rand::{thread_rng, Rng}; 15 | use text_io::read; 16 | use tokio::sync::mpsc::Receiver; 17 | use tokio::time::sleep; 18 | use tracing_subscriber::{self, EnvFilter}; 19 | 20 | use libwebauthn::ops::webauthn::{ 21 | GetAssertionRequest, MakeCredentialRequest, UserVerificationRequirement, 22 | }; 23 | use libwebauthn::proto::ctap2::{ 24 | Ctap2CredentialType, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity, 25 | Ctap2PublicKeyCredentialUserEntity, 26 | }; 27 | use libwebauthn::transport::Device; 28 | use libwebauthn::webauthn::{Error as WebAuthnError, WebAuthn}; 29 | 30 | const TIMEOUT: Duration = Duration::from_secs(120); 31 | 32 | fn setup_logging() { 33 | tracing_subscriber::fmt() 34 | .with_env_filter(EnvFilter::from_default_env()) 35 | .without_time() 36 | .init(); 37 | } 38 | 39 | async fn handle_updates(mut state_recv: Receiver) { 40 | while let Some(update) = state_recv.recv().await { 41 | match update { 42 | UxUpdate::PresenceRequired => println!("Please touch your device!"), 43 | UxUpdate::UvRetry { attempts_left } => { 44 | print!("UV failed."); 45 | if let Some(attempts_left) = attempts_left { 46 | print!(" You have {attempts_left} attempts left."); 47 | } 48 | } 49 | UxUpdate::PinRequired(update) => { 50 | let mut attempts_str = String::new(); 51 | if let Some(attempts) = update.attempts_left { 52 | attempts_str = format!(". You have {attempts} attempts left!"); 53 | }; 54 | 55 | match update.reason { 56 | PinRequestReason::RelyingPartyRequest => println!("RP required a PIN."), 57 | PinRequestReason::AuthenticatorPolicy => { 58 | println!("Your device requires a PIN.") 59 | } 60 | PinRequestReason::FallbackFromUV => { 61 | println!("UV failed too often and is blocked. Falling back to PIN.") 62 | } 63 | } 64 | print!("PIN: Please enter the PIN for your authenticator{attempts_str}: "); 65 | io::stdout().flush().unwrap(); 66 | let pin_raw: String = read!("{}\n"); 67 | 68 | if pin_raw.is_empty() { 69 | println!("PIN: No PIN provided, cancelling operation."); 70 | update.cancel(); 71 | } else { 72 | let _ = update.send_pin(&pin_raw); 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | #[tokio::main] 80 | pub async fn main() -> Result<(), Box> { 81 | setup_logging(); 82 | 83 | let device_info_store = Arc::new(EphemeralDeviceInfoStore::default()); 84 | let user_id: [u8; 32] = thread_rng().gen(); 85 | let challenge: [u8; 32] = thread_rng().gen(); 86 | 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::() 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 | }; 122 | 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)); 135 | } 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; 147 | 148 | let get_assertion = GetAssertionRequest { 149 | relying_party_id: "example.org".to_owned(), 150 | hash: Vec::from(challenge), 151 | allow: vec![credential], 152 | user_verification: UserVerificationRequirement::Discouraged, 153 | extensions: None, 154 | timeout: TIMEOUT, 155 | }; 156 | 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 = CableKnownDevice::new( 162 | ClientPayloadHint::GetAssertion, 163 | known_device_info, 164 | device_info_store.clone(), 165 | ) 166 | .await 167 | .unwrap(); 168 | 169 | // Connect to a known device 170 | let (mut channel, state_recv) = known_device.channel().await.unwrap(); 171 | println!("Tunnel established {:?}", channel); 172 | 173 | tokio::spawn(handle_updates(state_recv)); 174 | 175 | let response = loop { 176 | match channel.webauthn_get_assertion(&get_assertion).await { 177 | Ok(response) => break Ok(response), 178 | Err(WebAuthnError::Ctap(ctap_error)) => { 179 | if ctap_error.is_retryable_user_error() { 180 | println!("Oops, try again! Error: {}", ctap_error); 181 | continue; 182 | } 183 | break Err(WebAuthnError::Ctap(ctap_error)); 184 | } 185 | Err(err) => break Err(err), 186 | }; 187 | } 188 | .unwrap(); 189 | println!("WebAuthn GetAssertion response: {:?}", response); 190 | 191 | Ok(()) 192 | } 193 | -------------------------------------------------------------------------------- /libwebauthn/examples/webauthn_extensions_hid.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | use std::error::Error; 3 | use std::io::{self, Write}; 4 | use std::time::Duration; 5 | 6 | use libwebauthn::UxUpdate; 7 | use rand::{thread_rng, Rng}; 8 | use text_io::read; 9 | use tokio::sync::mpsc::Receiver; 10 | use tracing_subscriber::{self, EnvFilter}; 11 | 12 | use libwebauthn::ops::webauthn::{ 13 | CredentialProtectionExtension, CredentialProtectionPolicy, GetAssertionHmacOrPrfInput, 14 | GetAssertionRequest, GetAssertionRequestExtensions, HMACGetSecretInput, 15 | MakeCredentialHmacOrPrfInput, MakeCredentialLargeBlobExtension, MakeCredentialRequest, 16 | MakeCredentialsRequestExtensions, UserVerificationRequirement, 17 | }; 18 | use libwebauthn::pin::PinRequestReason; 19 | use libwebauthn::proto::ctap2::{ 20 | Ctap2CredentialType, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity, 21 | Ctap2PublicKeyCredentialUserEntity, 22 | }; 23 | use libwebauthn::transport::hid::list_devices; 24 | use libwebauthn::transport::Device; 25 | use libwebauthn::webauthn::{Error as WebAuthnError, WebAuthn}; 26 | 27 | const TIMEOUT: Duration = Duration::from_secs(10); 28 | 29 | fn setup_logging() { 30 | tracing_subscriber::fmt() 31 | .with_env_filter(EnvFilter::from_default_env()) 32 | .without_time() 33 | .init(); 34 | } 35 | 36 | async fn handle_updates(mut state_recv: Receiver) { 37 | while let Some(update) = state_recv.recv().await { 38 | match update { 39 | UxUpdate::PresenceRequired => println!("Please touch your device!"), 40 | UxUpdate::UvRetry { attempts_left } => { 41 | print!("UV failed."); 42 | if let Some(attempts_left) = attempts_left { 43 | print!(" You have {attempts_left} attempts left."); 44 | } 45 | } 46 | UxUpdate::PinRequired(update) => { 47 | let mut attempts_str = String::new(); 48 | if let Some(attempts) = update.attempts_left { 49 | attempts_str = format!(". You have {attempts} attempts left!"); 50 | }; 51 | 52 | match update.reason { 53 | PinRequestReason::RelyingPartyRequest => println!("RP required a PIN."), 54 | PinRequestReason::AuthenticatorPolicy => { 55 | println!("Your device requires a PIN.") 56 | } 57 | PinRequestReason::FallbackFromUV => { 58 | println!("UV failed too often and is blocked. Falling back to PIN.") 59 | } 60 | } 61 | print!("PIN: Please enter the PIN for your authenticator{attempts_str}: "); 62 | io::stdout().flush().unwrap(); 63 | let pin_raw: String = read!("{}\n"); 64 | 65 | if pin_raw.is_empty() { 66 | println!("PIN: No PIN provided, cancelling operation."); 67 | update.cancel(); 68 | } else { 69 | let _ = update.send_pin(&pin_raw); 70 | } 71 | } 72 | } 73 | } 74 | } 75 | 76 | #[tokio::main] 77 | pub async fn main() -> Result<(), Box> { 78 | setup_logging(); 79 | 80 | let devices = list_devices().await.unwrap(); 81 | println!("Devices found: {:?}", devices); 82 | 83 | let user_id: [u8; 32] = thread_rng().gen(); 84 | let challenge: [u8; 32] = thread_rng().gen(); 85 | 86 | let extensions = MakeCredentialsRequestExtensions { 87 | cred_protect: Some(CredentialProtectionExtension { 88 | policy: CredentialProtectionPolicy::UserVerificationRequired, 89 | enforce_policy: true, 90 | }), 91 | cred_blob: Some(r"My own little blob".into()), 92 | large_blob: MakeCredentialLargeBlobExtension::None, 93 | min_pin_length: Some(true), 94 | hmac_or_prf: MakeCredentialHmacOrPrfInput::HmacGetSecret, 95 | cred_props: Some(true), 96 | }; 97 | 98 | for mut device in devices { 99 | println!("Selected HID authenticator: {}", &device); 100 | let (mut channel, state_recv) = device.channel().await?; 101 | channel.wink(TIMEOUT).await?; 102 | 103 | tokio::spawn(handle_updates(state_recv)); 104 | 105 | // Make Credentials ceremony 106 | let make_credentials_request = MakeCredentialRequest { 107 | origin: "example.org".to_owned(), 108 | hash: Vec::from(challenge), 109 | relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"), 110 | user: Ctap2PublicKeyCredentialUserEntity::new(&user_id, "mario.rossi", "Mario Rossi"), 111 | require_resident_key: true, 112 | user_verification: UserVerificationRequirement::Preferred, 113 | algorithms: vec![Ctap2CredentialType::default()], 114 | exclude: None, 115 | extensions: Some(extensions.clone()), 116 | timeout: TIMEOUT, 117 | }; 118 | 119 | let response = loop { 120 | match channel 121 | .webauthn_make_credential(&make_credentials_request) 122 | .await 123 | { 124 | Ok(response) => break Ok(response), 125 | Err(WebAuthnError::Ctap(ctap_error)) => { 126 | if ctap_error.is_retryable_user_error() { 127 | println!("Oops, try again! Error: {}", ctap_error); 128 | continue; 129 | } 130 | break Err(WebAuthnError::Ctap(ctap_error)); 131 | } 132 | Err(err) => break Err(err), 133 | }; 134 | } 135 | .unwrap(); 136 | // println!("WebAuthn MakeCredential response: {:?}", response); 137 | println!( 138 | "WebAuthn MakeCredential extensions: {:?}", 139 | response.authenticator_data.extensions 140 | ); 141 | 142 | let credential: Ctap2PublicKeyCredentialDescriptor = 143 | (&response.authenticator_data).try_into().unwrap(); 144 | let get_assertion = GetAssertionRequest { 145 | relying_party_id: "example.org".to_owned(), 146 | hash: Vec::from(challenge), 147 | allow: vec![credential], 148 | user_verification: UserVerificationRequirement::Discouraged, 149 | extensions: Some(GetAssertionRequestExtensions { 150 | cred_blob: Some(true), 151 | hmac_or_prf: GetAssertionHmacOrPrfInput::HmacGetSecret(HMACGetSecretInput { 152 | salt1: [1; 32], 153 | salt2: None, 154 | }), 155 | ..Default::default() 156 | }), 157 | timeout: TIMEOUT, 158 | }; 159 | 160 | let response = loop { 161 | match channel.webauthn_get_assertion(&get_assertion).await { 162 | Ok(response) => break Ok(response), 163 | Err(WebAuthnError::Ctap(ctap_error)) => { 164 | if ctap_error.is_retryable_user_error() { 165 | println!("Oops, try again! Error: {}", ctap_error); 166 | continue; 167 | } 168 | break Err(WebAuthnError::Ctap(ctap_error)); 169 | } 170 | Err(err) => break Err(err), 171 | }; 172 | } 173 | .unwrap(); 174 | // println!("WebAuthn GetAssertion response: {:?}", response); 175 | println!( 176 | "WebAuthn GetAssertion extensions: {:?}", 177 | response.assertions[0].authenticator_data.extensions 178 | ); 179 | let blob = if let Some(ext) = &response.assertions[0].authenticator_data.extensions { 180 | ext.cred_blob 181 | .clone() 182 | .map(|x| String::from_utf8_lossy(&x).to_string()) 183 | } else { 184 | None 185 | }; 186 | println!("Credential blob: {blob:?}"); 187 | } 188 | 189 | Ok(()) 190 | } 191 | -------------------------------------------------------------------------------- /libwebauthn/examples/webauthn_hid.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | use std::error::Error; 3 | use std::io::{self, Write}; 4 | use std::time::Duration; 5 | 6 | use libwebauthn::UxUpdate; 7 | use rand::{thread_rng, Rng}; 8 | use text_io::read; 9 | use tokio::sync::mpsc::Receiver; 10 | use tracing_subscriber::{self, EnvFilter}; 11 | 12 | use libwebauthn::ops::webauthn::{ 13 | GetAssertionRequest, MakeCredentialRequest, UserVerificationRequirement, 14 | }; 15 | use libwebauthn::pin::PinRequestReason; 16 | use libwebauthn::proto::ctap2::{ 17 | Ctap2CredentialType, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity, 18 | Ctap2PublicKeyCredentialUserEntity, 19 | }; 20 | use libwebauthn::transport::hid::list_devices; 21 | use libwebauthn::transport::Device; 22 | use libwebauthn::webauthn::{Error as WebAuthnError, WebAuthn}; 23 | 24 | const TIMEOUT: Duration = Duration::from_secs(10); 25 | 26 | fn setup_logging() { 27 | tracing_subscriber::fmt() 28 | .with_env_filter(EnvFilter::from_default_env()) 29 | .without_time() 30 | .init(); 31 | } 32 | 33 | async fn handle_updates(mut state_recv: Receiver) { 34 | while let Some(update) = state_recv.recv().await { 35 | match update { 36 | UxUpdate::PresenceRequired => println!("Please touch your device!"), 37 | UxUpdate::UvRetry { attempts_left } => { 38 | print!("UV failed."); 39 | if let Some(attempts_left) = attempts_left { 40 | print!(" You have {attempts_left} attempts left."); 41 | } 42 | } 43 | UxUpdate::PinRequired(update) => { 44 | let mut attempts_str = String::new(); 45 | if let Some(attempts) = update.attempts_left { 46 | attempts_str = format!(". You have {attempts} attempts left!"); 47 | }; 48 | 49 | match update.reason { 50 | PinRequestReason::RelyingPartyRequest => println!("RP required a PIN."), 51 | PinRequestReason::AuthenticatorPolicy => { 52 | println!("Your device requires a PIN.") 53 | } 54 | PinRequestReason::FallbackFromUV => { 55 | println!("UV failed too often and is blocked. Falling back to PIN.") 56 | } 57 | } 58 | print!("PIN: Please enter the PIN for your authenticator{attempts_str}: "); 59 | io::stdout().flush().unwrap(); 60 | let pin_raw: String = read!("{}\n"); 61 | 62 | if pin_raw.is_empty() { 63 | println!("PIN: No PIN provided, cancelling operation."); 64 | update.cancel(); 65 | } else { 66 | let _ = update.send_pin(&pin_raw); 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | #[tokio::main] 74 | pub async fn main() -> Result<(), Box> { 75 | setup_logging(); 76 | 77 | let devices = list_devices().await.unwrap(); 78 | println!("Devices found: {:?}", devices); 79 | 80 | let user_id: [u8; 32] = thread_rng().gen(); 81 | let challenge: [u8; 32] = thread_rng().gen(); 82 | 83 | for mut device in devices { 84 | println!("Selected HID authenticator: {}", &device); 85 | let (mut channel, state_recv) = device.channel().await?; 86 | channel.wink(TIMEOUT).await?; 87 | 88 | // Make Credentials ceremony 89 | let make_credentials_request = MakeCredentialRequest { 90 | origin: "example.org".to_owned(), 91 | hash: Vec::from(challenge), 92 | relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"), 93 | user: Ctap2PublicKeyCredentialUserEntity::new(&user_id, "mario.rossi", "Mario Rossi"), 94 | require_resident_key: false, 95 | user_verification: UserVerificationRequirement::Preferred, 96 | algorithms: vec![Ctap2CredentialType::default()], 97 | exclude: None, 98 | extensions: None, 99 | timeout: TIMEOUT, 100 | }; 101 | 102 | tokio::spawn(handle_updates(state_recv)); 103 | 104 | let response = loop { 105 | match channel 106 | .webauthn_make_credential(&make_credentials_request) 107 | .await 108 | { 109 | Ok(response) => break Ok(response), 110 | Err(WebAuthnError::Ctap(ctap_error)) => { 111 | if ctap_error.is_retryable_user_error() { 112 | println!("Oops, try again! Error: {}", ctap_error); 113 | continue; 114 | } 115 | break Err(WebAuthnError::Ctap(ctap_error)); 116 | } 117 | Err(err) => break Err(err), 118 | }; 119 | } 120 | .unwrap(); 121 | println!("WebAuthn MakeCredential response: {:?}", response); 122 | 123 | let credential: Ctap2PublicKeyCredentialDescriptor = 124 | (&response.authenticator_data).try_into().unwrap(); 125 | let get_assertion = GetAssertionRequest { 126 | relying_party_id: "example.org".to_owned(), 127 | hash: Vec::from(challenge), 128 | allow: vec![credential], 129 | user_verification: UserVerificationRequirement::Discouraged, 130 | extensions: None, 131 | timeout: TIMEOUT, 132 | }; 133 | 134 | let response = loop { 135 | match channel.webauthn_get_assertion(&get_assertion).await { 136 | Ok(response) => break Ok(response), 137 | Err(WebAuthnError::Ctap(ctap_error)) => { 138 | if ctap_error.is_retryable_user_error() { 139 | println!("Oops, try again! Error: {}", ctap_error); 140 | continue; 141 | } 142 | break Err(WebAuthnError::Ctap(ctap_error)); 143 | } 144 | Err(err) => break Err(err), 145 | }; 146 | } 147 | .unwrap(); 148 | println!("WebAuthn GetAssertion response: {:?}", response); 149 | } 150 | 151 | Ok(()) 152 | } 153 | -------------------------------------------------------------------------------- /libwebauthn/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod fido; 2 | pub mod management; 3 | pub mod ops; 4 | pub mod pin; 5 | pub mod proto; 6 | pub mod transport; 7 | pub mod u2f; 8 | pub mod webauthn; 9 | use tokio::sync::oneshot; 10 | 11 | #[macro_use] 12 | extern crate num_derive; 13 | 14 | #[macro_use] 15 | extern crate bitflags; 16 | 17 | macro_rules! unwrap_field { 18 | ($field:expr) => {{ 19 | if let Some(f) = $field { 20 | f 21 | } else { 22 | tracing::error!( 23 | "Device response did not contain expected field: {}", 24 | stringify!($field) 25 | ); 26 | return Err(Error::Platform(PlatformError::InvalidDeviceResponse)); 27 | } 28 | }}; 29 | } 30 | use pin::PinRequestReason; 31 | pub(crate) use unwrap_field; 32 | 33 | #[derive(Debug)] 34 | pub enum Transport { 35 | Usb, 36 | Ble, 37 | } 38 | 39 | #[derive(Debug)] 40 | pub enum UxUpdate { 41 | /// UV failed, but we can still retry. `attempts_left` optionally shows how many tries _in total_ are left. 42 | /// Builtin UV may still temporarily be blocked. 43 | UvRetry { 44 | attempts_left: Option, 45 | }, 46 | /// The device requires a PIN. Use `send_pin()` method to answer the request. 47 | /// The ongoing operation may run into a timeout, no answer is provided in time. 48 | PinRequired(PinRequiredUpdate), 49 | PresenceRequired, 50 | } 51 | 52 | #[derive(Debug)] 53 | pub struct PinRequiredUpdate { 54 | reply_to: oneshot::Sender, 55 | /// What caused the PIN request. 56 | pub reason: PinRequestReason, 57 | /// Optionally, how many PIN attempts are left _in total_. 58 | pub attempts_left: Option, 59 | } 60 | 61 | impl PinRequiredUpdate { 62 | /// This consumes `self`, because we should only ever send exactly one answer back. 63 | pub fn send_pin(self, pin: &str) -> Result<(), String> { 64 | self.reply_to.send(pin.to_string()) 65 | } 66 | 67 | /// The user cancels the PIN entry, without making an attempt. 68 | pub fn cancel(self) { 69 | // We hang up to signal an abort 70 | drop(self.reply_to) 71 | } 72 | } 73 | 74 | pub fn available_transports() -> Vec { 75 | vec![Transport::Usb, Transport::Ble] 76 | } 77 | -------------------------------------------------------------------------------- /libwebauthn/src/management.rs: -------------------------------------------------------------------------------- 1 | mod bio_enrollment; 2 | pub use bio_enrollment::BioEnrollment; 3 | 4 | mod authenticator_config; 5 | pub use authenticator_config::AuthenticatorConfig; 6 | 7 | mod credential_management; 8 | pub use credential_management::CredentialManagement; 9 | -------------------------------------------------------------------------------- /libwebauthn/src/management/authenticator_config.rs: -------------------------------------------------------------------------------- 1 | use crate::proto::ctap2::Ctap2ClientPinRequest; 2 | pub use crate::transport::error::{CtapError, Error}; 3 | use crate::transport::Channel; 4 | use crate::webauthn::handle_errors; 5 | use crate::webauthn::{user_verification, UsedPinUvAuthToken}; 6 | use crate::{ 7 | ops::webauthn::UserVerificationRequirement, 8 | pin::PinUvAuthProtocol, 9 | proto::ctap2::{ 10 | Ctap2, Ctap2AuthTokenPermissionRole, Ctap2AuthenticatorConfigCommand, 11 | Ctap2AuthenticatorConfigRequest, Ctap2GetInfoResponse, Ctap2UserVerifiableRequest, 12 | }, 13 | UxUpdate, 14 | }; 15 | use async_trait::async_trait; 16 | use serde_bytes::ByteBuf; 17 | use serde_cbor::ser::to_vec; 18 | use std::time::Duration; 19 | use tracing::info; 20 | 21 | #[async_trait] 22 | pub trait AuthenticatorConfig { 23 | async fn toggle_always_uv(&mut self, timeout: Duration) -> Result<(), Error>; 24 | 25 | async fn enable_enterprise_attestation(&mut self, timeout: Duration) -> Result<(), Error>; 26 | 27 | async fn set_min_pin_length( 28 | &mut self, 29 | new_pin_length: u64, 30 | timeout: Duration, 31 | ) -> Result<(), Error>; 32 | 33 | async fn force_change_pin(&mut self, force: bool, timeout: Duration) -> Result<(), Error>; 34 | 35 | async fn set_min_pin_length_rpids( 36 | &mut self, 37 | rpids: Vec, 38 | timeout: Duration, 39 | ) -> Result<(), Error>; 40 | } 41 | 42 | #[async_trait] 43 | impl AuthenticatorConfig for C 44 | where 45 | C: Channel, 46 | { 47 | async fn toggle_always_uv(&mut self, timeout: Duration) -> Result<(), Error> { 48 | let mut req = Ctap2AuthenticatorConfigRequest::new_toggle_always_uv(); 49 | 50 | loop { 51 | let uv_auth_used = user_verification( 52 | self, 53 | UserVerificationRequirement::Required, 54 | &mut req, 55 | timeout, 56 | ) 57 | .await?; 58 | // On success, this is an all-empty Ctap2AuthenticatorConfigResponse 59 | handle_errors!( 60 | self, 61 | self.ctap2_authenticator_config(&req, timeout).await, 62 | uv_auth_used, 63 | timeout 64 | ) 65 | } 66 | } 67 | 68 | async fn enable_enterprise_attestation(&mut self, timeout: Duration) -> Result<(), Error> { 69 | let mut req = Ctap2AuthenticatorConfigRequest::new_enable_enterprise_attestation(); 70 | 71 | loop { 72 | let uv_auth_used = user_verification( 73 | self, 74 | UserVerificationRequirement::Required, 75 | &mut req, 76 | timeout, 77 | ) 78 | .await?; 79 | // On success, this is an all-empty Ctap2AuthenticatorConfigResponse 80 | handle_errors!( 81 | self, 82 | self.ctap2_authenticator_config(&req, timeout).await, 83 | uv_auth_used, 84 | timeout 85 | ) 86 | } 87 | } 88 | 89 | async fn set_min_pin_length( 90 | &mut self, 91 | new_pin_length: u64, 92 | timeout: Duration, 93 | ) -> Result<(), Error> { 94 | let mut req = Ctap2AuthenticatorConfigRequest::new_set_min_pin_length(new_pin_length); 95 | 96 | loop { 97 | let uv_auth_used = user_verification( 98 | self, 99 | UserVerificationRequirement::Required, 100 | &mut req, 101 | timeout, 102 | ) 103 | .await?; 104 | // On success, this is an all-empty Ctap2AuthenticatorConfigResponse 105 | handle_errors!( 106 | self, 107 | self.ctap2_authenticator_config(&req, timeout).await, 108 | uv_auth_used, 109 | timeout 110 | ) 111 | } 112 | } 113 | 114 | async fn force_change_pin(&mut self, force: bool, timeout: Duration) -> Result<(), Error> { 115 | let mut req = Ctap2AuthenticatorConfigRequest::new_force_change_pin(force); 116 | 117 | loop { 118 | let uv_auth_used = user_verification( 119 | self, 120 | UserVerificationRequirement::Required, 121 | &mut req, 122 | timeout, 123 | ) 124 | .await?; 125 | // On success, this is an all-empty Ctap2AuthenticatorConfigResponse 126 | handle_errors!( 127 | self, 128 | self.ctap2_authenticator_config(&req, timeout).await, 129 | uv_auth_used, 130 | timeout 131 | ) 132 | } 133 | } 134 | 135 | async fn set_min_pin_length_rpids( 136 | &mut self, 137 | rpids: Vec, 138 | timeout: Duration, 139 | ) -> Result<(), Error> { 140 | let mut req = Ctap2AuthenticatorConfigRequest::new_set_min_pin_length_rpids(rpids); 141 | loop { 142 | let uv_auth_used = user_verification( 143 | self, 144 | UserVerificationRequirement::Required, 145 | &mut req, 146 | timeout, 147 | ) 148 | .await?; 149 | // On success, this is an all-empty Ctap2AuthenticatorConfigResponse 150 | handle_errors!( 151 | self, 152 | self.ctap2_authenticator_config(&req, timeout).await, 153 | uv_auth_used, 154 | timeout 155 | ) 156 | } 157 | } 158 | } 159 | 160 | impl Ctap2UserVerifiableRequest for Ctap2AuthenticatorConfigRequest { 161 | fn ensure_uv_set(&mut self) { 162 | // No-op 163 | } 164 | 165 | fn calculate_and_set_uv_auth( 166 | &mut self, 167 | uv_proto: &Box, 168 | uv_auth_token: &[u8], 169 | ) { 170 | // pinUvAuthParam (0x04): the result of calling 171 | // authenticate(pinUvAuthToken, 32×0xff || 0x0d || uint8(subCommand) || subCommandParams). 172 | let mut data = vec![0xff; 32]; 173 | data.push(0x0D); 174 | data.push(self.subcommand as u8); 175 | if self.subcommand == Ctap2AuthenticatorConfigCommand::SetMinPINLength { 176 | data.extend(to_vec(&self.subcommand_params).unwrap()); 177 | } 178 | let uv_auth_param = uv_proto.authenticate(uv_auth_token, &data); 179 | self.protocol = Some(uv_proto.version()); 180 | self.uv_auth_param = Some(ByteBuf::from(uv_auth_param)); 181 | } 182 | 183 | fn client_data_hash(&self) -> &[u8] { 184 | unreachable!() 185 | } 186 | 187 | fn permissions(&self) -> Ctap2AuthTokenPermissionRole { 188 | return Ctap2AuthTokenPermissionRole::AUTHENTICATOR_CONFIGURATION; 189 | } 190 | 191 | fn permissions_rpid(&self) -> Option<&str> { 192 | None 193 | } 194 | 195 | fn can_use_uv(&self, info: &Ctap2GetInfoResponse) -> bool { 196 | info.option_enabled("uvAcfg") 197 | } 198 | 199 | fn handle_legacy_preview(&mut self, _info: &Ctap2GetInfoResponse) { 200 | // No-op 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /libwebauthn/src/ops/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod u2f; 2 | pub mod webauthn; 3 | -------------------------------------------------------------------------------- /libwebauthn/src/proto/ctap1/apdu/mod.rs: -------------------------------------------------------------------------------- 1 | mod request; 2 | mod response; 3 | 4 | pub use request::ApduRequest; 5 | pub use response::{ApduResponse, ApduResponseStatus}; 6 | -------------------------------------------------------------------------------- /libwebauthn/src/proto/ctap1/apdu/request.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Error as IOError, ErrorKind as IOErrorKind}; 2 | 3 | use byteorder::{BigEndian, WriteBytesExt}; 4 | 5 | use crate::proto::ctap1::model::Ctap1VersionRequest; 6 | use crate::proto::ctap1::{Ctap1RegisterRequest, Ctap1SignRequest}; 7 | 8 | const APDU_SHORT_MAX_DATA: usize = 0x100; 9 | const APDU_SHORT_MAX_LE: usize = 0x100; 10 | const APDU_SHORT_LE: usize = APDU_SHORT_MAX_LE; 11 | 12 | const APDI_LONG_MAX_DATA: usize = 0xFF_FF_FF; 13 | 14 | const U2F_REGISTER: u8 = 0x01; 15 | const U2F_AUTHENTICATE: u8 = 0x02; 16 | const U2F_VERSION: u8 = 0x03; 17 | 18 | const _CONTROL_BYTE_CHECK_ONLY: u8 = 0x07; 19 | const CONTROL_BYTE_ENFORCE_UP_AND_SIGN: u8 = 0x03; 20 | const CONTROL_BYTE_DONT_ENFORCE_UP_AND_SIGN: u8 = 0x08; 21 | 22 | #[derive(Debug)] 23 | pub struct ApduRequest { 24 | ins: u8, 25 | p1: u8, 26 | p2: u8, 27 | data: Option>, 28 | response_max_length: Option, 29 | } 30 | 31 | impl ApduRequest { 32 | pub fn new( 33 | ins: u8, 34 | p1: u8, 35 | p2: u8, 36 | data: Option<&[u8]>, 37 | response_max_length: Option, 38 | ) -> Self { 39 | Self { 40 | ins, 41 | p1, 42 | p2, 43 | data: if let Some(bytes) = data { 44 | Some(Vec::from(bytes)) 45 | } else { 46 | None 47 | }, 48 | response_max_length, 49 | } 50 | } 51 | 52 | pub fn raw_short(&self) -> Result, IOError> { 53 | let mut raw: Vec = Vec::new(); 54 | raw.push(0x00); // CLA 55 | raw.push(self.ins); 56 | raw.push(self.p1); 57 | raw.push(self.p2); 58 | 59 | if let Some(data) = &self.data { 60 | if data.len() > APDU_SHORT_MAX_DATA { 61 | return Err(IOError::new( 62 | IOErrorKind::InvalidData, 63 | format!( 64 | "Unable to serialize {} bytes of data in APDU short form.", 65 | data.len() 66 | ), 67 | )); 68 | } else if data.len() == 0 { 69 | return Err(IOError::new( 70 | IOErrorKind::InvalidData, 71 | "Cannot serialize an empty payload.", 72 | )); 73 | }; 74 | 75 | raw.push(if data.len() != APDU_SHORT_MAX_DATA { 76 | data.len() as u8 77 | } else { 78 | 0 79 | }); 80 | raw.extend(data); 81 | } 82 | 83 | if let Some(le) = self.response_max_length { 84 | if le > APDU_SHORT_MAX_LE { 85 | return Err(IOError::new( 86 | IOErrorKind::InvalidData, 87 | format!("Unable to serialize L_e value ({}) in APDU short form.", le), 88 | )); 89 | } 90 | 91 | raw.push(if le == APDU_SHORT_MAX_LE { 0 } else { le as u8 }); 92 | } 93 | Ok(raw) 94 | } 95 | 96 | pub fn raw_long(&self) -> Result, IOError> { 97 | let mut raw: Vec = Vec::new(); 98 | raw.push(0x00); // CLA 99 | raw.push(self.ins); 100 | raw.push(self.p1); 101 | raw.push(self.p2); 102 | 103 | if let Some(data) = &self.data { 104 | if data.len() > APDI_LONG_MAX_DATA { 105 | return Err(IOError::new( 106 | IOErrorKind::InvalidData, 107 | format!( 108 | "Unable to serialize {} bytes of data in APDU long form.", 109 | data.len() 110 | ), 111 | )); 112 | } 113 | raw.write_u24::(data.len() as u32)?; 114 | raw.extend(data); 115 | } else { 116 | raw.write_u24::(0)?; 117 | } 118 | 119 | Ok(raw) 120 | } 121 | } 122 | 123 | impl From<&Ctap1RegisterRequest> for ApduRequest { 124 | fn from(request: &Ctap1RegisterRequest) -> Self { 125 | let mut data = request.challenge.clone(); 126 | data.extend(&request.app_id_hash); 127 | Self::new( 128 | U2F_REGISTER, 129 | CONTROL_BYTE_ENFORCE_UP_AND_SIGN, 130 | 0x00, 131 | Some(&data), 132 | Some(APDU_SHORT_LE), 133 | ) 134 | } 135 | } 136 | 137 | impl From<&Ctap1VersionRequest> for ApduRequest { 138 | fn from(_: &Ctap1VersionRequest) -> Self { 139 | Self::new(U2F_VERSION, 0x00, 0x00, None, Some(APDU_SHORT_LE)) 140 | } 141 | } 142 | 143 | impl From<&Ctap1SignRequest> for ApduRequest { 144 | fn from(request: &Ctap1SignRequest) -> Self { 145 | let p1 = if request.require_user_presence { 146 | CONTROL_BYTE_ENFORCE_UP_AND_SIGN 147 | } else { 148 | CONTROL_BYTE_DONT_ENFORCE_UP_AND_SIGN 149 | }; 150 | let mut data = request.challenge.clone(); 151 | data.extend(&request.app_id_hash); 152 | data.write_u8(request.key_handle.len() as u8).unwrap(); 153 | data.extend(&request.key_handle); 154 | Self::new(U2F_AUTHENTICATE, p1, 0x00, Some(&data), Some(APDU_SHORT_LE)) 155 | } 156 | } 157 | 158 | #[cfg(test)] 159 | mod tests { 160 | use crate::proto::ctap1::apdu::ApduRequest; 161 | 162 | #[test] 163 | fn apdu_raw_short_no_data() { 164 | let apdu = ApduRequest::new(0x01, 0x02, 0x03, None, None); 165 | assert_eq!(apdu.raw_short().unwrap(), [0x00, 0x01, 0x02, 0x03]); 166 | } 167 | 168 | #[test] 169 | fn apdu_raw_short_no_data_le() { 170 | let apdu = ApduRequest::new(0x01, 0x02, 0x03, None, Some(0x42)); 171 | assert_eq!(apdu.raw_short().unwrap(), [0x00, 0x01, 0x02, 0x03, 0x42]); 172 | } 173 | 174 | #[test] 175 | fn apdu_raw_short_with_data() { 176 | let data = &[0xAA, 0xBB, 0xCC]; 177 | let apdu = ApduRequest::new(0x03, 0x02, 0x01, Some(data), None); 178 | assert_eq!( 179 | apdu.raw_short().unwrap(), 180 | [0x00, 0x03, 0x02, 0x01, 0x03, 0xAA, 0xBB, 0xCC] 181 | ); 182 | } 183 | 184 | #[test] 185 | fn apdu_raw_short_with_data_le() { 186 | let data = &[0xAA, 0xBB, 0xCC]; 187 | let apdu = ApduRequest::new(0x03, 0x02, 0x01, Some(data), Some(0x42)); 188 | assert_eq!( 189 | apdu.raw_short().unwrap(), 190 | [0x00, 0x03, 0x02, 0x01, 0x03, 0xAA, 0xBB, 0xCC, 0x42] 191 | ); 192 | } 193 | 194 | #[test] 195 | fn apdu_raw_short_with_max_len_data() { 196 | let data: Vec = vec![0xF1; 256]; 197 | let apdu = ApduRequest::new(0x0A, 0x0B, 0x0C, Some(&data), None); 198 | let serialized = apdu.raw_short().unwrap(); 199 | assert_eq!(&serialized[0..5], &[0x00, 0x0A, 0x0B, 0x0C, 0x00]); 200 | assert_eq!(&serialized[5..261], data.as_slice()); 201 | } 202 | 203 | #[test] 204 | fn apdu_raw_long_no_data() { 205 | let apdu = ApduRequest::new(0x01, 0x02, 0x03, None, None); 206 | assert_eq!( 207 | apdu.raw_long().unwrap(), 208 | [0x00, 0x01, 0x02, 0x03, 0x00, 0x00, 0x00], 209 | ); 210 | } 211 | 212 | #[test] 213 | fn apdu_raw_long_with_data() { 214 | let data: Vec = vec![0xF1; 512]; 215 | let apdu = ApduRequest::new(0x01, 0x02, 0x03, Some(&data), None); 216 | let serialized = apdu.raw_long().unwrap(); 217 | assert_eq!( 218 | &serialized[0..7], 219 | &[0x00, 0x01, 0x02, 0x03, 0x00, 0x02, 0x00], 220 | ); 221 | assert_eq!(&serialized[7..519], data.as_slice()); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /libwebauthn/src/proto/ctap1/apdu/response.rs: -------------------------------------------------------------------------------- 1 | use std::convert::{TryFrom, TryInto}; 2 | use std::io::{Cursor as IOCursor, Error as IOError, ErrorKind as IOErrorKind}; 3 | 4 | use byteorder::{BigEndian, ReadBytesExt}; 5 | use num_enum::{IntoPrimitive, TryFromPrimitive}; 6 | 7 | #[derive(Debug, PartialEq)] 8 | pub struct ApduResponse { 9 | pub data: Option>, 10 | sw1: u8, 11 | sw2: u8, 12 | } 13 | 14 | #[derive(Debug, IntoPrimitive, TryFromPrimitive, Copy, Clone, PartialEq)] 15 | #[repr(u16)] 16 | pub enum ApduResponseStatus { 17 | NoError = 0x9000, 18 | UserPresenceTestFailed = 0x6985, 19 | InvalidKeyHandle = 0x6A80, 20 | InvalidRequestLength = 0x6700, 21 | InvalidClassByte = 0x6E00, 22 | InvalidInstruction = 0x6D00, 23 | } 24 | 25 | impl ApduResponse { 26 | pub fn new_success(data: &[u8]) -> Self { 27 | Self { 28 | data: Some(Vec::from(data)), 29 | sw1: 0x90, 30 | sw2: 0x00, 31 | } 32 | } 33 | 34 | pub fn status(&self) -> Result { 35 | let mut cursor = IOCursor::new(vec![self.sw1, self.sw2]); 36 | let code = cursor.read_u16::().unwrap() as u16; 37 | 38 | code.try_into().or(Err(IOError::new( 39 | IOErrorKind::InvalidData, 40 | format!("Unknown APDU response code returned: {:x}", code), 41 | ))) 42 | } 43 | } 44 | 45 | impl TryFrom<&Vec> for ApduResponse { 46 | type Error = IOError; 47 | fn try_from(packet: &Vec) -> Result { 48 | if packet.len() < 2 { 49 | return Err(IOError::new( 50 | IOErrorKind::InvalidData, 51 | "Apdu response packets must contain at least 2 bytes.", 52 | )); 53 | } 54 | 55 | let data = if packet.len() > 2 { 56 | Some(Vec::from(&packet[0..packet.len() - 2])) 57 | } else { 58 | None 59 | }; 60 | let (sw1, sw2) = (packet[packet.len() - 2], packet[packet.len() - 1]); 61 | 62 | Ok(Self { data, sw1, sw2 }) 63 | } 64 | } 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use crate::proto::ctap1::apdu::response::ApduResponseStatus; 69 | use crate::proto::ctap1::apdu::ApduResponse; 70 | use std::convert::TryInto; 71 | use std::io::{Error as IOError, ErrorKind as IOErrorKind}; 72 | 73 | #[test] 74 | fn apdu_from_status_only_packet() { 75 | let packet: &Vec = &vec![0x69, 0x85]; 76 | let apdu: ApduResponse = packet.try_into().unwrap(); 77 | assert_eq!( 78 | apdu.status().unwrap(), 79 | ApduResponseStatus::UserPresenceTestFailed 80 | ); 81 | assert_eq!(apdu.data, None); 82 | } 83 | 84 | #[test] 85 | fn apdu_from_full_packet() { 86 | let packet: &Vec = &vec![0x01, 0x02, 0x03, 0x90, 0x00]; 87 | let apdu: ApduResponse = packet.try_into().unwrap(); 88 | assert_eq!(apdu.status().unwrap(), ApduResponseStatus::NoError); 89 | assert_eq!(apdu.data, Some(vec![0x01, 0x02, 0x03])); 90 | } 91 | 92 | #[test] 93 | fn apdu_from_invalid_packet() { 94 | let packet: &Vec = &vec![0xB0]; 95 | let apdu: Result = 96 | packet.try_into().map_err(|ioe: IOError| ioe.kind()); 97 | assert_eq!(apdu, Err(IOErrorKind::InvalidData)); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /libwebauthn/src/proto/ctap1/mod.rs: -------------------------------------------------------------------------------- 1 | mod model; 2 | mod protocol; 3 | 4 | pub mod apdu; 5 | 6 | pub use self::model::Ctap1RegisteredKey; 7 | pub use self::model::Ctap1Transport; 8 | pub use self::model::Ctap1Version; 9 | pub use self::model::{Ctap1RegisterRequest, Ctap1RegisterResponse}; 10 | pub use self::model::{Ctap1SignRequest, Ctap1SignResponse}; 11 | pub use self::model::{Ctap1VersionRequest, Ctap1VersionResponse}; 12 | 13 | pub use self::protocol::Ctap1; 14 | -------------------------------------------------------------------------------- /libwebauthn/src/proto/ctap1/protocol.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | use std::time::Duration; 3 | 4 | use async_trait::async_trait; 5 | use tokio::time::{sleep, timeout as tokio_timeout}; 6 | use tracing::{debug, error, info, instrument, span, trace, warn, Level}; 7 | 8 | use super::apdu::{ApduRequest, ApduResponse, ApduResponseStatus}; 9 | use super::{ 10 | Ctap1RegisterRequest, Ctap1RegisterResponse, Ctap1SignRequest, Ctap1SignResponse, 11 | Ctap1VersionRequest, Ctap1VersionResponse, 12 | }; 13 | use crate::proto::ctap1::model::Preflight; 14 | use crate::proto::CtapError; 15 | use crate::transport::error::{Error, TransportError}; 16 | use crate::transport::Channel; 17 | 18 | const UP_SLEEP: Duration = Duration::from_millis(150); 19 | const VERSION_TIMEOUT: Duration = Duration::from_millis(500); 20 | 21 | #[async_trait] 22 | pub trait Ctap1 { 23 | async fn ctap1_version(&mut self) -> Result; 24 | async fn ctap1_register( 25 | &mut self, 26 | op: &Ctap1RegisterRequest, 27 | ) -> Result; 28 | async fn ctap1_sign(&mut self, op: &Ctap1SignRequest) -> Result; 29 | } 30 | 31 | #[async_trait] 32 | impl Ctap1 for C 33 | where 34 | C: Channel, 35 | { 36 | #[instrument(skip_all)] 37 | async fn ctap1_version(&mut self) -> Result { 38 | let request = &Ctap1VersionRequest::new(); 39 | let apdu_request: ApduRequest = request.into(); 40 | self.apdu_send(&apdu_request, VERSION_TIMEOUT).await?; 41 | let apdu_response = self.apdu_recv(VERSION_TIMEOUT).await?; 42 | let response: Ctap1VersionResponse = apdu_response.try_into().or(Err(CtapError::Other))?; 43 | debug!({ ?response.version }, "CTAP1 version response"); 44 | Ok(response) 45 | } 46 | 47 | #[instrument(skip_all)] 48 | async fn ctap1_register( 49 | &mut self, 50 | request: &Ctap1RegisterRequest, 51 | ) -> Result { 52 | debug!({ %request.require_user_presence }, "CTAP1 register request"); 53 | trace!(?request); 54 | 55 | let (request, preflight_requests) = request.preflight()?; 56 | debug!({ count = preflight_requests.len() }, "Preflight requests"); 57 | for preflight in preflight_requests.iter() { 58 | let span = span!(Level::DEBUG, "preflight"); 59 | let _enter = span.enter(); 60 | match self.ctap1_sign(preflight).await { 61 | Ok(_) => { 62 | info!("Already-registered credential found during preflight request."); 63 | return Err(Error::Ctap(CtapError::CredentialExcluded)); 64 | } 65 | Err(Error::Ctap(CtapError::NoCredentials)) => { 66 | debug!("Credential doesn't already exist, continuing."); 67 | } 68 | Err(err) => { 69 | warn!(?err, "Preflight request failed with unexpected error."); 70 | } 71 | }; 72 | } 73 | 74 | let apdu_request: ApduRequest = (&request).into(); 75 | let apdu_response = send_apdu_request_wait_uv(self, &apdu_request, request.timeout).await?; 76 | let status = apdu_response.status().or(Err(CtapError::Other))?; 77 | if status != ApduResponseStatus::NoError { 78 | error!(?status, "APDU response has error code"); 79 | return Err(Error::Ctap(CtapError::from(status))); 80 | } 81 | 82 | let response: Ctap1RegisterResponse = apdu_response.try_into().unwrap(); 83 | debug!("CTAP1 register response"); 84 | trace!(?response); 85 | Ok(response) 86 | } 87 | 88 | #[instrument(skip_all, fields(preflight = !request.require_user_presence))] 89 | async fn ctap1_sign(&mut self, request: &Ctap1SignRequest) -> Result { 90 | debug!({ %request.require_user_presence }, "CTAP1 sign request"); 91 | trace!(?request); 92 | 93 | let apdu_request: ApduRequest = request.into(); 94 | let apdu_response = send_apdu_request_wait_uv(self, &apdu_request, request.timeout).await?; 95 | let status = apdu_response.status().or(Err(CtapError::Other))?; 96 | if status != ApduResponseStatus::NoError { 97 | error!(?status, "APDU response has error code"); 98 | return Err(Error::Ctap(CtapError::from(status))); 99 | } 100 | 101 | let response: Ctap1SignResponse = apdu_response.try_into().unwrap(); 102 | debug!({ ?response.user_presence_verified }, "CTAP1 sign response received"); 103 | trace!(?response); 104 | Ok(response) 105 | } 106 | } 107 | 108 | async fn send_apdu_request_wait_uv<'c, C: Channel>( 109 | channel: &'c mut C, 110 | request: &ApduRequest, 111 | timeout: Duration, 112 | ) -> Result { 113 | tokio_timeout(timeout, async { 114 | loop { 115 | channel.apdu_send(request, timeout).await?; 116 | let apdu_response = channel.apdu_recv(timeout).await?; 117 | let apdu_status = apdu_response 118 | .status() 119 | .or(Err(Error::Transport(TransportError::InvalidFraming)))?; 120 | let ctap_error: CtapError = apdu_status.into(); 121 | match ctap_error { 122 | CtapError::Ok => return Ok(apdu_response), 123 | CtapError::UserPresenceRequired => (), // Sleep some more. 124 | _ => return Err(Error::Ctap(ctap_error)), 125 | }; 126 | debug!("UP required. Sleeping for {:?}.", UP_SLEEP); 127 | sleep(UP_SLEEP).await; 128 | } 129 | }) 130 | .await 131 | .or(Err(Error::Ctap(CtapError::UserActionTimeout)))? 132 | } 133 | -------------------------------------------------------------------------------- /libwebauthn/src/proto/ctap2/cbor/mod.rs: -------------------------------------------------------------------------------- 1 | mod request; 2 | mod response; 3 | 4 | pub use request::CborRequest; 5 | pub use response::CborResponse; 6 | -------------------------------------------------------------------------------- /libwebauthn/src/proto/ctap2/cbor/request.rs: -------------------------------------------------------------------------------- 1 | extern crate serde_cbor; 2 | 3 | use serde_cbor::ser::to_vec; 4 | 5 | use std::io::Error as IOError; 6 | 7 | use crate::proto::ctap2::model::Ctap2ClientPinRequest; 8 | use crate::proto::ctap2::model::Ctap2CommandCode; 9 | use crate::proto::ctap2::model::Ctap2GetAssertionRequest; 10 | use crate::proto::ctap2::model::Ctap2MakeCredentialRequest; 11 | use crate::proto::ctap2::Ctap2AuthenticatorConfigRequest; 12 | use crate::proto::ctap2::Ctap2BioEnrollmentRequest; 13 | use crate::proto::ctap2::Ctap2CredentialManagementRequest; 14 | 15 | #[derive(Debug, Clone)] 16 | pub struct CborRequest { 17 | pub command: Ctap2CommandCode, 18 | pub encoded_data: Vec, 19 | } 20 | 21 | impl CborRequest { 22 | pub fn new(command: Ctap2CommandCode) -> Self { 23 | Self { 24 | command: command, 25 | encoded_data: vec![], 26 | } 27 | } 28 | 29 | pub fn ctap_hid_data(&self) -> Vec { 30 | let mut data = vec![self.command as u8]; 31 | data.extend(&self.encoded_data); 32 | data 33 | } 34 | 35 | pub fn raw_long(&self) -> Result, IOError> { 36 | let mut data = vec![self.command as u8]; 37 | data.extend(self.encoded_data.iter().copied()); 38 | Ok(data) 39 | } 40 | } 41 | 42 | impl From<&Ctap2MakeCredentialRequest> for CborRequest { 43 | fn from(request: &Ctap2MakeCredentialRequest) -> CborRequest { 44 | CborRequest { 45 | command: Ctap2CommandCode::AuthenticatorMakeCredential, 46 | encoded_data: to_vec(request).unwrap(), 47 | } 48 | } 49 | } 50 | 51 | impl From<&Ctap2GetAssertionRequest> for CborRequest { 52 | fn from(request: &Ctap2GetAssertionRequest) -> CborRequest { 53 | CborRequest { 54 | command: Ctap2CommandCode::AuthenticatorGetAssertion, 55 | encoded_data: to_vec(request).unwrap(), 56 | } 57 | } 58 | } 59 | 60 | impl From<&Ctap2ClientPinRequest> for CborRequest { 61 | fn from(request: &Ctap2ClientPinRequest) -> CborRequest { 62 | CborRequest { 63 | command: Ctap2CommandCode::AuthenticatorClientPin, 64 | encoded_data: to_vec(request).unwrap(), 65 | } 66 | } 67 | } 68 | 69 | impl From<&Ctap2AuthenticatorConfigRequest> for CborRequest { 70 | fn from(request: &Ctap2AuthenticatorConfigRequest) -> CborRequest { 71 | CborRequest { 72 | command: Ctap2CommandCode::AuthenticatorConfig, 73 | encoded_data: to_vec(request).unwrap(), 74 | } 75 | } 76 | } 77 | 78 | impl From<&Ctap2BioEnrollmentRequest> for CborRequest { 79 | fn from(request: &Ctap2BioEnrollmentRequest) -> CborRequest { 80 | let command = if request.use_legacy_preview { 81 | Ctap2CommandCode::AuthenticatorBioEnrollmentPreview 82 | } else { 83 | Ctap2CommandCode::AuthenticatorBioEnrollment 84 | }; 85 | CborRequest { 86 | command, 87 | encoded_data: to_vec(request).unwrap(), 88 | } 89 | } 90 | } 91 | 92 | impl From<&Ctap2CredentialManagementRequest> for CborRequest { 93 | fn from(request: &Ctap2CredentialManagementRequest) -> CborRequest { 94 | let command = if request.use_legacy_preview { 95 | Ctap2CommandCode::AuthenticatorCredentialManagementPreview 96 | } else { 97 | Ctap2CommandCode::AuthenticatorCredentialManagement 98 | }; 99 | CborRequest { 100 | command, 101 | encoded_data: to_vec(request).unwrap(), 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /libwebauthn/src/proto/ctap2/cbor/response.rs: -------------------------------------------------------------------------------- 1 | use crate::proto::error::CtapError; 2 | 3 | use std::convert::{TryFrom, TryInto}; 4 | use std::io::{Error as IOError, ErrorKind as IOErrorKind}; 5 | use tracing::error; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct CborResponse { 9 | pub status_code: CtapError, 10 | pub data: Option>, 11 | } 12 | 13 | impl CborResponse { 14 | pub fn new_success_from_slice(slice: &[u8]) -> Self { 15 | Self { 16 | status_code: CtapError::Ok, 17 | data: match slice.len() { 18 | 0 => None, 19 | _ => Some(Vec::from(slice)), 20 | }, 21 | } 22 | } 23 | } 24 | 25 | impl TryFrom<&Vec> for CborResponse { 26 | type Error = IOError; 27 | fn try_from(packet: &Vec) -> Result { 28 | if packet.len() < 1 { 29 | return Err(IOError::new( 30 | IOErrorKind::InvalidData, 31 | "Cbor response packets must contain at least 1 byte.", 32 | )); 33 | } 34 | 35 | let Ok(status_code) = packet[0].try_into() else { 36 | error!({ code = ?packet[0] }, "Invalid CTAP error code"); 37 | return Err(IOError::new( 38 | IOErrorKind::InvalidData, 39 | format!("Invalid CTAP error code: {:x}", packet[0]), 40 | )); 41 | }; 42 | 43 | let data = if packet.len() > 1 { 44 | Some(Vec::from(&packet[1..])) 45 | } else { 46 | None 47 | }; 48 | Ok(CborResponse { status_code, data }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /libwebauthn/src/proto/ctap2/mod.rs: -------------------------------------------------------------------------------- 1 | extern crate async_trait; 2 | 3 | pub mod cbor; 4 | 5 | mod model; 6 | mod protocol; 7 | 8 | pub use model::Ctap2GetInfoResponse; 9 | pub use model::{ 10 | Ctap2AttestationStatement, Ctap2AuthTokenPermissionRole, Ctap2COSEAlgorithmIdentifier, 11 | Ctap2ClientPinRequest, Ctap2CommandCode, Ctap2CredentialType, Ctap2MakeCredentialOptions, 12 | Ctap2PinUvAuthProtocol, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity, 13 | Ctap2PublicKeyCredentialType, Ctap2PublicKeyCredentialUserEntity, Ctap2Transport, 14 | Ctap2UserVerifiableRequest, Ctap2UserVerificationOperation, FidoU2fAttestationStmt, 15 | }; 16 | pub use model::{ 17 | Ctap2AuthenticatorConfigCommand, Ctap2AuthenticatorConfigParams, 18 | Ctap2AuthenticatorConfigRequest, 19 | }; 20 | pub use model::{ 21 | Ctap2BioEnrollmentFingerprintKind, Ctap2BioEnrollmentModality, Ctap2BioEnrollmentRequest, 22 | Ctap2BioEnrollmentResponse, Ctap2BioEnrollmentTemplateId, Ctap2LastEnrollmentSampleStatus, 23 | }; 24 | pub use model::{ 25 | Ctap2CredentialData, Ctap2CredentialManagementMetadata, Ctap2CredentialManagementRequest, 26 | Ctap2CredentialManagementResponse, Ctap2RPData, 27 | }; 28 | pub use model::{ 29 | Ctap2GetAssertionRequest, Ctap2GetAssertionResponse, Ctap2GetAssertionResponseExtensions, 30 | }; 31 | pub use model::{ 32 | Ctap2MakeCredentialRequest, Ctap2MakeCredentialResponse, Ctap2MakeCredentialsResponseExtensions, 33 | }; 34 | pub mod preflight; 35 | pub use protocol::Ctap2; 36 | -------------------------------------------------------------------------------- /libwebauthn/src/proto/ctap2/model/authenticator_config.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | use serde_bytes::ByteBuf; 3 | use serde_indexed::SerializeIndexed; 4 | use serde_repr::{Deserialize_repr, Serialize_repr}; 5 | 6 | use super::Ctap2PinUvAuthProtocol; 7 | 8 | #[derive(Debug, Clone, SerializeIndexed)] 9 | #[serde_indexed(offset = 1)] 10 | pub struct Ctap2AuthenticatorConfigRequest { 11 | // subCommand (0x01) 12 | pub subcommand: Ctap2AuthenticatorConfigCommand, 13 | 14 | // subCommandParams (0x02) 15 | #[serde(skip_serializing_if = "Option::is_none")] 16 | pub subcommand_params: Option, 17 | 18 | ///pinUvAuthProtocol (0x03) 19 | #[serde(skip_serializing_if = "Option::is_none")] 20 | pub protocol: Option, 21 | 22 | /// pinUvAuthParam (0x04): 23 | #[serde(skip_serializing_if = "Option::is_none")] 24 | pub uv_auth_param: Option, 25 | } 26 | 27 | impl Ctap2AuthenticatorConfigRequest { 28 | pub(crate) fn new_toggle_always_uv() -> Self { 29 | Ctap2AuthenticatorConfigRequest { 30 | subcommand: Ctap2AuthenticatorConfigCommand::ToggleAlwaysUv, 31 | subcommand_params: None, 32 | protocol: None, // Will be filled out later by user_verification() 33 | uv_auth_param: None, // Will be filled out later by user_verification() 34 | } 35 | } 36 | 37 | pub(crate) fn new_enable_enterprise_attestation() -> Self { 38 | Ctap2AuthenticatorConfigRequest { 39 | subcommand: Ctap2AuthenticatorConfigCommand::EnableEnterpriseAttestation, 40 | subcommand_params: None, 41 | protocol: None, // Will be filled out later by user_verification() 42 | uv_auth_param: None, // Will be filled out later by user_verification() 43 | } 44 | } 45 | 46 | pub(crate) fn new_force_change_pin(force_change_pin: bool) -> Self { 47 | let subcommand_params = 48 | Ctap2AuthenticatorConfigParams::SetMinPINLength(Ctap2SetMinPINLengthParams { 49 | new_min_pin_length: None, 50 | min_pin_length_rpids: None, 51 | force_change_pin: Some(force_change_pin), 52 | }); 53 | Ctap2AuthenticatorConfigRequest { 54 | subcommand: Ctap2AuthenticatorConfigCommand::SetMinPINLength, 55 | subcommand_params: Some(subcommand_params), 56 | protocol: None, // Will be filled out later by user_verification() 57 | uv_auth_param: None, // Will be filled out later by user_verification() 58 | } 59 | } 60 | 61 | pub(crate) fn new_set_min_pin_length(new_pin_length: u64) -> Self { 62 | let subcommand_params = 63 | Ctap2AuthenticatorConfigParams::SetMinPINLength(Ctap2SetMinPINLengthParams { 64 | new_min_pin_length: Some(new_pin_length), 65 | min_pin_length_rpids: None, 66 | force_change_pin: None, 67 | }); 68 | Ctap2AuthenticatorConfigRequest { 69 | subcommand: Ctap2AuthenticatorConfigCommand::SetMinPINLength, 70 | subcommand_params: Some(subcommand_params), 71 | protocol: None, // Will be filled out later by user_verification() 72 | uv_auth_param: None, // Will be filled out later by user_verification() 73 | } 74 | } 75 | 76 | pub(crate) fn new_set_min_pin_length_rpids(rpids: Vec) -> Self { 77 | let subcommand_params = 78 | Ctap2AuthenticatorConfigParams::SetMinPINLengthRPIDs(Ctap2SetMinPINLengthParams { 79 | new_min_pin_length: None, 80 | min_pin_length_rpids: Some(rpids), 81 | force_change_pin: None, 82 | }); 83 | Ctap2AuthenticatorConfigRequest { 84 | subcommand: Ctap2AuthenticatorConfigCommand::SetMinPINLength, 85 | subcommand_params: Some(subcommand_params), 86 | protocol: None, // Will be filled out later by user_verification() 87 | uv_auth_param: None, // Will be filled out later by user_verification() 88 | } 89 | } 90 | } 91 | 92 | #[repr(u32)] 93 | #[derive(Debug, Copy, Clone, FromPrimitive, PartialEq, Serialize_repr, Deserialize_repr)] 94 | pub enum Ctap2AuthenticatorConfigCommand { 95 | EnableEnterpriseAttestation = 0x01, 96 | ToggleAlwaysUv = 0x02, 97 | SetMinPINLength = 0x03, 98 | VendorPrototype = 0xFF, 99 | } 100 | 101 | #[derive(Debug, Clone, Serialize)] 102 | #[serde(untagged)] 103 | pub enum Ctap2AuthenticatorConfigParams { 104 | SetMinPINLength(Ctap2SetMinPINLengthParams), 105 | SetMinPINLengthRPIDs(Ctap2SetMinPINLengthParams), 106 | } 107 | 108 | #[derive(Debug, Clone, SerializeIndexed)] 109 | #[serde_indexed(offset = 1)] 110 | pub struct Ctap2SetMinPINLengthParams { 111 | // newMinPINLength (0x01) 112 | #[serde(skip_serializing_if = "Option::is_none")] 113 | pub new_min_pin_length: Option, 114 | 115 | // minPinLengthRPIDs (0x02) 116 | #[serde(skip_serializing_if = "Option::is_none")] 117 | pub min_pin_length_rpids: Option>, 118 | 119 | // forceChangePin (0x03) 120 | #[serde(skip_serializing_if = "Option::is_none")] 121 | pub force_change_pin: Option, 122 | } 123 | -------------------------------------------------------------------------------- /libwebauthn/src/proto/ctap2/model/client_pin.rs: -------------------------------------------------------------------------------- 1 | use cosey::PublicKey; 2 | use serde_bytes::ByteBuf; 3 | use serde_indexed::{DeserializeIndexed, SerializeIndexed}; 4 | use serde_repr::{Deserialize_repr, Serialize_repr}; 5 | 6 | use crate::pin::{PinUvAuthProtocol, PinUvAuthProtocolOne, PinUvAuthProtocolTwo}; 7 | 8 | #[derive(Debug, Clone, SerializeIndexed)] 9 | #[serde_indexed(offset = 1)] 10 | pub struct Ctap2ClientPinRequest { 11 | ///pinUvAuthProtocol (0x01) 12 | #[serde(skip_serializing_if = "Option::is_none")] 13 | pub protocol: Option, 14 | 15 | /// subCommand (0x02) 16 | pub command: Ctap2PinUvAuthProtocolCommand, 17 | 18 | /// keyAgreement (0x03) 19 | #[serde(skip_serializing_if = "Option::is_none")] 20 | pub key_agreement: Option, 21 | 22 | /// pinUvAuthParam (0x04): 23 | #[serde(skip_serializing_if = "Option::is_none")] 24 | pub uv_auth_param: Option, 25 | 26 | /// newPinEnc (0x05) 27 | #[serde(skip_serializing_if = "Option::is_none")] 28 | pub new_pin_encrypted: Option, 29 | 30 | /// pinHashEnc (0x06) 31 | #[serde(skip_serializing_if = "Option::is_none")] 32 | pub pin_hash_encrypted: Option, 33 | 34 | #[serde(skip_serializing_if = "always_skip")] 35 | pub unused_07: (), 36 | 37 | #[serde(skip_serializing_if = "always_skip")] 38 | pub unused_08: (), 39 | 40 | /// permissions (0x09) 41 | #[serde(skip_serializing_if = "Option::is_none")] 42 | pub permissions: Option, 43 | 44 | /// permissions RPID (0x10) 45 | #[serde(skip_serializing_if = "Option::is_none")] 46 | pub permissions_rpid: Option, 47 | } 48 | 49 | impl Ctap2ClientPinRequest { 50 | pub fn new_get_key_agreement(protocol: Ctap2PinUvAuthProtocol) -> Self { 51 | Self { 52 | protocol: Some(protocol), 53 | command: Ctap2PinUvAuthProtocolCommand::GetKeyAgreement, 54 | key_agreement: None, 55 | uv_auth_param: None, 56 | new_pin_encrypted: None, 57 | pin_hash_encrypted: None, 58 | unused_07: (), 59 | unused_08: (), 60 | permissions: None, 61 | permissions_rpid: None, 62 | } 63 | } 64 | 65 | pub fn new_get_pin_token( 66 | protocol: Ctap2PinUvAuthProtocol, 67 | public_key: PublicKey, 68 | pin_hash_enc: &[u8], 69 | ) -> Self { 70 | Self { 71 | protocol: Some(protocol), 72 | command: Ctap2PinUvAuthProtocolCommand::GetPinToken, 73 | key_agreement: Some(public_key), 74 | uv_auth_param: None, 75 | new_pin_encrypted: None, 76 | pin_hash_encrypted: Some(ByteBuf::from(pin_hash_enc)), 77 | unused_07: (), 78 | unused_08: (), 79 | permissions: None, 80 | permissions_rpid: None, 81 | } 82 | } 83 | 84 | pub fn new_get_pin_retries(pin_proto: Option) -> Self { 85 | Self { 86 | protocol: pin_proto, 87 | command: Ctap2PinUvAuthProtocolCommand::GetPinRetries, 88 | key_agreement: None, 89 | uv_auth_param: None, 90 | new_pin_encrypted: None, 91 | pin_hash_encrypted: None, 92 | unused_07: (), 93 | unused_08: (), 94 | permissions: None, 95 | permissions_rpid: None, 96 | } 97 | } 98 | 99 | pub fn new_get_pin_token_with_perm( 100 | protocol: Ctap2PinUvAuthProtocol, 101 | public_key: PublicKey, 102 | pin_hash_enc: &[u8], 103 | permissions: Ctap2AuthTokenPermissionRole, 104 | permissions_rpid: Option<&str>, 105 | ) -> Self { 106 | Self { 107 | protocol: Some(protocol), 108 | command: Ctap2PinUvAuthProtocolCommand::GetPinUvAuthTokenUsingPinWithPermissions, 109 | key_agreement: Some(public_key), 110 | uv_auth_param: None, 111 | new_pin_encrypted: None, 112 | pin_hash_encrypted: Some(ByteBuf::from(pin_hash_enc)), 113 | unused_07: (), 114 | unused_08: (), 115 | permissions: Some(permissions.bits()), 116 | permissions_rpid: permissions_rpid.map(str::to_owned), 117 | } 118 | } 119 | 120 | pub fn new_get_uv_token_with_perm( 121 | protocol: Ctap2PinUvAuthProtocol, 122 | public_key: PublicKey, 123 | permissions: Ctap2AuthTokenPermissionRole, 124 | permissions_rpid: Option<&str>, 125 | ) -> Self { 126 | Self { 127 | protocol: Some(protocol), 128 | command: Ctap2PinUvAuthProtocolCommand::GetPinUvAuthTokenUsingUvWithPermissions, 129 | key_agreement: Some(public_key), 130 | uv_auth_param: None, 131 | new_pin_encrypted: None, 132 | pin_hash_encrypted: None, 133 | unused_07: (), 134 | unused_08: (), 135 | permissions: Some(permissions.bits()), 136 | permissions_rpid: permissions_rpid.map(str::to_owned), 137 | } 138 | } 139 | 140 | pub fn new_get_uv_retries() -> Self { 141 | Self { 142 | // GetUvRetries never needed the protocol sent. GetPINRetries did for CTAP 2.0 143 | protocol: None, 144 | command: Ctap2PinUvAuthProtocolCommand::GetUvRetries, 145 | key_agreement: None, 146 | uv_auth_param: None, 147 | new_pin_encrypted: None, 148 | pin_hash_encrypted: None, 149 | unused_07: (), 150 | unused_08: (), 151 | permissions: None, 152 | permissions_rpid: None, 153 | } 154 | } 155 | 156 | pub fn new_change_pin( 157 | protocol: Ctap2PinUvAuthProtocol, 158 | new_pin_enc: &[u8], 159 | curr_pin_enc: &[u8], 160 | public_key: PublicKey, 161 | uv_auth_param: &[u8], 162 | ) -> Self { 163 | Self { 164 | protocol: Some(protocol), 165 | command: Ctap2PinUvAuthProtocolCommand::ChangePin, 166 | key_agreement: Some(public_key), 167 | uv_auth_param: Some(ByteBuf::from(uv_auth_param)), 168 | new_pin_encrypted: Some(ByteBuf::from(new_pin_enc)), 169 | pin_hash_encrypted: Some(ByteBuf::from(curr_pin_enc)), 170 | unused_07: (), 171 | unused_08: (), 172 | permissions: None, 173 | permissions_rpid: None, 174 | } 175 | } 176 | 177 | pub fn new_set_pin( 178 | protocol: Ctap2PinUvAuthProtocol, 179 | new_pin_enc: &[u8], 180 | public_key: PublicKey, 181 | uv_auth_param: &[u8], 182 | ) -> Self { 183 | Self { 184 | protocol: Some(protocol), 185 | command: Ctap2PinUvAuthProtocolCommand::SetPin, 186 | key_agreement: Some(public_key), 187 | uv_auth_param: Some(ByteBuf::from(uv_auth_param)), 188 | new_pin_encrypted: Some(ByteBuf::from(new_pin_enc)), 189 | pin_hash_encrypted: None, 190 | unused_07: (), 191 | unused_08: (), 192 | permissions: None, 193 | permissions_rpid: None, 194 | } 195 | } 196 | } 197 | 198 | bitflags! { 199 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 200 | pub struct Ctap2AuthTokenPermissionRole: u32 { 201 | const MAKE_CREDENTIAL = 0x01; 202 | const GET_ASSERTION = 0x02; 203 | const CREDENTIAL_MANAGEMENT = 0x04; 204 | const BIO_ENROLLMENT = 0x08; 205 | const LARGE_BLOB_WRITE = 0x10; 206 | const AUTHENTICATOR_CONFIGURATION = 0x20; 207 | } 208 | } 209 | 210 | #[repr(u32)] 211 | #[derive(Debug, Clone, Copy, FromPrimitive, PartialEq, Eq, Serialize_repr, Deserialize_repr)] 212 | pub enum Ctap2PinUvAuthProtocol { 213 | One = 1, 214 | Two = 2, 215 | } 216 | 217 | impl Ctap2PinUvAuthProtocol { 218 | pub(crate) fn create_protocol_object(&self) -> Box { 219 | match self { 220 | Ctap2PinUvAuthProtocol::One => Box::new(PinUvAuthProtocolOne::new()), 221 | Ctap2PinUvAuthProtocol::Two => Box::new(PinUvAuthProtocolTwo::new()), 222 | } 223 | } 224 | } 225 | 226 | #[repr(u32)] 227 | #[derive(Debug, Clone, FromPrimitive, PartialEq, Serialize_repr, Deserialize_repr)] 228 | pub enum Ctap2PinUvAuthProtocolCommand { 229 | GetPinRetries = 0x01, 230 | GetKeyAgreement = 0x02, 231 | SetPin = 0x03, 232 | ChangePin = 0x04, 233 | GetPinToken = 0x05, 234 | GetPinUvAuthTokenUsingUvWithPermissions = 0x06, 235 | GetUvRetries = 0x07, 236 | GetPinUvAuthTokenUsingPinWithPermissions = 0x09, 237 | } 238 | 239 | #[derive(Debug, Clone, Default, DeserializeIndexed)] 240 | #[serde_indexed(offset = 1)] 241 | pub struct Ctap2ClientPinResponse { 242 | /// keyAgreement (0x01) 243 | #[serde(skip_serializing_if = "Option::is_none")] 244 | pub key_agreement: Option, 245 | 246 | /// pinUvAuthToken (0x02) 247 | #[serde(skip_serializing_if = "Option::is_none")] 248 | pub pin_uv_auth_token: Option, 249 | 250 | /// pinRetries (0x03) 251 | #[serde(skip_serializing_if = "Option::is_none")] 252 | pub pin_retries: Option, 253 | 254 | /// powerCycleState (0x04) 255 | #[serde(skip_serializing_if = "Option::is_none")] 256 | pub power_cycle_state: Option, 257 | 258 | /// uvRetries (0x05) 259 | #[serde(skip_serializing_if = "Option::is_none")] 260 | pub uv_retries: Option, 261 | } 262 | 263 | // Required by serde_indexed, as serde(skip) isn't supported yet: 264 | // https://github.com/trussed-dev/serde-indexed/pull/14 265 | fn always_skip(_v: &()) -> bool { 266 | true 267 | } 268 | -------------------------------------------------------------------------------- /libwebauthn/src/proto/ctap2/model/get_info.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde_bytes::ByteBuf; 4 | use serde_indexed::DeserializeIndexed; 5 | use tracing::debug; 6 | 7 | use super::{Ctap2CredentialType, Ctap2UserVerificationOperation}; 8 | 9 | #[derive(Debug, Clone, DeserializeIndexed)] 10 | #[serde_indexed(offset = 1)] 11 | pub struct Ctap2GetInfoResponse { 12 | /// versions (0x01) 13 | pub versions: Vec, 14 | 15 | /// extensions (0x02) 16 | #[serde(skip_serializing_if = "Option::is_none")] 17 | pub extensions: Option>, 18 | 19 | /// aaguid (0x03) 20 | pub aaguid: ByteBuf, 21 | 22 | /// options (0x04) 23 | #[serde(skip_serializing_if = "Option::is_none")] 24 | pub options: Option>, 25 | 26 | /// maxMsgSize (0x05) 27 | #[serde(skip_serializing_if = "Option::is_none")] 28 | pub max_msg_size: Option, 29 | 30 | /// pinUvAuthProtocols (0x06) 31 | #[serde(skip_serializing_if = "Option::is_none")] 32 | pub pin_auth_protos: Option>, 33 | 34 | /// maxCredentialCountInList (0x07) 35 | #[serde(skip_serializing_if = "Option::is_none")] 36 | pub max_credential_count: Option, 37 | 38 | /// maxCredentialIdLength (0x08) 39 | #[serde(skip_serializing_if = "Option::is_none")] 40 | pub max_credential_id_length: Option, 41 | 42 | /// transports (0x09) 43 | #[serde(skip_serializing_if = "Option::is_none")] 44 | pub transports: Option>, 45 | 46 | /// algorithms (0x0A) 47 | #[serde(skip_serializing_if = "Option::is_none")] 48 | pub algorithms: Option>, 49 | 50 | /// maxSerializedLargeBlobArray (0x0B) 51 | #[serde(skip_serializing_if = "Option::is_none")] 52 | pub max_blob_array: Option, 53 | 54 | /// forcePINChange (0x0C) 55 | #[serde(skip_serializing_if = "Option::is_none")] 56 | pub force_pin_change: Option, 57 | 58 | /// minPINLength (0x0D) 59 | #[serde(skip_serializing_if = "Option::is_none")] 60 | pub min_pin_length: Option, 61 | 62 | /// firmwareVersion (0x0E) 63 | #[serde(skip_serializing_if = "Option::is_none")] 64 | pub firmware_version: Option, 65 | 66 | /// maxCredBlobLength (0x0F) 67 | #[serde(skip_serializing_if = "Option::is_none")] 68 | pub max_cred_blob_length: Option, 69 | 70 | /// maxRPIDsForSetMinPINLength (0x10) 71 | #[serde(skip_serializing_if = "Option::is_none")] 72 | pub max_rpids_for_setminpinlength: Option, 73 | 74 | /// preferredPlatformUvAttempts (0x11) 75 | #[serde(skip_serializing_if = "Option::is_none")] 76 | pub preferred_platform_uv_attempts: Option, 77 | 78 | /// uvModality (0x12) 79 | #[serde(skip_serializing_if = "Option::is_none")] 80 | pub uv_modality: Option, 81 | 82 | /// certifications (0x13) 83 | #[serde(skip_serializing_if = "Option::is_none")] 84 | pub certifications: Option>, 85 | 86 | /// remainingDiscoverableCredentials (0x14) 87 | #[serde(skip_serializing_if = "Option::is_none")] 88 | pub remaining_discoverable_creds: Option, 89 | 90 | /// vendorPrototypeConfigCommands (0x15) 91 | #[serde(skip_serializing_if = "Option::is_none")] 92 | pub vendor_proto_config_cmds: Option>, 93 | 94 | /// attestationFormats (0x16) 95 | #[serde(skip_serializing_if = "Option::is_none")] 96 | pub attestation_formats: Option>, 97 | 98 | /// uvCountSinceLastPinEntry (0x17) 99 | #[serde(skip_serializing_if = "Option::is_none")] 100 | pub uv_count_since_last_pin_entry: Option, 101 | 102 | /// longTouchForReset (0x18) 103 | #[serde(skip_serializing_if = "Option::is_none")] 104 | pub long_touch_for_reset: Option, 105 | 106 | /// encIdentifier (0x19) 107 | #[serde(skip_serializing_if = "Option::is_none")] 108 | pub enc_identifier: Option, 109 | 110 | /// transportsForReset (0x1A) 111 | #[serde(skip_serializing_if = "Option::is_none")] 112 | pub transports_for_reset: Option>, 113 | 114 | /// pinComplexityPolicy (0x1B) 115 | #[serde(skip_serializing_if = "Option::is_none")] 116 | pub pin_complexity_policy: Option, 117 | 118 | /// pinComplexityPolicyURL (0x1C) 119 | #[serde(skip_serializing_if = "Option::is_none")] 120 | pub pin_complexity_policy_url: Option, 121 | 122 | /// maxPINLength (0x1D) 123 | #[serde(skip_serializing_if = "Option::is_none")] 124 | pub max_pin_length: Option, 125 | } 126 | 127 | impl Ctap2GetInfoResponse { 128 | pub fn option_enabled(&self, name: &str) -> bool { 129 | if self.options.is_none() { 130 | return false; 131 | } 132 | let options = self.options.as_ref().unwrap(); 133 | options.get(name) == Some(&true) 134 | } 135 | 136 | pub fn supports_fido_2_1(&self) -> bool { 137 | self.versions.iter().any(|v| v == "FIDO_2_1") 138 | } 139 | 140 | pub fn supports_credential_management(&self) -> bool { 141 | self.option_enabled("credMgmt") || self.option_enabled("credentialMgmtPreview") 142 | } 143 | 144 | pub fn supports_bio_enrollment(&self) -> bool { 145 | if let Some(options) = &self.options { 146 | return options.get("bioEnroll").is_some() 147 | || options.get("userVerificationMgmtPreview").is_some(); 148 | } 149 | false 150 | } 151 | 152 | pub fn has_bio_enrollments(&self) -> bool { 153 | if let Some(options) = &self.options { 154 | return options.get("bioEnroll") == Some(&true) 155 | || options.get("userVerificationMgmtPreview") == Some(&true); 156 | } 157 | false 158 | } 159 | 160 | /// Implements check for "Protected by some form of User Verification": 161 | /// Either or both clientPin or built-in user verification methods are supported and enabled. 162 | /// I.e., in the authenticatorGetInfo response the pinUvAuthToken option ID is present and set to true, 163 | /// and either clientPin option ID is present and set to true or uv option ID is present and set to true or both. 164 | pub fn is_uv_protected(&self) -> bool { 165 | self.option_enabled("uv") || // Deprecated no-op UV 166 | self.option_enabled("clientPin") || 167 | (self.option_enabled("pinUvAuthToken") && self.option_enabled("uv")) 168 | } 169 | 170 | pub fn uv_operation(&self, uv_blocked: bool) -> Option { 171 | if self.option_enabled("uv") && !uv_blocked { 172 | if self.option_enabled("pinUvAuthToken") { 173 | debug!("getPinUvAuthTokenUsingUvWithPermissions"); 174 | return Some( 175 | Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingUvWithPermissions, 176 | ); 177 | } else { 178 | debug!("Deprecated FIDO 2.0 behaviour: populating 'uv' flag"); 179 | return Some(Ctap2UserVerificationOperation::None); 180 | } 181 | } else { 182 | // !uv 183 | if self.option_enabled("pinUvAuthToken") { 184 | assert!(self.option_enabled("clientPin")); 185 | debug!("getPinUvAuthTokenUsingPinWithPermissions"); 186 | return Some( 187 | Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingPinWithPermissions, 188 | ); 189 | } else if self.option_enabled("clientPin") { 190 | // !pinUvAuthToken 191 | debug!("getPinToken"); 192 | return Some(Ctap2UserVerificationOperation::GetPinToken); 193 | } else { 194 | debug!("No UV and no PIN (e.g. maybe UV was blocked and no PIN available)"); 195 | return None; 196 | } 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /libwebauthn/src/proto/ctap2/preflight.rs: -------------------------------------------------------------------------------- 1 | use serde_bytes::ByteBuf; 2 | use std::time::Duration; 3 | use tracing::{debug, info}; 4 | 5 | use super::{Ctap2GetAssertionRequest, Ctap2PublicKeyCredentialDescriptor}; 6 | use crate::{ 7 | proto::ctap2::{model::Ctap2GetAssertionOptions, Ctap2}, 8 | transport::Channel, 9 | }; 10 | 11 | /// https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#pre-flight 12 | /// pre-flight 13 | /// 14 | /// In order to determine whether authenticatorMakeCredential's excludeList or 15 | /// authenticatorGetAssertion's allowList contain credential IDs that are already present on an 16 | /// authenticator, a platform typically invokes authenticatorGetAssertion with the "up" option 17 | /// key set to false and optionally pinUvAuthParam one or more times. If a credential is found an 18 | /// assertion is returned. If a valid pinUvAuthParam was also provided, the response will contain 19 | /// "up"=0 and "uv"=1 within the "flags bits" of the authenticator data structure, otherwise the 20 | /// "flag bits" will contain "up"=0 and "uv"=0. 21 | pub(crate) async fn ctap2_preflight( 22 | channel: &mut C, 23 | credentials: &[Ctap2PublicKeyCredentialDescriptor], 24 | client_data_hash: &[u8], 25 | rp: &str, 26 | ) -> Vec { 27 | info!("Credential list BEFORE preflight: {credentials:?}"); 28 | let mut filtered_list = Vec::new(); 29 | for credential in credentials { 30 | let preflight_request = Ctap2GetAssertionRequest { 31 | relying_party_id: rp.to_string(), 32 | client_data_hash: ByteBuf::from(client_data_hash), 33 | allow: vec![credential.clone()], 34 | extensions: None, 35 | options: Some(Ctap2GetAssertionOptions { 36 | require_user_presence: false, 37 | require_user_verification: false, 38 | }), 39 | pin_auth_param: None, 40 | pin_auth_proto: None, 41 | }; 42 | match channel 43 | .ctap2_get_assertion(&preflight_request, Duration::from_secs(2)) 44 | .await 45 | { 46 | Ok(resp) => { 47 | debug!("Pre-flight: Found already known credential {credential:?}"); 48 | // This credential is known to the device 49 | // Now we have to figure out it's ID. There are 3 options: 50 | let id = resp 51 | // 1. Directly in the response "credential_id" 52 | .credential_id 53 | // 2. In the attested_credential 54 | .or(resp 55 | .authenticator_data 56 | .attested_credential 57 | .map(|x| Ctap2PublicKeyCredentialDescriptor::from(&x))) 58 | // 3. Neither, which is allowed, if the allow_list was of length 1, then 59 | // we have to copy it ourselfs from the input 60 | .unwrap_or(credential.clone()); 61 | filtered_list.push(id); 62 | } 63 | Err(e) => { 64 | debug!("Pre-flight: Filtering out {credential:?}, because of error: {e:?}"); 65 | // This credential is unknown to the device. So we can filter it out. 66 | // NOTE: According to spec a CTAP2_ERR_NO_CREDENTIALS should be returned, other return values have been observed. 67 | continue; 68 | } 69 | } 70 | } 71 | info!("Credential list AFTER preflight: {filtered_list:?}"); 72 | filtered_list 73 | } 74 | -------------------------------------------------------------------------------- /libwebauthn/src/proto/error.rs: -------------------------------------------------------------------------------- 1 | use num_enum::{IntoPrimitive, TryFromPrimitive}; 2 | 3 | use crate::proto::ctap1::apdu::ApduResponseStatus; 4 | 5 | // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#error-responses 6 | 7 | #[derive(Debug, IntoPrimitive, TryFromPrimitive, Copy, Clone, PartialEq)] 8 | #[repr(u8)] 9 | pub enum CtapError { 10 | Ok = 0x00, // CTAP1_ERR_SUCCESS, CTAP2_OK 11 | InvalidCommand = 0x01, // CTAP1_ERR_INVALID_COMMAND 12 | InvalidParameter = 0x02, // CTAP1_ERR_INVALID_PARAMETER 13 | InvalidLength = 0x03, // CTAP1_ERR_INVALID_LENGTH 14 | InvalidSeq = 0x04, // CTAP1_ERR_INVALID_SEQ 15 | Timeout = 0x05, // CTAP1_ERR_TIMEOUT 16 | ChannelBusy = 0x06, // CTAP1_ERR_CHANNEL_BUSY 17 | LockRequired = 0x0A, // CTAP1_ERR_LOCK_REQUIRED 18 | InvalidChannel = 0x0B, // CTAP1_ERR_INVALID_CHANNEL 19 | InvalidCborType = 0x11, // CTAP2_ERR_CBOR_UNEXPECTED_TYPE 20 | InvalidCbor = 0x12, // CTAP2_ERR_INVALID_CBOR 21 | MissingParameter = 0x14, // CTAP2_ERR_MISSING_PARAMETER 22 | LimitExceeded = 0x15, // CTAP2_ERR_LIMIT_EXCEEDED, 23 | UnsupportedExtension = 0x16, // CTAP2_ERR_UNSUPPORTED_EXTENSION 24 | CredentialExcluded = 0x19, // CTAP2_ERR_CREDENTIAL_EXCLUDED 25 | Processing = 0x21, // CTAP2_ERR_PROCESSING 26 | InvalidCredential = 0x22, // CTAP2_ERR_INVALID_CREDENTIAL 27 | UserActionPending = 0x23, // CTAP2_ERR_USER_ACTION_PENDING 28 | OperationPending = 0x24, // CTAP2_ERR_OPERATION_PENDING 29 | NoOperations = 0x25, // CTAP2_ERR_NO_OPERATIONS 30 | UnsupportedAlgorithm = 0x26, // CTAP2_ERR_UNSUPPORTED_ALGORITHM 31 | OperationDenied = 0x27, // CTAP2_ERR_OPERATION_DENIED 32 | KeyStoreFull = 0x28, // CTAP2_ERR_KEY_STORE_FULL 33 | NoOperationPending = 0x2A, // CTAP2_ERR_NO_OPERATION_PENDING 34 | UnsupportedOption = 0x2B, // CTAP2_ERR_UNSUPPORTED_OPTION 35 | InvalidOption = 0x2C, // CTAP2_ERR_INVALID_OPTION 36 | KeepAliveCancel = 0x2D, // CTAP2_ERR_KEEPALIVE_CANCEL 37 | NoCredentials = 0x2E, // CTAP2_ERR_NO_CREDENTIALS 38 | UserActionTimeout = 0x2F, // CTAP2_ERR_USER_ACTION_TIMEOUT 39 | NotAllowed = 0x30, // CTAP2_ERR_NOT_ALLOWED 40 | PINInvalid = 0x31, // CTAP2_ERR_PIN_INVALID 41 | PINBlocked = 0x32, // CTAP2_ERR_PIN_BLOCKED 42 | PINAuthInvalid = 0x33, // CTAP2_ERR_PIN_AUTH_INVALID 43 | PINAuthBlocked = 0x34, // CTAP2_ERR_PIN_AUTH_BLOCKED 44 | PINNotSet = 0x35, // CTAP2_ERR_PIN_NOT_SET 45 | PINRequired = 0x36, // CTAP2_ERR_PIN_REQUIRED 46 | PINPolicyViolation = 0x37, // CTAP2_ERR_PIN_POLICY_VIOLATION 47 | PINTokenExpired = 0x38, // CTAP2_ERR_PIN_TOKEN_EXPIRED 48 | RequestTooLarge = 0x39, // CTAP2_ERR_REQUEST_TOO_LARGE 49 | ActionTimeout = 0x3A, // CTAP2_ERR_ACTION_TIMEOUT 50 | UserPresenceRequired = 0x3B, // CTAP2_ERR_UP_REQUIRED 51 | UvBlocked = 0x3C, // CTAP2_ERR_UV_BLOCKED 52 | IntegrityFailure = 0x3D, // CTAP2_ERR_INTEGRITY_FAILURE 53 | InvalidSubcommand = 0x3E, // CTAP2_ERR_INVALID_SUBCOMMAND 54 | UVInvalid = 0x3F, // CTAP2_ERR_UV_INVALID 55 | UnauthorizedPermission = 0x40, // CTAP2_ERR_UNAUTHORIZED_PERMISSION 56 | Other = 0x7F, // CTAP1_ERR_OTHER 57 | } 58 | 59 | impl CtapError { 60 | pub fn is_retryable_user_error(&self) -> bool { 61 | match &self { 62 | Self::PINInvalid | Self::UVInvalid => true, // PIN or biometric auth failed 63 | Self::UserActionTimeout => true, // User action timed out 64 | _ => false, 65 | } 66 | } 67 | } 68 | 69 | impl std::error::Error for CtapError {} 70 | 71 | impl std::fmt::Display for CtapError { 72 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 73 | write!( 74 | f, 75 | "{:?} (retryable user error: {})", 76 | self, 77 | self.is_retryable_user_error() 78 | ) 79 | } 80 | } 81 | 82 | impl From for CtapError { 83 | fn from(status: ApduResponseStatus) -> Self { 84 | match status { 85 | ApduResponseStatus::NoError => CtapError::Ok, 86 | ApduResponseStatus::UserPresenceTestFailed => CtapError::UserPresenceRequired, 87 | ApduResponseStatus::InvalidKeyHandle => CtapError::NoCredentials, 88 | ApduResponseStatus::InvalidRequestLength => CtapError::InvalidLength, 89 | ApduResponseStatus::InvalidClassByte => CtapError::Other, 90 | ApduResponseStatus::InvalidInstruction => CtapError::InvalidCommand, 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /libwebauthn/src/proto/mod.rs: -------------------------------------------------------------------------------- 1 | mod error; 2 | 3 | pub mod ctap1; 4 | pub mod ctap2; 5 | 6 | pub use error::CtapError; 7 | -------------------------------------------------------------------------------- /libwebauthn/src/transport/ble/btleplug/connection.rs: -------------------------------------------------------------------------------- 1 | use std::io::Cursor as IOCursor; 2 | 3 | use btleplug::api::{Peripheral as _, WriteType}; 4 | use btleplug::platform::Peripheral; 5 | use byteorder::{BigEndian, ReadBytesExt}; 6 | use tracing::{debug, info, instrument, trace, warn}; 7 | 8 | use super::device::FidoEndpoints; 9 | use super::Error; 10 | use crate::fido::FidoRevision; 11 | use crate::transport::ble::framing::{ 12 | BleCommand, BleFrame as Frame, BleFrameParser, BleFrameParserResult, 13 | }; 14 | 15 | #[derive(Debug, Clone)] 16 | pub struct Connection { 17 | pub peripheral: Peripheral, 18 | pub services: FidoEndpoints, 19 | } 20 | 21 | impl Connection { 22 | pub async fn new( 23 | peripheral: &Peripheral, 24 | services: &FidoEndpoints, 25 | revision: &FidoRevision, 26 | ) -> Result { 27 | let connection = Self { 28 | peripheral: peripheral.to_owned(), 29 | services: services.clone(), 30 | }; 31 | connection.select_fido_revision(revision).await?; 32 | Ok(connection) 33 | } 34 | 35 | async fn control_point_length(&self) -> Result { 36 | let max_fragment_length = self 37 | .peripheral 38 | .read(&self.services.control_point_length) 39 | .await 40 | .or(Err(Error::OperationFailed))?; 41 | 42 | if max_fragment_length.len() != 2 { 43 | warn!( 44 | { len = max_fragment_length.len() }, 45 | "Control point length endpoint returned an unexpected number of bytes", 46 | ); 47 | return Err(Error::OperationFailed); 48 | } 49 | 50 | let mut cursor = IOCursor::new(max_fragment_length); 51 | let max_fragment_size = cursor 52 | .read_u16::() 53 | .map_err(|_| Error::OperationFailed)? as usize; 54 | Ok(max_fragment_size) 55 | } 56 | 57 | pub async fn frame_send(&self, frame: &Frame) -> Result<(), Error> { 58 | let max_fragment_size = self.control_point_length().await?; 59 | let fragments = frame 60 | .fragments(max_fragment_size) 61 | .or(Err(Error::InvalidFraming))?; 62 | 63 | for (i, fragment) in fragments.iter().enumerate() { 64 | debug!({ fragment = i, len = fragment.len() }, "Sending fragment"); 65 | trace!(?fragment); 66 | 67 | self.peripheral 68 | .write( 69 | &self.services.control_point, 70 | fragment, 71 | WriteType::WithoutResponse, 72 | ) 73 | .await 74 | .or(Err(Error::OperationFailed))?; 75 | } 76 | 77 | Ok(()) 78 | } 79 | 80 | pub(crate) async fn select_fido_revision(&self, revision: &FidoRevision) -> Result<(), Error> { 81 | let ack: u8 = revision.clone() as u8; 82 | self.peripheral 83 | .write( 84 | &self.services.service_revision_bitfield, 85 | &[ack], 86 | WriteType::WithoutResponse, 87 | ) 88 | .await 89 | .or(Err(Error::OperationFailed))?; 90 | 91 | info!(?revision, "Successfully selected FIDO revision"); 92 | Ok(()) 93 | } 94 | 95 | #[instrument(skip_all)] 96 | pub async fn frame_recv(&self) -> Result { 97 | let mut parser = BleFrameParser::new(); 98 | 99 | loop { 100 | let fragment = self.receive_fragment().await?; 101 | debug!("Received fragment"); 102 | trace!(?fragment); 103 | 104 | let status = parser.update(&fragment).or(Err(Error::InvalidFraming))?; 105 | match status { 106 | BleFrameParserResult::Done => { 107 | let frame = parser.frame().unwrap(); 108 | trace!(?frame, "Received frame"); 109 | match frame.cmd { 110 | BleCommand::Keepalive => { 111 | debug!("Received keep-alive from authenticator"); 112 | parser.reset(); 113 | } 114 | BleCommand::Cancel => { 115 | info!("Device canceled operation"); 116 | return Err(Error::Canceled); 117 | } 118 | BleCommand::Error => { 119 | warn!("Received error frame"); 120 | return Err(Error::OperationFailed); 121 | } 122 | BleCommand::Ping => { 123 | debug!("Ignoring ping from device"); 124 | } 125 | BleCommand::Msg => { 126 | debug!("Received operation response"); 127 | return Ok(frame); 128 | } 129 | } 130 | } 131 | BleFrameParserResult::MoreFragmentsExpected => {} 132 | } 133 | } 134 | } 135 | 136 | async fn receive_fragment(&self) -> Result, Error> { 137 | self.peripheral 138 | .read(&self.services.status) 139 | .await 140 | .or(Err(Error::OperationFailed)) 141 | } 142 | 143 | pub async fn subscribe(&self) -> Result<(), Error> { 144 | self.peripheral 145 | .subscribe(&self.services.status) 146 | .await 147 | .or(Err(Error::OperationFailed)) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /libwebauthn/src/transport/ble/btleplug/device.rs: -------------------------------------------------------------------------------- 1 | use std::hash::Hash; 2 | 3 | use btleplug::{ 4 | api::{Characteristic, Peripheral as _, PeripheralProperties}, 5 | platform::Peripheral, 6 | }; 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct FidoDevice { 10 | pub peripheral: Peripheral, 11 | pub properties: PeripheralProperties, 12 | } 13 | 14 | impl PartialEq for FidoDevice { 15 | fn eq(&self, other: &Self) -> bool { 16 | self.peripheral.id() == other.peripheral.id() 17 | } 18 | } 19 | 20 | impl Eq for FidoDevice {} 21 | 22 | impl Hash for FidoDevice { 23 | fn hash(&self, state: &mut H) { 24 | self.peripheral.id().hash(state); 25 | } 26 | } 27 | 28 | #[derive(Debug, Clone)] 29 | pub struct FidoEndpoints { 30 | pub control_point: Characteristic, 31 | pub control_point_length: Characteristic, 32 | pub status: Characteristic, 33 | pub service_revision_bitfield: Characteristic, 34 | } 35 | -------------------------------------------------------------------------------- /libwebauthn/src/transport/ble/btleplug/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror; 2 | 3 | #[derive(thiserror::Error, Debug, Copy, Clone, PartialEq)] 4 | pub enum Error { 5 | #[error("invalid framing")] 6 | InvalidFraming, 7 | #[error("operation failed")] 8 | OperationFailed, 9 | #[error("connection failed")] 10 | ConnectionFailed, 11 | #[error("unavailalbe")] 12 | Unavailable, 13 | #[error("adapter powered off")] 14 | PoweredOff, 15 | #[error("operation canceled")] 16 | Canceled, 17 | #[error("operation timed out")] 18 | Timeout, 19 | } 20 | -------------------------------------------------------------------------------- /libwebauthn/src/transport/ble/btleplug/gatt.rs: -------------------------------------------------------------------------------- 1 | use btleplug::api::{Characteristic, Peripheral as _}; 2 | use btleplug::platform::Peripheral; 3 | use uuid::Uuid; 4 | 5 | use super::Error; 6 | 7 | pub fn get_gatt_characteristic( 8 | peripheral: &Peripheral, 9 | uuid: Uuid, 10 | ) -> Result { 11 | peripheral 12 | .characteristics() 13 | .iter() 14 | .find(|c| c.uuid == uuid) 15 | .map(ToOwned::to_owned) 16 | .ok_or(Error::ConnectionFailed) 17 | } 18 | -------------------------------------------------------------------------------- /libwebauthn/src/transport/ble/btleplug/manager.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use btleplug::api::bleuuid::uuid_from_u16; 4 | use btleplug::api::{ 5 | Central as _, CentralEvent, Manager as _, Peripheral as _, PeripheralProperties, ScanFilter, 6 | }; 7 | use btleplug::platform::{Adapter, Manager, Peripheral, PeripheralId}; 8 | use futures::{Stream, StreamExt}; 9 | use tracing::{debug, info, instrument, trace, warn, Level}; 10 | use uuid::Uuid; 11 | 12 | use super::device::FidoEndpoints; 13 | use super::gatt::get_gatt_characteristic; 14 | use super::{Connection, Error, FidoDevice}; 15 | use crate::fido::{FidoProtocol, FidoRevision}; 16 | 17 | pub const FIDO_PROFILE_UUID: Uuid = uuid_from_u16(0xFFFD); 18 | 19 | pub const FIDO_CONTROL_POINT_UUID: &str = "f1d0fff1-deaa-ecee-b42f-c9ba7ed623bb"; 20 | pub const FIDO_STATUS_UUID: &str = "f1d0fff2-deaa-ecee-b42f-c9ba7ed623bb"; 21 | pub const FIDO_CONTROL_POINT_LENGTH_UUID: &str = "f1d0fff3-deaa-ecee-b42f-c9ba7ed623bb"; 22 | pub const FIDO_REVISION_BITFIELD_UUID: &str = "f1d0fff4-deaa-ecee-b42f-c9ba7ed623bb"; 23 | 24 | #[derive(Debug, Copy, Clone)] 25 | pub struct SupportedRevisions { 26 | pub u2fv11: bool, 27 | pub u2fv12: bool, 28 | pub v2: bool, 29 | } 30 | 31 | impl SupportedRevisions { 32 | pub fn select_protocol(&self, protocol: FidoProtocol) -> Option { 33 | match protocol { 34 | FidoProtocol::FIDO2 => { 35 | if self.v2 { 36 | Some(FidoRevision::V2) 37 | } else { 38 | None 39 | } 40 | } 41 | FidoProtocol::U2F => { 42 | if self.u2fv12 { 43 | Some(FidoRevision::U2fv12) 44 | } else if self.u2fv11 { 45 | Some(FidoRevision::U2fv11) 46 | } else { 47 | None 48 | } 49 | } 50 | } 51 | } 52 | } 53 | 54 | async fn on_peripheral_service_data( 55 | adapter: &Adapter, 56 | id: &PeripheralId, 57 | uuids: &[Uuid], 58 | service_data: HashMap>, 59 | ) -> Option<(Adapter, Peripheral, Vec)> { 60 | for uuid in uuids { 61 | if let Some(service_data) = service_data.get(uuid) { 62 | trace!(?id, ?service_data, "Found service data"); 63 | let Ok(peripheral) = adapter.peripheral(id).await else { 64 | warn!(?id, "Could not get peripheral"); 65 | return None; 66 | }; 67 | 68 | debug!({ ?id, ?service_data }, "Found service data for peripheral"); 69 | return Some((adapter.clone(), peripheral, service_data.to_owned())); 70 | } 71 | } 72 | 73 | trace!( 74 | { ?id, ?service_data }, 75 | "Ignoring periperal as it doesn't have service data for desired UUID" 76 | ); 77 | None 78 | } 79 | 80 | #[instrument(level = Level::DEBUG, skip_all)] 81 | /// Starts a discovery for devices advertising service data on any of the provided UUIDs 82 | pub async fn start_discovery_for_service_data( 83 | uuids: &[Uuid], 84 | ) -> Result)> + use<'_>, Error> { 85 | let adapter = get_adapter().await?; 86 | let scan_filter = ScanFilter::default(); 87 | 88 | let events = adapter.events().await.or(Err(Error::Unavailable))?; 89 | 90 | adapter 91 | .start_scan(scan_filter) 92 | .await 93 | .or(Err(Error::ConnectionFailed))?; 94 | 95 | let stream = events.filter_map({ 96 | move |event| { 97 | let adapter = adapter.clone(); 98 | let uuids = uuids.to_vec(); 99 | async move { 100 | // trace!(?event); 101 | match event { 102 | CentralEvent::ServiceDataAdvertisement { id, service_data } => { 103 | on_peripheral_service_data(&adapter, &id, &uuids, service_data).await 104 | } 105 | _ => None, 106 | } 107 | } 108 | } 109 | }); 110 | 111 | Ok(stream) 112 | } 113 | 114 | /// TODO(#86): Support multiple adapters. 115 | async fn get_adapter() -> Result { 116 | let manager = Manager::new().await.or(Err(Error::Unavailable))?; 117 | manager 118 | .adapters() 119 | .await 120 | .or(Err(Error::Unavailable))? 121 | .into_iter() 122 | .nth(0) 123 | .ok_or(Error::PoweredOff) 124 | } 125 | 126 | async fn discover_properties( 127 | peripherals: Vec, 128 | ) -> Result, Error> { 129 | let mut result = vec![]; 130 | for peripheral in peripherals { 131 | let properties = peripheral 132 | .properties() 133 | .await 134 | .or(Err(Error::ConnectionFailed))?; 135 | trace!({ ?peripheral, ?properties }); 136 | if let Some(properties) = properties { 137 | result.push((peripheral, properties)); 138 | } 139 | } 140 | Ok(result) 141 | } 142 | 143 | #[instrument(level = Level::DEBUG, skip_all)] 144 | pub async fn list_fido_devices() -> Result, Error> { 145 | let adapter = get_adapter().await?; 146 | let peripherals: Vec = adapter 147 | .peripherals() 148 | .await 149 | .or(Err(Error::ConnectionFailed))? 150 | .into_iter() 151 | .filter(|p| { 152 | p.services() 153 | .iter() 154 | .find(|s| s.uuid == FIDO_PROFILE_UUID) 155 | .is_some() 156 | }) 157 | .collect(); 158 | let with_properties = discover_properties(peripherals) 159 | .await? 160 | .into_iter() 161 | .map(|(peripheral, properties)| FidoDevice { 162 | peripheral, 163 | properties, 164 | }) 165 | .collect(); 166 | Ok(with_properties) 167 | } 168 | 169 | pub async fn get_device(peripheral: Peripheral) -> Result, Error> { 170 | let Some(properties) = peripheral 171 | .properties() 172 | .await 173 | .or(Err(Error::ConnectionFailed))? 174 | else { 175 | return Ok(None); 176 | }; 177 | 178 | let device = FidoDevice { 179 | peripheral, 180 | properties, 181 | }; 182 | Ok(Some(device)) 183 | } 184 | 185 | pub async fn supported_fido_revisions( 186 | peripheral: &Peripheral, 187 | ) -> Result { 188 | let services = discover_services(peripheral).await?; 189 | let revision = peripheral 190 | .read(&services.service_revision_bitfield) 191 | .await 192 | .or(Err(Error::ConnectionFailed))?; 193 | let bitfield = revision.iter().next().ok_or(Error::OperationFailed)?; 194 | debug!(?revision, "Supported revision bitfield"); 195 | 196 | let supported = SupportedRevisions { 197 | u2fv11: bitfield & FidoRevision::U2fv11 as u8 != 0x00, 198 | u2fv12: bitfield & FidoRevision::U2fv12 as u8 != 0x00, 199 | v2: bitfield & FidoRevision::V2 as u8 != 0x00, 200 | }; 201 | info!(?supported, "Device reported supporting FIDO revisions"); 202 | Ok(supported) 203 | } 204 | 205 | /// Connect, discover FIDO services on this device, and 206 | /// select the FIDO revision to be used. 207 | pub async fn connect( 208 | peripheral: &Peripheral, 209 | revision: &FidoRevision, 210 | ) -> Result { 211 | peripheral 212 | .connect() 213 | .await 214 | .or(Err(Error::ConnectionFailed))?; 215 | peripheral 216 | .discover_services() 217 | .await 218 | .or(Err(Error::ConnectionFailed))?; 219 | let services = discover_services(peripheral).await?; 220 | Connection::new(peripheral, &services, revision).await 221 | } 222 | 223 | async fn discover_services(peripheral: &Peripheral) -> Result { 224 | let control_point_uuid = Uuid::parse_str(FIDO_CONTROL_POINT_UUID).unwrap(); 225 | let control_point = get_gatt_characteristic(peripheral, control_point_uuid)?; 226 | 227 | let control_point_length_uuid = Uuid::parse_str(FIDO_CONTROL_POINT_LENGTH_UUID).unwrap(); 228 | let control_point_length = get_gatt_characteristic(peripheral, control_point_length_uuid)?; 229 | 230 | let status_uuid = Uuid::parse_str(FIDO_STATUS_UUID).unwrap(); 231 | let status = get_gatt_characteristic(peripheral, status_uuid)?; 232 | 233 | let service_revision_bitfield_uuid = Uuid::parse_str(FIDO_REVISION_BITFIELD_UUID).unwrap(); 234 | let service_revision_bitfield = 235 | get_gatt_characteristic(peripheral, service_revision_bitfield_uuid)?; 236 | 237 | Ok(FidoEndpoints { 238 | control_point, 239 | control_point_length, 240 | status, 241 | service_revision_bitfield, 242 | }) 243 | } 244 | -------------------------------------------------------------------------------- /libwebauthn/src/transport/ble/btleplug/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod connection; 2 | pub mod device; 3 | pub mod error; 4 | pub mod gatt; 5 | pub mod manager; 6 | 7 | pub use connection::Connection; 8 | pub use device::FidoDevice; 9 | pub use error::Error; 10 | pub use manager::{ 11 | connect, list_fido_devices, start_discovery_for_service_data, supported_fido_revisions, 12 | }; 13 | -------------------------------------------------------------------------------- /libwebauthn/src/transport/ble/channel.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | use std::fmt::{Display, Formatter}; 3 | use std::time::Duration; 4 | 5 | use crate::fido::{FidoProtocol, FidoRevision}; 6 | use crate::proto::ctap1::apdu::{ApduRequest, ApduResponse}; 7 | use crate::proto::ctap2::cbor::{CborRequest, CborResponse}; 8 | use crate::proto::CtapError; 9 | use crate::transport::ble::btleplug; 10 | use crate::transport::channel::{AuthTokenData, Channel, ChannelStatus, Ctap2AuthTokenStore}; 11 | use crate::transport::device::SupportedProtocols; 12 | use crate::transport::error::{Error, TransportError}; 13 | use crate::UxUpdate; 14 | 15 | use super::btleplug::manager::SupportedRevisions; 16 | use super::btleplug::Connection; 17 | use super::framing::{BleCommand, BleFrame}; 18 | use super::BleDevice; 19 | 20 | use async_trait::async_trait; 21 | use tokio::sync::mpsc; 22 | use tracing::{debug, instrument, trace, Level}; 23 | 24 | #[derive(Debug)] 25 | pub struct BleChannel<'a> { 26 | status: ChannelStatus, 27 | device: &'a BleDevice, 28 | connection: Connection, 29 | revision: FidoRevision, 30 | auth_token_data: Option, 31 | tx: mpsc::Sender, 32 | } 33 | 34 | impl<'a> BleChannel<'a> { 35 | pub async fn new( 36 | device: &'a BleDevice, 37 | revisions: &SupportedRevisions, 38 | tx: mpsc::Sender, 39 | ) -> Result, Error> { 40 | let revision = revisions 41 | .select_protocol(FidoProtocol::U2F) 42 | .ok_or(Error::Transport(TransportError::NegotiationFailed))?; 43 | let connection = btleplug::connect(&device.btleplug_device.peripheral, &revision) 44 | .await 45 | .or(Err(Error::Transport(TransportError::ConnectionFailed)))?; 46 | let channel = BleChannel { 47 | status: ChannelStatus::Ready, 48 | device, 49 | connection, 50 | revision, 51 | auth_token_data: None, 52 | tx, 53 | }; 54 | channel 55 | .connection 56 | .subscribe() 57 | .await 58 | .or(Err(Error::Transport(TransportError::TransportUnavailable)))?; 59 | Ok(channel) 60 | } 61 | } 62 | 63 | impl Display for BleChannel<'_> { 64 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 65 | self.device.fmt(f) 66 | } 67 | } 68 | 69 | #[async_trait] 70 | impl<'a> Channel for BleChannel<'a> { 71 | async fn supported_protocols(&self) -> Result { 72 | Ok(self.revision.into()) 73 | } 74 | 75 | async fn status(&self) -> ChannelStatus { 76 | self.status 77 | } 78 | 79 | async fn close(&mut self) { 80 | let _x = self.device; 81 | todo!() 82 | } 83 | 84 | #[instrument(level = Level::DEBUG, skip_all)] 85 | async fn apdu_send(&self, request: &ApduRequest, _timeout: Duration) -> Result<(), Error> { 86 | debug!({rev = ?self.revision}, "Sending APDU request"); 87 | trace!(?request); 88 | 89 | let request_apdu_packet = request.raw_long().or(Err(TransportError::InvalidFraming))?; 90 | let request_frame = BleFrame::new(BleCommand::Msg, &request_apdu_packet); 91 | self.connection 92 | .frame_send(&request_frame) 93 | .await 94 | .or(Err(Error::Transport(TransportError::ConnectionFailed)))?; 95 | Ok(()) 96 | } 97 | 98 | #[instrument(level = Level::DEBUG, skip_all)] 99 | async fn apdu_recv(&self, _timeout: Duration) -> Result { 100 | let response_frame = self 101 | .connection 102 | .frame_recv() 103 | .await 104 | .or(Err(Error::Transport(TransportError::ConnectionFailed)))?; 105 | match response_frame.cmd { 106 | BleCommand::Error => return Err(Error::Transport(TransportError::InvalidFraming)), // Encapsulation layer error 107 | BleCommand::Cancel => return Err(Error::Ctap(CtapError::KeepAliveCancel)), 108 | BleCommand::Keepalive | BleCommand::Ping => return Err(Error::Ctap(CtapError::Other)), // Unexpected 109 | BleCommand::Msg => {} 110 | } 111 | let response_apdu_packet = &response_frame.data; 112 | let response_apdu: ApduResponse = response_apdu_packet 113 | .try_into() 114 | .or(Err(TransportError::InvalidFraming))?; 115 | 116 | debug!("Received APDU response"); 117 | trace!(?response_apdu); 118 | Ok(response_apdu) 119 | } 120 | 121 | #[instrument(level = Level::DEBUG, skip_all)] 122 | async fn cbor_send( 123 | &mut self, 124 | request: &CborRequest, 125 | _timeout: std::time::Duration, 126 | ) -> Result<(), Error> { 127 | debug!("Sending CBOR request"); 128 | trace!(?request); 129 | 130 | let cbor_request = request 131 | .raw_long() 132 | .map_err(|e| TransportError::IoError(e.kind()))?; 133 | let request_frame = BleFrame::new(BleCommand::Msg, &cbor_request); 134 | self.connection 135 | .frame_send(&request_frame) 136 | .await 137 | .or(Err(Error::Transport(TransportError::ConnectionFailed)))?; 138 | Ok(()) 139 | } 140 | 141 | #[instrument(level = Level::DEBUG, skip_all)] 142 | async fn cbor_recv(&mut self, _timeout: std::time::Duration) -> Result { 143 | let response_frame = self 144 | .connection 145 | .frame_recv() 146 | .await 147 | .or(Err(Error::Transport(TransportError::ConnectionFailed)))?; 148 | match response_frame.cmd { 149 | BleCommand::Error => return Err(Error::Transport(TransportError::InvalidFraming)), // Encapsulation layer error 150 | BleCommand::Cancel => return Err(Error::Ctap(CtapError::KeepAliveCancel)), 151 | BleCommand::Keepalive | BleCommand::Ping => return Err(Error::Ctap(CtapError::Other)), // Unexpected 152 | BleCommand::Msg => {} 153 | } 154 | let cbor_response_packet = &response_frame.data; 155 | let cbor_response: CborResponse = cbor_response_packet 156 | .try_into() 157 | .or(Err(TransportError::InvalidFraming))?; 158 | 159 | debug!("Received CBOR response"); 160 | trace!(?cbor_response); 161 | Ok(cbor_response) 162 | } 163 | 164 | fn get_state_sender(&self) -> &mpsc::Sender { 165 | &self.tx 166 | } 167 | } 168 | 169 | impl Ctap2AuthTokenStore for BleChannel<'_> { 170 | fn store_auth_data(&mut self, auth_token_data: AuthTokenData) { 171 | self.auth_token_data = Some(auth_token_data); 172 | } 173 | 174 | fn get_auth_data(&self) -> Option<&AuthTokenData> { 175 | self.auth_token_data.as_ref() 176 | } 177 | 178 | fn clear_uv_auth_token_store(&mut self) { 179 | self.auth_token_data = None; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /libwebauthn/src/transport/ble/device.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use ::btleplug::api::Peripheral; 4 | use async_trait::async_trait; 5 | use hex::ToHex; 6 | use tokio::sync::mpsc; 7 | use tracing::{info, instrument}; 8 | 9 | use crate::transport::device::Device; 10 | use crate::transport::error::{Error, TransportError}; 11 | use crate::UxUpdate; 12 | 13 | use super::btleplug::manager::SupportedRevisions; 14 | use super::btleplug::{supported_fido_revisions, FidoDevice as BtleplugFidoDevice}; 15 | 16 | use super::channel::BleChannel; 17 | use super::{btleplug, Ble}; 18 | 19 | #[instrument] 20 | pub async fn list_devices() -> Result, Error> { 21 | let devices: Vec<_> = btleplug::list_fido_devices() 22 | .await 23 | .or(Err(Error::Transport(TransportError::TransportUnavailable)))? 24 | .iter() 25 | .map(|bluez_device| bluez_device.into()) 26 | .collect(); 27 | info!({ count = devices.len() }, "Listing available BLE devices"); 28 | Ok(devices) 29 | } 30 | 31 | #[derive(Debug, Clone)] 32 | pub struct BleDevice { 33 | pub btleplug_device: BtleplugFidoDevice, 34 | pub revisions: Option, 35 | } 36 | 37 | impl BleDevice { 38 | pub fn alias(&self) -> String { 39 | match &self.btleplug_device.properties.local_name { 40 | Some(local_name) => local_name.clone(), 41 | None => self.btleplug_device.properties.address.encode_hex(), 42 | } 43 | } 44 | 45 | pub async fn is_connected(&self) -> bool { 46 | self.btleplug_device 47 | .peripheral 48 | .is_connected() 49 | .await 50 | .unwrap_or(false) 51 | } 52 | } 53 | 54 | impl From<&BtleplugFidoDevice> for BleDevice { 55 | fn from(btleplug_device: &BtleplugFidoDevice) -> Self { 56 | Self { 57 | btleplug_device: btleplug_device.clone(), 58 | revisions: None, 59 | } 60 | } 61 | } 62 | 63 | impl Into for &BleDevice { 64 | fn into(self) -> BtleplugFidoDevice { 65 | self.btleplug_device.clone() 66 | } 67 | } 68 | 69 | impl fmt::Display for BleDevice { 70 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 71 | write!(f, "{}", self.alias()) 72 | } 73 | } 74 | 75 | #[async_trait] 76 | impl<'d> Device<'d, Ble, BleChannel<'d>> for BleDevice { 77 | async fn channel(&'d mut self) -> Result<(BleChannel<'d>, mpsc::Receiver), Error> { 78 | let revisions = self.supported_revisions().await?; 79 | let (send, recv) = mpsc::channel(1); 80 | let channel = BleChannel::new(self, &revisions, send).await?; 81 | Ok((channel, recv)) 82 | } 83 | 84 | // #[instrument(skip_all)] 85 | // async fn supported_protocols(&mut self) -> Result { 86 | // let revisions = self.supported_revisions().await?; 87 | // Ok(revisions.into()) 88 | // } 89 | } 90 | 91 | impl BleDevice { 92 | async fn supported_revisions(&mut self) -> Result { 93 | let revisions = match self.revisions { 94 | None => { 95 | let revisions = supported_fido_revisions(&self.btleplug_device.peripheral) 96 | .await 97 | .or(Err(Error::Transport(TransportError::NegotiationFailed)))?; 98 | self.revisions = Some(revisions); 99 | revisions 100 | } 101 | Some(revisions) => revisions, 102 | }; 103 | Ok(revisions) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /libwebauthn/src/transport/ble/framing.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | use std::io::{Cursor as IOCursor, Error as IOError, ErrorKind as IOErrorKind}; 3 | 4 | use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; 5 | use num_enum::{IntoPrimitive, TryFromPrimitive}; 6 | 7 | const INITIAL_FRAGMENT_HEADER_LENGTH: usize = 3; // 1B op, 2B length 8 | const INITIAL_FRAGMENT_MIN_LENGTH: usize = INITIAL_FRAGMENT_HEADER_LENGTH; 9 | const CONT_FRAGMENT_HEADER_LENGTH: usize = 1; 10 | const CONT_FRAGMENT_MIN_LENGTH: usize = CONT_FRAGMENT_HEADER_LENGTH; // 1B header, 1B data 11 | 12 | // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#ble-constants 13 | #[derive(Debug, IntoPrimitive, TryFromPrimitive, Copy, Clone, PartialEq)] 14 | #[repr(u8)] 15 | pub enum BleCommand { 16 | Ping = 0x81, 17 | Keepalive = 0x82, 18 | Msg = 0x83, 19 | Cancel = 0xBE, 20 | Error = 0xBF, 21 | } 22 | 23 | #[derive(Debug, Clone)] 24 | pub struct BleFrame { 25 | pub cmd: BleCommand, 26 | pub data: Vec, 27 | } 28 | 29 | impl BleFrame { 30 | pub fn new(cmd: BleCommand, data: &[u8]) -> Self { 31 | Self { 32 | data: Vec::from(data), 33 | cmd, 34 | } 35 | } 36 | 37 | // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#ble-framing-fragmentation 38 | pub fn fragments(&self, max_fragment_length: usize) -> Result>, IOError> { 39 | if max_fragment_length < 4 { 40 | return Err(IOError::new( 41 | IOErrorKind::InvalidData, 42 | format!( 43 | "Desired maximum fragment length is unsupported: {}", 44 | max_fragment_length 45 | ), 46 | )); 47 | } 48 | 49 | let length = self.data.len() as u16; 50 | let mut message = self.data.as_slice().into_iter().cloned().peekable(); 51 | let mut fragments = vec![]; 52 | 53 | // Initial fragment 54 | let cmd: u8 = self.cmd.into(); 55 | let mut fragment = vec![cmd]; 56 | fragment.write_u16::(length)?; 57 | let mut chunk: Vec = message 58 | .by_ref() 59 | .take(max_fragment_length - INITIAL_FRAGMENT_HEADER_LENGTH) 60 | .collect(); 61 | fragment.append(&mut chunk); 62 | fragments.push(fragment); 63 | 64 | // Sequence fragments 65 | let mut seq: u8 = 0; 66 | while message.peek().is_some() { 67 | let mut fragment = vec![seq]; 68 | let mut chunk: Vec = message 69 | .by_ref() 70 | .take(max_fragment_length - CONT_FRAGMENT_HEADER_LENGTH) 71 | .collect(); 72 | fragment.append(&mut chunk); 73 | fragments.push(fragment); 74 | seq += 1; 75 | } 76 | 77 | Ok(fragments) 78 | } 79 | } 80 | 81 | #[derive(Debug, PartialEq)] 82 | pub enum BleFrameParserResult { 83 | MoreFragmentsExpected, 84 | Done, 85 | } 86 | 87 | #[derive(Debug)] 88 | pub struct BleFrameParser { 89 | fragments: Vec>, 90 | } 91 | 92 | impl BleFrameParser { 93 | pub fn new() -> Self { 94 | Self { fragments: vec![] } 95 | } 96 | 97 | pub fn update(&mut self, fragment: &[u8]) -> Result { 98 | if (self.fragments.len() == 0 && fragment.len() < INITIAL_FRAGMENT_MIN_LENGTH) 99 | || fragment.len() < CONT_FRAGMENT_MIN_LENGTH 100 | { 101 | return Err(IOError::new( 102 | IOErrorKind::InvalidInput, 103 | "Fragment length is invalid. 3 bytes are required for an initial fragment, 2 bytes for each continuation fragment." 104 | )); 105 | } 106 | 107 | self.fragments.push(Vec::from(fragment)); 108 | return if self.more_fragments_needed() { 109 | Ok(BleFrameParserResult::MoreFragmentsExpected) 110 | } else { 111 | Ok(BleFrameParserResult::Done) 112 | }; 113 | } 114 | 115 | pub fn frame(&self) -> Result { 116 | if self.more_fragments_needed() { 117 | return Err(IOError::new( 118 | IOErrorKind::InvalidData, 119 | "Frame is not yet complete, more fragments need to be ingested.", 120 | )); 121 | } 122 | 123 | let cmd = self.fragments[0][0].try_into().or(Err(IOError::new( 124 | IOErrorKind::InvalidData, 125 | format!("Invalid BLE frame command: {:x}", self.fragments[0][0]), 126 | )))?; 127 | let mut data = vec![]; 128 | data.extend(&self.fragments[0][INITIAL_FRAGMENT_HEADER_LENGTH..self.fragments[0].len()]); 129 | for cont_fragment in &self.fragments[1..self.fragments.len()] { 130 | data.extend_from_slice( 131 | &cont_fragment[CONT_FRAGMENT_HEADER_LENGTH..cont_fragment.len()], 132 | ); 133 | } 134 | 135 | Ok(BleFrame::new(cmd, &data)) 136 | } 137 | 138 | pub fn reset(&mut self) { 139 | self.fragments = vec![]; 140 | } 141 | 142 | fn more_fragments_needed(&self) -> bool { 143 | if self.fragments.is_empty() { 144 | return true; 145 | } 146 | 147 | self.expected_bytes().unwrap() > self.data_len() 148 | } 149 | 150 | fn expected_bytes(&self) -> Option { 151 | if self.fragments.is_empty() { 152 | return None; 153 | } 154 | 155 | let mut cursor = IOCursor::new(vec![self.fragments[0][1], self.fragments[0][2]]); 156 | Some(cursor.read_u16::().unwrap() as usize) 157 | } 158 | 159 | fn data_len(&self) -> usize { 160 | if self.fragments.is_empty() { 161 | return 0; 162 | } 163 | 164 | let mut data_len = self.fragments[0].len() - INITIAL_FRAGMENT_HEADER_LENGTH; 165 | for cont_fragment in &self.fragments[1..self.fragments.len()] { 166 | data_len += cont_fragment.len() - CONT_FRAGMENT_HEADER_LENGTH; 167 | } 168 | data_len 169 | } 170 | } 171 | 172 | #[cfg(test)] 173 | mod tests { 174 | use crate::transport::ble::framing::{ 175 | BleCommand, BleFrame, BleFrameParser, BleFrameParserResult, 176 | }; 177 | 178 | #[test] 179 | fn encode_single_fragment() { 180 | let frame = BleFrame::new(BleCommand::Msg, &[0x0A, 0x0B, 0x0C, 0x0D]); 181 | let expected: Vec> = vec![vec![0x83, 0x00, 0x04, 0x0A, 0x0B, 0x0C, 0x0D]]; 182 | assert_eq!(frame.fragments(8).unwrap(), expected) 183 | } 184 | 185 | #[test] 186 | fn encode_multiple_frames() { 187 | let frame = BleFrame::new( 188 | BleCommand::Msg, 189 | &[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08], 190 | ); 191 | let expected: Vec> = vec![ 192 | vec![0x83, 0x00, 0x08, 0x01], 193 | vec![0x00, 0x02, 0x03, 0x04], 194 | vec![0x01, 0x05, 0x06, 0x07], 195 | vec![0x02, 0x08], 196 | ]; 197 | assert_eq!(frame.fragments(4).unwrap(), expected) 198 | } 199 | 200 | #[test] 201 | fn parse_single_fragment() { 202 | let mut parser = BleFrameParser::new(); 203 | assert_eq!( 204 | parser 205 | .update(&vec![0x83, 0x00, 0x04, 0x0A, 0x0B, 0x0C, 0x0D]) 206 | .unwrap(), 207 | BleFrameParserResult::Done 208 | ); 209 | assert_eq!(parser.frame().unwrap().data, vec![0x0A, 0x0B, 0x0C, 0x0D]); 210 | } 211 | 212 | #[test] 213 | fn parse_multiple_fragments() { 214 | let mut parser = BleFrameParser::new(); 215 | assert_eq!( 216 | parser.update(&vec![0x83, 0x00, 0x05, 0x0A]).unwrap(), 217 | BleFrameParserResult::MoreFragmentsExpected 218 | ); 219 | assert_eq!( 220 | parser.update(&vec![0x00, 0x0B, 0x0C, 0x0D]).unwrap(), 221 | BleFrameParserResult::MoreFragmentsExpected 222 | ); 223 | assert_eq!( 224 | parser.update(&vec![0x01, 0x0E]).unwrap(), 225 | BleFrameParserResult::Done 226 | ); 227 | assert_eq!( 228 | parser.frame().unwrap().data, 229 | vec![0x0A, 0x0B, 0x0C, 0x0D, 0x0E] 230 | ); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /libwebauthn/src/transport/ble/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | pub mod btleplug; 4 | pub mod channel; 5 | pub mod device; 6 | pub mod framing; 7 | 8 | pub use device::list_devices; 9 | pub use device::BleDevice; 10 | 11 | use super::Transport; 12 | 13 | pub struct Ble {} 14 | impl Transport for Ble {} 15 | unsafe impl Send for Ble {} 16 | unsafe impl Sync for Ble {} 17 | 18 | impl Display for Ble { 19 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 20 | write!(f, "Ble") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /libwebauthn/src/transport/cable/advertisement.rs: -------------------------------------------------------------------------------- 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 | } 91 | -------------------------------------------------------------------------------- /libwebauthn/src/transport/cable/channel.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | use std::time::Duration; 3 | 4 | use async_trait::async_trait; 5 | use tokio::sync::mpsc; 6 | use tokio::{task, time}; 7 | use tracing::error; 8 | 9 | use crate::proto::{ 10 | ctap1::apdu::{ApduRequest, ApduResponse}, 11 | ctap2::cbor::{CborRequest, CborResponse}, 12 | }; 13 | use crate::transport::error::{Error, TransportError}; 14 | use crate::transport::AuthTokenData; 15 | use crate::transport::{ 16 | channel::ChannelStatus, device::SupportedProtocols, Channel, Ctap2AuthTokenStore, 17 | }; 18 | use crate::UxUpdate; 19 | 20 | use super::known_devices::CableKnownDevice; 21 | use super::qr_code_device::CableQrCodeDevice; 22 | 23 | #[derive(Debug)] 24 | pub enum CableChannelDevice<'d> { 25 | QrCode(&'d CableQrCodeDevice), 26 | Known(&'d CableKnownDevice), 27 | } 28 | 29 | #[derive(Debug)] 30 | pub struct CableChannel { 31 | /// The WebSocket stream used for communication. 32 | // pub(crate) ws_stream: WebSocketStream>, 33 | 34 | /// The noise state used for encryption over the WebSocket stream. 35 | // pub(crate) noise_state: TransportState, 36 | 37 | /// The device that this channel is connected to. 38 | pub(crate) handle_connection: task::JoinHandle<()>, 39 | pub(crate) cbor_sender: mpsc::Sender, 40 | pub(crate) cbor_receiver: mpsc::Receiver, 41 | pub(crate) tx: mpsc::Sender, 42 | } 43 | 44 | impl Display for CableChannel { 45 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 46 | write!(f, "CableChannel") 47 | } 48 | } 49 | 50 | impl Drop for CableChannel { 51 | fn drop(&mut self) { 52 | self.handle_connection.abort(); 53 | } 54 | } 55 | 56 | #[async_trait] 57 | impl<'d> Channel for CableChannel { 58 | async fn supported_protocols(&self) -> Result { 59 | Ok(SupportedProtocols::fido2_only()) 60 | } 61 | 62 | async fn status(&self) -> ChannelStatus { 63 | match self.handle_connection.is_finished() { 64 | true => ChannelStatus::Closed, 65 | false => ChannelStatus::Ready, 66 | } 67 | } 68 | 69 | async fn close(&mut self) { 70 | // TODO Send CableTunnelMessageType#Shutdown and drop the connection 71 | } 72 | 73 | async fn apdu_send(&self, _request: &ApduRequest, _timeout: Duration) -> Result<(), Error> { 74 | error!("APDU send not supported in caBLE transport"); 75 | Err(Error::Transport(TransportError::TransportUnavailable)) 76 | } 77 | 78 | async fn apdu_recv(&self, _timeout: Duration) -> Result { 79 | error!("APDU recv not supported in caBLE transport"); 80 | Err(Error::Transport(TransportError::TransportUnavailable)) 81 | } 82 | 83 | async fn cbor_send(&mut self, request: &CborRequest, timeout: Duration) -> Result<(), Error> { 84 | match time::timeout(timeout, self.cbor_sender.send(request.clone())).await { 85 | Ok(Ok(_)) => Ok(()), 86 | Ok(Err(error)) => { 87 | error!(%error, "CBOR request send failure"); 88 | Err(Error::Transport(TransportError::TransportUnavailable)) 89 | } 90 | Err(elapsed) => { 91 | error!({ %elapsed, ?timeout }, "CBOR request send timeout"); 92 | Err(Error::Transport(TransportError::Timeout)) 93 | } 94 | } 95 | } 96 | 97 | async fn cbor_recv(&mut self, timeout: Duration) -> Result { 98 | match time::timeout(timeout, self.cbor_receiver.recv()).await { 99 | Ok(Some(response)) => Ok(response), 100 | Ok(None) => Err(Error::Transport(TransportError::TransportUnavailable)), 101 | Err(elapsed) => { 102 | error!({ %elapsed, ?timeout }, "CBOR response recv timeout"); 103 | Err(Error::Transport(TransportError::Timeout)) 104 | } 105 | } 106 | } 107 | 108 | fn get_state_sender(&self) -> &mpsc::Sender { 109 | &self.tx 110 | } 111 | 112 | fn supports_preflight() -> bool { 113 | // Disable pre-flight requests, as hybrid transport authenticators do not support silent requests. 114 | false 115 | } 116 | } 117 | 118 | impl<'d> Ctap2AuthTokenStore for CableChannel { 119 | fn store_auth_data(&mut self, _auth_token_data: AuthTokenData) {} 120 | 121 | fn get_auth_data(&self) -> Option<&AuthTokenData> { 122 | None 123 | } 124 | 125 | fn clear_uv_auth_token_store(&mut self) {} 126 | } 127 | -------------------------------------------------------------------------------- /libwebauthn/src/transport/cable/crypto.rs: -------------------------------------------------------------------------------- 1 | use aes::cipher::{BlockDecrypt, KeyInit}; 2 | use aes::{Aes256, Block}; 3 | use hkdf::Hkdf; 4 | use sha2::Sha256; 5 | use tracing::{instrument, warn}; 6 | 7 | use crate::pin::hmac_sha256; 8 | 9 | pub enum KeyPurpose { 10 | EIDKey = 1, 11 | TunnelID = 2, 12 | PSK = 3, 13 | } 14 | 15 | 16 | pub fn derive(secret: &[u8], salt: Option<&[u8]>, purpose: KeyPurpose) -> [u8; 64] { 17 | let mut purpose32 = [0u8; 4]; 18 | purpose32[0] = purpose as u8; 19 | 20 | let hkdf = Hkdf::::new(salt, secret); 21 | let mut output = [0u8; 64]; 22 | hkdf.expand(&purpose32, &mut output).unwrap(); 23 | output 24 | } 25 | 26 | fn reserved_bits_are_zero(plaintext: &[u8]) -> bool { 27 | plaintext[0] == 0 28 | } 29 | 30 | #[instrument] 31 | pub fn trial_decrypt_advert(eid_key: &[u8], candidate_advert: &[u8]) -> Option<[u8; 16]> { 32 | if candidate_advert.len() != 20 { 33 | warn!("candidate advert is not 20 bytes"); 34 | return None; 35 | } 36 | 37 | if eid_key.len() != 64 { 38 | warn!("EID key is not 64 bytes"); 39 | return None; 40 | } 41 | 42 | let expected_tag = hmac_sha256(&eid_key[32..], &candidate_advert[..16]); 43 | if expected_tag[..4] != candidate_advert[16..] { 44 | warn!({ expected = ?expected_tag[..4], actual = ?candidate_advert[16..] }, 45 | "candidate advert HMAC tag does not match"); 46 | return None; 47 | } 48 | 49 | let cipher = Aes256::new_from_slice(&eid_key[..32]).unwrap(); 50 | let mut block = Block::clone_from_slice(&candidate_advert[..16]); 51 | cipher.decrypt_block(&mut block); 52 | 53 | if !reserved_bits_are_zero(&block) { 54 | warn!("reserved bits are not zero"); 55 | return None; 56 | } 57 | 58 | let mut plaintext = [0u8; 16]; 59 | plaintext.copy_from_slice(&block); 60 | Some(plaintext) 61 | } 62 | 63 | #[cfg(test)] 64 | mod tests { 65 | use super::derive; 66 | use super::KeyPurpose; 67 | 68 | #[test] 69 | fn derive_eidkey_nosalt() { 70 | let input: [u8; 16] = hex::decode("00112233445566778899aabbccddeeff").unwrap().try_into().unwrap(); 71 | let output = derive(&input, None, KeyPurpose::EIDKey).to_vec(); 72 | let expected = hex::decode("efafab5b2c84a11c80e3ad0770353138b414a859ccd3afcc99e3d3250dba65084ede8e38e75432617c0ccae1ffe5d8143df0db0cd6d296f489419cd6411ee505").unwrap(); 73 | assert_eq!(output, expected); 74 | } 75 | 76 | #[test] 77 | fn derive_eidkey_salt() { 78 | let input: [u8; 16] = hex::decode("00112233445566778899aabbccddeeff").unwrap().try_into().unwrap(); 79 | let salt = hex::decode("ffeeddccbbaa998877665544332211").unwrap(); 80 | let output = derive(&input, Some(&salt), KeyPurpose::EIDKey).to_vec(); 81 | let expected = hex::decode("168cf3dd220a7907f8bac30f559be92a3b6d937fe5594beeaf1e50e35976b7d654dd550e22ae4c801b9d1cdbf0d2b1472daa1328661eb889acae3023b7ffa509").unwrap(); 82 | assert_eq!(output, expected); 83 | } 84 | 85 | 86 | } -------------------------------------------------------------------------------- /libwebauthn/src/transport/cable/digit_encode.rs: -------------------------------------------------------------------------------- 1 | const CHUNK_SIZE: usize = 7; 2 | const CHUNK_DIGITS: usize = 17; 3 | const ZEROS: &str = "00000000000000000"; 4 | 5 | /// The number of digits needed to encode each length of trailing data from 6 bytes down to zero, 6 | /// i.e. it’s 15, 13, 10, 8, 5, 3, 0 written in hex. 7 | const PARTIAL_CHUNK_DIGITS: usize = 0x0fda8530; 8 | 9 | pub fn digit_encode(input: &[u8]) -> String { 10 | let mut output = String::new(); 11 | let mut input = input; 12 | while input.len() >= CHUNK_SIZE { 13 | let mut chunk = [0u8; 8]; 14 | chunk[..CHUNK_SIZE].copy_from_slice(&input[..CHUNK_SIZE]); 15 | let v = u64::from_le_bytes(chunk); 16 | let v = v.to_string(); 17 | output.push_str(&ZEROS[..CHUNK_DIGITS - v.len()]); 18 | output.push_str(&v); 19 | input = &input[CHUNK_SIZE..]; 20 | } 21 | if !input.is_empty() { 22 | let digits = 0x0F & (PARTIAL_CHUNK_DIGITS >> (4 * input.len())); 23 | let mut chunk = [0u8; 8]; 24 | chunk[..input.len()].copy_from_slice(input); 25 | let v = u64::from_le_bytes(chunk); 26 | let v = v.to_string(); 27 | output.push_str(&ZEROS[..digits - v.len()]); 28 | output.push_str(&v); 29 | } 30 | output 31 | } 32 | 33 | #[cfg(test)] 34 | mod tests { 35 | use super::digit_encode; 36 | 37 | #[test] 38 | fn test_digit_encode() { 39 | assert_eq!(digit_encode(b"hello world"), "335311851610699281684828783") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /libwebauthn/src/transport/cable/known_devices.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fmt::{Debug, Display}; 3 | use std::sync::Arc; 4 | 5 | use crate::transport::cable::advertisement::await_advertisement; 6 | use crate::transport::cable::crypto::{derive, KeyPurpose}; 7 | use crate::transport::error::Error; 8 | use crate::transport::Device; 9 | use crate::webauthn::TransportError; 10 | use crate::UxUpdate; 11 | 12 | use async_trait::async_trait; 13 | use futures::lock::Mutex; 14 | use serde::Serialize; 15 | use serde_bytes::ByteBuf; 16 | use serde_indexed::SerializeIndexed; 17 | use tokio::sync::mpsc; 18 | use tracing::{debug, trace}; 19 | 20 | use super::channel::CableChannel; 21 | use super::tunnel::{self, CableLinkingInfo}; 22 | use super::Cable; 23 | 24 | #[async_trait] 25 | pub trait CableKnownDeviceInfoStore: Debug + Send + Sync { 26 | /// Called whenever a known device should be added or updated. 27 | async fn put_known_device(&self, device_id: &CableKnownDeviceId, device: &CableKnownDeviceInfo); 28 | /// Called whenever a known device becomes permanently unavailable. 29 | async fn delete_known_device(&self, device_id: &CableKnownDeviceId); 30 | } 31 | 32 | /// An in-memory store for testing purposes. 33 | #[derive(Debug, Default, Clone)] 34 | pub struct EphemeralDeviceInfoStore { 35 | pub known_devices: Arc>>, 36 | } 37 | 38 | impl EphemeralDeviceInfoStore { 39 | pub fn new() -> Self { 40 | Self { 41 | known_devices: Arc::new(Mutex::new(HashMap::new())), 42 | } 43 | } 44 | 45 | pub async fn list_all(&self) -> Vec<(CableKnownDeviceId, CableKnownDeviceInfo)> { 46 | debug!("Listing all known devices"); 47 | let known_devices = self.known_devices.lock().await; 48 | known_devices 49 | .iter() 50 | .map(|(id, info)| (id.clone(), info.clone())) 51 | .collect() 52 | } 53 | } 54 | 55 | unsafe impl Send for EphemeralDeviceInfoStore {} 56 | 57 | #[async_trait] 58 | impl CableKnownDeviceInfoStore for EphemeralDeviceInfoStore { 59 | async fn put_known_device( 60 | &self, 61 | device_id: &CableKnownDeviceId, 62 | device: &CableKnownDeviceInfo, 63 | ) { 64 | debug!(?device_id, "Inserting or updating known device"); 65 | trace!(?device); 66 | let mut known_devices = self.known_devices.lock().await; 67 | known_devices.insert(device_id.clone(), device.clone()); 68 | } 69 | 70 | async fn delete_known_device(&self, device_id: &CableKnownDeviceId) { 71 | debug!(?device_id, "Deleting known device"); 72 | let mut known_devices = self.known_devices.lock().await; 73 | known_devices.remove(device_id); 74 | } 75 | } 76 | 77 | pub type CableKnownDeviceId = String; 78 | 79 | #[derive(Debug, Clone)] 80 | pub struct CableKnownDeviceInfo { 81 | pub contact_id: Vec, 82 | pub link_id: [u8; 8], 83 | pub link_secret: [u8; 32], 84 | pub public_key: [u8; 65], 85 | pub name: String, 86 | pub tunnel_domain: String, 87 | } 88 | 89 | impl From<&CableLinkingInfo> for CableKnownDeviceId { 90 | fn from(linking_info: &CableLinkingInfo) -> Self { 91 | hex::encode(&linking_info.authenticator_public_key) 92 | } 93 | } 94 | 95 | impl CableKnownDeviceInfo { 96 | pub(crate) fn new(tunnel_domain: &str, linking_info: &CableLinkingInfo) -> Result { 97 | let info = Self { 98 | contact_id: linking_info.contact_id.to_vec(), 99 | link_id: linking_info 100 | .link_id 101 | .clone() 102 | .try_into() 103 | .map_err(|_| Error::Transport(TransportError::InvalidFraming))?, 104 | link_secret: linking_info 105 | .link_secret 106 | .clone() 107 | .try_into() 108 | .map_err(|_| Error::Transport(TransportError::InvalidFraming))?, 109 | public_key: linking_info 110 | .authenticator_public_key 111 | .clone() 112 | .try_into() 113 | .map_err(|_| Error::Transport(TransportError::InvalidFraming))?, 114 | name: linking_info.authenticator_name.clone(), 115 | tunnel_domain: tunnel_domain.to_string(), 116 | }; 117 | Ok(info) 118 | } 119 | } 120 | 121 | #[derive(Debug)] 122 | pub struct CableKnownDevice { 123 | pub hint: ClientPayloadHint, 124 | pub device_info: CableKnownDeviceInfo, 125 | pub(crate) store: Arc, 126 | } 127 | 128 | impl Display for CableKnownDevice { 129 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 130 | write!( 131 | f, 132 | "{} ({})", 133 | &self.device_info.name, 134 | hex::encode(&self.device_info.public_key) 135 | ) 136 | } 137 | } 138 | 139 | unsafe impl Send for CableKnownDevice {} 140 | unsafe impl Sync for CableKnownDevice {} 141 | 142 | impl CableKnownDevice { 143 | pub async fn new( 144 | hint: ClientPayloadHint, 145 | device_info: &CableKnownDeviceInfo, 146 | store: Arc, 147 | ) -> Result { 148 | let device = CableKnownDevice { 149 | hint, 150 | device_info: device_info.clone(), 151 | store: store, 152 | }; 153 | Ok(device) 154 | } 155 | } 156 | 157 | #[async_trait] 158 | impl<'d> Device<'d, Cable, CableChannel> for CableKnownDevice { 159 | async fn channel(&'d mut self) -> Result<(CableChannel, mpsc::Receiver), Error> { 160 | debug!(?self.device_info.tunnel_domain, "Creating channel to tunnel server"); 161 | 162 | let (client_nonce, client_payload) = 163 | construct_client_payload(self.hint, self.device_info.link_id); 164 | let contact_id = base64_url::encode(&self.device_info.contact_id); 165 | 166 | let connection_type = tunnel::CableTunnelConnectionType::KnownDevice { 167 | contact_id: contact_id, 168 | authenticator_public_key: self.device_info.public_key.to_vec(), 169 | client_payload, 170 | }; 171 | let mut ws_stream = 172 | tunnel::connect(&self.device_info.tunnel_domain, &connection_type).await?; 173 | 174 | let eid_key: [u8; 64] = derive( 175 | &self.device_info.link_secret, 176 | Some(&client_nonce), 177 | KeyPurpose::EIDKey, 178 | ); 179 | 180 | let (_device, advert) = await_advertisement(&eid_key).await?; 181 | 182 | let mut psk: [u8; 32] = [0u8; 32]; 183 | psk.copy_from_slice( 184 | &derive( 185 | &self.device_info.link_secret, 186 | Some(&advert.plaintext), 187 | KeyPurpose::PSK, 188 | )[..32], 189 | ); 190 | 191 | let noise_state = tunnel::do_handshake(&mut ws_stream, psk, &connection_type).await?; 192 | 193 | tunnel::channel( 194 | &connection_type, 195 | noise_state, 196 | &self.device_info.tunnel_domain, 197 | &Some(self.store.clone()), 198 | ws_stream, 199 | ) 200 | .await 201 | } 202 | } 203 | 204 | type ClientNonce = [u8; 16]; 205 | 206 | // Key 3: either the string “ga” to hint that a getAssertion will follow, or “mc” to hint that a makeCredential will follow. 207 | #[derive(Clone, Debug, SerializeIndexed)] 208 | #[serde(offset = 1)] 209 | pub struct ClientPayload { 210 | pub link_id: ByteBuf, 211 | pub client_nonce: ByteBuf, 212 | pub hint: ClientPayloadHint, 213 | } 214 | 215 | #[derive(Debug, Copy, Clone, Serialize, PartialEq)] 216 | pub enum ClientPayloadHint { 217 | #[serde(rename = "ga")] 218 | GetAssertion, 219 | #[serde(rename = "mc")] 220 | MakeCredential, 221 | } 222 | 223 | fn construct_client_payload( 224 | hint: ClientPayloadHint, 225 | link_id: [u8; 8], 226 | ) -> (ClientNonce, ClientPayload) { 227 | let client_nonce = rand::random::(); 228 | let client_payload = { 229 | ClientPayload { 230 | link_id: ByteBuf::from(link_id), 231 | client_nonce: ByteBuf::from(client_nonce), 232 | hint, 233 | } 234 | }; 235 | (client_nonce, client_payload) 236 | } 237 | 238 | #[cfg(test)] 239 | mod tests { 240 | use crate::transport::cable::tunnel::KNOWN_TUNNEL_DOMAINS; 241 | 242 | #[test] 243 | fn known_tunnels_domains_count() { 244 | assert!( 245 | KNOWN_TUNNEL_DOMAINS.len() < 25, 246 | "KNOWN_TUNNEL_DOMAINS must be encoded as a single byte." 247 | ) 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /libwebauthn/src/transport/cable/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | mod crypto; 4 | mod digit_encode; 5 | 6 | pub mod advertisement; 7 | pub mod channel; 8 | pub mod known_devices; 9 | pub mod qr_code_device; 10 | pub mod tunnel; 11 | 12 | use super::Transport; 13 | pub use digit_encode::digit_encode; 14 | 15 | pub struct Cable {} 16 | impl Transport for Cable {} 17 | unsafe impl Send for Cable {} 18 | unsafe impl Sync for Cable {} 19 | 20 | impl Display for Cable { 21 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 22 | write!(f, "Cable") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /libwebauthn/src/transport/cable/qr_code_device.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display}; 2 | use std::sync::Arc; 3 | use std::time::SystemTime; 4 | 5 | use async_trait::async_trait; 6 | use p256::elliptic_curve::sec1::ToEncodedPoint; 7 | use p256::{NonZeroScalar, SecretKey}; 8 | use rand::rngs::OsRng; 9 | use rand::RngCore; 10 | use serde::Serialize; 11 | use serde_bytes::ByteArray; 12 | use serde_indexed::SerializeIndexed; 13 | use tokio::sync::mpsc; 14 | use tracing::{debug, error}; 15 | 16 | use super::known_devices::CableKnownDeviceInfoStore; 17 | use super::tunnel::{self, KNOWN_TUNNEL_DOMAINS}; 18 | use super::{channel::CableChannel, Cable}; 19 | use crate::transport::cable::advertisement::await_advertisement; 20 | use crate::transport::cable::crypto::{derive, KeyPurpose}; 21 | use crate::transport::cable::digit_encode; 22 | use crate::transport::error::Error; 23 | use crate::transport::Device; 24 | use crate::webauthn::TransportError; 25 | use crate::UxUpdate; 26 | 27 | #[derive(Debug, Clone, Copy, Serialize, PartialEq)] 28 | pub enum QrCodeOperationHint { 29 | #[serde(rename = "ga")] 30 | GetAssertionRequest, 31 | #[serde(rename = "mc")] 32 | MakeCredential, 33 | } 34 | 35 | #[derive(Debug, SerializeIndexed)] 36 | pub struct CableQrCode { 37 | // Key 0: a 33-byte, P-256, X9.62, compressed public key. 38 | pub public_key: ByteArray<33>, 39 | // Key 1: a 16-byte random QR secret. 40 | pub qr_secret: ByteArray<16>, 41 | /// Key 2: the number of assigned tunnel server domains known to this implementation. 42 | pub known_tunnel_domains_count: u8, 43 | /// Key 3: (optional) the current time in epoch seconds. 44 | #[serde(skip_serializing_if = "Option::is_none")] 45 | pub current_time: Option, 46 | /// Key 4: (optional) a boolean that is true if the device displaying the QR code can perform state- 47 | /// assisted transactions. 48 | #[serde(skip_serializing_if = "Option::is_none")] 49 | pub state_assisted: Option, 50 | /// Key 5: either the string “ga” to hint that a getAssertion will follow, or “mc” to hint that a 51 | /// makeCredential will follow. Implementations SHOULD treat unknown values as if they were “ga”. 52 | /// This field exists so that guidance can be given to the user immediately upon scanning the QR code, 53 | /// prior to the authenticator receiving any CTAP message. While this hint SHOULD be as accurate as 54 | /// possible, it does not constrain the subsequent CTAP messages that the platform may send. 55 | pub operation_hint: QrCodeOperationHint, 56 | #[serde(skip_serializing_if = "Option::is_none")] 57 | pub supports_non_discoverable_mc: Option, 58 | } 59 | 60 | impl ToString for CableQrCode { 61 | fn to_string(&self) -> String { 62 | let serialized = serde_cbor::to_vec(self).unwrap(); 63 | format!("FIDO:/{}", digit_encode(&serialized)) 64 | } 65 | } 66 | 67 | /// Represents a new device which will connect by scanning a QR code. 68 | /// This could be a new device, or an ephmemeral device whose details were not stored. 69 | pub struct CableQrCodeDevice { 70 | /// The QR code to be scanned by the new authenticator. 71 | pub qr_code: CableQrCode, 72 | /// An ephemeral private key, corresponding to the public key within the QR code. 73 | pub private_key: NonZeroScalar, 74 | /// An optional reference to the store. This may be None, if no persistence is desired. 75 | pub(crate) store: Option>, 76 | } 77 | 78 | impl Debug for CableQrCodeDevice { 79 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 80 | f.debug_struct("CableQrCodeDevice") 81 | .field("qr_code", &self.qr_code) 82 | .field("store", &self.store) 83 | .finish() 84 | } 85 | } 86 | 87 | impl CableQrCodeDevice { 88 | /// Generates a QR code, linking the provided known-device store. A device scanning 89 | /// this QR code may be persisted to the store after a successful connection. 90 | pub fn new_persistent( 91 | hint: QrCodeOperationHint, 92 | store: Arc, 93 | ) -> Self { 94 | Self::new(hint, true, Some(store)) 95 | } 96 | 97 | fn new( 98 | hint: QrCodeOperationHint, 99 | state_assisted: bool, 100 | store: Option>, 101 | ) -> Self { 102 | let private_key_scalar = NonZeroScalar::random(&mut OsRng); 103 | let private_key = SecretKey::from_bytes(&private_key_scalar.to_bytes()).unwrap(); 104 | let public_key: [u8; 33] = private_key 105 | .public_key() 106 | .as_affine() 107 | .to_encoded_point(true) 108 | .as_bytes() 109 | .try_into() 110 | .unwrap(); 111 | let mut qr_secret = [0u8; 16]; 112 | OsRng::default().fill_bytes(&mut qr_secret); 113 | 114 | let current_unix_time = SystemTime::now() 115 | .duration_since(SystemTime::UNIX_EPOCH) 116 | .ok() 117 | .map(|t| t.as_secs()); 118 | 119 | Self { 120 | qr_code: CableQrCode { 121 | public_key: ByteArray::from(public_key), 122 | qr_secret: ByteArray::from(qr_secret), 123 | known_tunnel_domains_count: KNOWN_TUNNEL_DOMAINS.len() as u8, 124 | current_time: current_unix_time, 125 | operation_hint: hint, 126 | state_assisted: Some(state_assisted), 127 | supports_non_discoverable_mc: match hint { 128 | QrCodeOperationHint::MakeCredential => Some(true), 129 | _ => None, 130 | }, 131 | }, 132 | private_key: private_key_scalar, 133 | store, 134 | } 135 | } 136 | } 137 | 138 | impl CableQrCodeDevice { 139 | /// Generates a QR code, without any known-device store. A device scanning this QR code 140 | /// will not be persisted. 141 | pub fn new_transient(hint: QrCodeOperationHint) -> Self { 142 | Self::new(hint, false, None) 143 | } 144 | } 145 | 146 | unsafe impl Send for CableQrCodeDevice {} 147 | 148 | unsafe impl Sync for CableQrCodeDevice {} 149 | 150 | impl Display for CableQrCodeDevice { 151 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 152 | write!(f, "CableQrCodeDevice") 153 | } 154 | } 155 | 156 | #[async_trait] 157 | impl<'d> Device<'d, Cable, CableChannel> for CableQrCodeDevice { 158 | async fn channel(&'d mut self) -> Result<(CableChannel, mpsc::Receiver), Error> { 159 | let eid_key: [u8; 64] = derive(self.qr_code.qr_secret.as_ref(), None, KeyPurpose::EIDKey); 160 | let (_device, advert) = await_advertisement(&eid_key).await?; 161 | 162 | let Some(tunnel_domain) = 163 | tunnel::decode_tunnel_server_domain(advert.encoded_tunnel_server_domain) 164 | else { 165 | error!({ encoded = %advert.encoded_tunnel_server_domain }, "Failed to decode tunnel server domain"); 166 | return Err(Error::Transport(TransportError::InvalidEndpoint)); 167 | }; 168 | 169 | debug!(?tunnel_domain, "Creating channel to tunnel server"); 170 | let routing_id_str = hex::encode(&advert.routing_id); 171 | let _nonce_str = hex::encode(&advert.nonce); 172 | 173 | let tunnel_id = &derive(self.qr_code.qr_secret.as_ref(), None, KeyPurpose::TunnelID)[..16]; 174 | let tunnel_id_str = hex::encode(&tunnel_id); 175 | 176 | let mut psk: [u8; 32] = [0u8; 32]; 177 | psk.copy_from_slice( 178 | &derive( 179 | self.qr_code.qr_secret.as_ref(), 180 | Some(&advert.plaintext), 181 | KeyPurpose::PSK, 182 | )[..32], 183 | ); 184 | 185 | let connection_type = tunnel::CableTunnelConnectionType::QrCode { 186 | routing_id: routing_id_str, 187 | tunnel_id: tunnel_id_str.clone(), 188 | private_key: self.private_key, 189 | }; 190 | let mut ws_stream = tunnel::connect(&tunnel_domain, &connection_type).await?; 191 | let noise_state = tunnel::do_handshake(&mut ws_stream, psk, &connection_type).await?; 192 | tunnel::channel( 193 | &connection_type, 194 | noise_state, 195 | &tunnel_domain, 196 | &self.store, 197 | ws_stream, 198 | ) 199 | .await 200 | } 201 | 202 | // #[instrument(skip_all)] 203 | // async fn supported_protocols(&mut self) -> Result { 204 | // Ok(SupportedProtocols::fido2_only()) 205 | // } 206 | } 207 | 208 | // TODO: unit tests 209 | // https://source.chromium.org/chromium/chromium/src/+/main:device/fido/cable/v2_handshake_unittest.cc 210 | -------------------------------------------------------------------------------- /libwebauthn/src/transport/channel.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display}; 2 | use std::time::Duration; 3 | 4 | use crate::proto::ctap2::{ 5 | Ctap2AuthTokenPermissionRole, Ctap2PinUvAuthProtocol, Ctap2UserVerificationOperation, 6 | }; 7 | use crate::proto::{ 8 | ctap1::apdu::{ApduRequest, ApduResponse}, 9 | ctap2::cbor::{CborRequest, CborResponse}, 10 | }; 11 | use crate::transport::error::Error; 12 | use crate::UxUpdate; 13 | 14 | use async_trait::async_trait; 15 | use cosey::PublicKey; 16 | use tokio::sync::mpsc; 17 | use tracing::{debug, error}; 18 | 19 | use super::device::SupportedProtocols; 20 | 21 | #[derive(Debug, Copy, Clone)] 22 | pub enum ChannelStatus { 23 | Ready, // Channels are created asynchrounously, and are always ready. 24 | Processing, 25 | Closed, 26 | } 27 | 28 | #[async_trait] 29 | pub trait Channel: Send + Sync + Display + Ctap2AuthTokenStore { 30 | fn get_state_sender(&self) -> &mpsc::Sender; 31 | async fn send_state_update(&mut self, state: UxUpdate) { 32 | debug!("Sending state update: {state:?}"); 33 | match self.get_state_sender().send(state).await { 34 | Ok(_) => (), // Success 35 | Err(_) => { 36 | error!("Failed to send state update. Application must have hung up. Closing."); 37 | self.close().await; 38 | } 39 | }; 40 | } 41 | async fn supported_protocols(&self) -> Result; 42 | async fn status(&self) -> ChannelStatus; 43 | async fn close(&mut self); 44 | 45 | async fn apdu_send(&self, request: &ApduRequest, timeout: Duration) -> Result<(), Error>; 46 | async fn apdu_recv(&self, timeout: Duration) -> Result; 47 | 48 | async fn cbor_send(&mut self, request: &CborRequest, timeout: Duration) -> Result<(), Error>; 49 | async fn cbor_recv(&mut self, timeout: Duration) -> Result; 50 | 51 | /// Allows channels to disable support for pre-flight requests 52 | fn supports_preflight() -> bool { 53 | true 54 | } 55 | } 56 | 57 | #[derive(Debug, Clone, PartialEq, Eq)] 58 | pub struct Ctap2AuthTokenPermission { 59 | pub(crate) pin_uv_auth_protocol: Ctap2PinUvAuthProtocol, 60 | pub(crate) role: Ctap2AuthTokenPermissionRole, 61 | pub(crate) rpid: Option, 62 | } 63 | 64 | impl Ctap2AuthTokenPermission { 65 | pub fn new( 66 | pin_uv_auth_protocol: Ctap2PinUvAuthProtocol, 67 | permissions: Ctap2AuthTokenPermissionRole, 68 | permissions_rpid: Option<&str>, 69 | ) -> Self { 70 | Self { 71 | pin_uv_auth_protocol, 72 | role: permissions, 73 | rpid: permissions_rpid.map(str::to_string), 74 | } 75 | } 76 | 77 | pub fn contains(&self, requested: &Ctap2AuthTokenPermission) -> bool { 78 | if self.pin_uv_auth_protocol != requested.pin_uv_auth_protocol { 79 | return false; 80 | } 81 | if self.rpid != requested.rpid { 82 | return false; 83 | } 84 | self.role.contains(requested.role) 85 | } 86 | } 87 | 88 | #[derive(Debug, Clone)] 89 | pub struct AuthTokenData { 90 | pub shared_secret: Vec, 91 | pub permission: Ctap2AuthTokenPermission, 92 | pub pin_uv_auth_token: Vec, 93 | pub protocol_version: Ctap2PinUvAuthProtocol, 94 | pub key_agreement: PublicKey, 95 | pub uv_operation: Ctap2UserVerificationOperation, 96 | } 97 | 98 | #[async_trait] 99 | pub trait Ctap2AuthTokenStore { 100 | fn store_auth_data(&mut self, auth_token_data: AuthTokenData); 101 | fn get_auth_data(&self) -> Option<&AuthTokenData>; 102 | fn clear_uv_auth_token_store(&mut self); 103 | fn get_uv_auth_token(&self, requested_permission: &Ctap2AuthTokenPermission) -> Option<&[u8]> { 104 | if let Some(stored_data) = self.get_auth_data() { 105 | if stored_data.permission.contains(requested_permission) { 106 | return Some(&stored_data.pin_uv_auth_token); 107 | } 108 | } 109 | None 110 | } 111 | fn used_pin_for_auth(&self) -> bool { 112 | if let Some(stored_data) = self.get_auth_data() { 113 | return stored_data.uv_operation 114 | == Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingPinWithPermissions 115 | || stored_data.uv_operation == Ctap2UserVerificationOperation::GetPinToken; 116 | } 117 | false 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /libwebauthn/src/transport/device.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use crate::{fido::FidoRevision, UxUpdate}; 4 | use async_trait::async_trait; 5 | use tokio::sync::mpsc; 6 | 7 | use crate::transport::ble::btleplug::manager::SupportedRevisions; 8 | use crate::transport::error::Error; 9 | 10 | use super::{Channel, Transport}; 11 | 12 | #[async_trait] 13 | pub trait Device<'d, T, C>: Send + Display 14 | where 15 | T: Transport, 16 | C: Channel + 'd, 17 | { 18 | async fn channel(&'d mut self) -> Result<(C, mpsc::Receiver), Error>; 19 | // async fn supported_protocols(&mut self) -> Result; 20 | } 21 | 22 | #[derive(Debug, Copy, Clone, Default)] 23 | pub struct SupportedProtocols { 24 | pub u2f: bool, // Can be split into U2F revisions, if needed. 25 | pub fido2: bool, 26 | } 27 | 28 | impl SupportedProtocols { 29 | pub fn u2f_only() -> Self { 30 | Self { 31 | u2f: true, 32 | ..SupportedProtocols::default() 33 | } 34 | } 35 | 36 | pub fn fido2_only() -> Self { 37 | Self { 38 | fido2: true, 39 | ..SupportedProtocols::default() 40 | } 41 | } 42 | } 43 | 44 | impl From for SupportedProtocols { 45 | fn from(revs: SupportedRevisions) -> Self { 46 | Self { 47 | u2f: revs.u2fv11 || revs.u2fv12, 48 | fido2: revs.v2, 49 | } 50 | } 51 | } 52 | 53 | impl From for SupportedProtocols { 54 | fn from(rev: FidoRevision) -> Self { 55 | match rev { 56 | FidoRevision::V2 => SupportedProtocols::fido2_only(), 57 | FidoRevision::U2fv12 => SupportedProtocols::u2f_only(), 58 | FidoRevision::U2fv11 => SupportedProtocols::u2f_only(), 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /libwebauthn/src/transport/error.rs: -------------------------------------------------------------------------------- 1 | pub use crate::proto::CtapError; 2 | 3 | #[derive(thiserror::Error, Debug, Copy, Clone, PartialEq)] 4 | pub enum PlatformError { 5 | #[error("pin too short")] 6 | PinTooShort, 7 | #[error("pin too long")] 8 | PinTooLong, 9 | #[error("pin not supported")] 10 | PinNotSupported, 11 | #[error("no user verification mechanism available")] 12 | NoUvAvailable, 13 | #[error("invalid device response")] 14 | InvalidDeviceResponse, 15 | #[error("operation not supported")] 16 | NotSupported, 17 | #[error("syntax error")] 18 | SyntaxError, 19 | } 20 | 21 | #[derive(thiserror::Error, Debug, Copy, Clone, PartialEq)] 22 | pub enum TransportError { 23 | #[error("connection failed")] 24 | ConnectionFailed, 25 | #[error("connection lost")] 26 | ConnectionLost, 27 | #[error("invalid endpoint")] 28 | InvalidEndpoint, 29 | #[error("invalid framing")] 30 | InvalidFraming, 31 | #[error("negotiation failed")] 32 | NegotiationFailed, 33 | #[error("transport unavailable")] 34 | TransportUnavailable, 35 | #[error("timeout")] 36 | Timeout, 37 | #[error("device not found")] 38 | UnknownDevice, 39 | #[error("invalid key")] 40 | InvalidKey, 41 | #[error("invalid signature")] 42 | InvalidSignature, 43 | #[error("input/output error: {0}")] 44 | IoError(std::io::ErrorKind), 45 | } 46 | 47 | #[derive(thiserror::Error, Debug, Copy, Clone, PartialEq)] 48 | pub enum Error { 49 | #[error("Transport error: {0}")] 50 | Transport(#[from] TransportError), 51 | #[error("Ctap error: {0}")] 52 | Ctap(#[from] CtapError), 53 | #[error("Platform error: {0}")] 54 | Platform(#[from] PlatformError), 55 | } 56 | 57 | impl From for Error { 58 | fn from(_error: snow::Error) -> Self { 59 | Error::Transport(TransportError::NegotiationFailed) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /libwebauthn/src/transport/hid/device.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use hidapi::DeviceInfo; 3 | use hidapi::HidApi; 4 | use std::fmt; 5 | use tokio::sync::mpsc; 6 | #[allow(unused_imports)] 7 | use tracing::{debug, info, instrument}; 8 | 9 | #[cfg(feature = "virtual-hid-device")] 10 | use solo::SoloVirtualKey; 11 | 12 | use super::channel::HidChannel; 13 | use super::Hid; 14 | 15 | use crate::transport::error::{Error, TransportError}; 16 | use crate::transport::Device; 17 | use crate::UxUpdate; 18 | 19 | #[derive(Debug)] 20 | // SoloVirtualKey is not clone-able, but in test-mode we don't care 21 | #[cfg_attr(not(feature = "virtual-hid-device"), derive(Clone))] 22 | pub struct HidDevice { 23 | pub backend: HidBackendDevice, 24 | } 25 | 26 | #[derive(Debug)] 27 | // SoloVirtualKey is not clone-able, but in test-mode we don't care 28 | #[cfg_attr(not(feature = "virtual-hid-device"), derive(Clone))] 29 | pub enum HidBackendDevice { 30 | HidApiDevice(DeviceInfo), 31 | #[cfg(feature = "virtual-hid-device")] 32 | VirtualDevice(SoloVirtualKey), 33 | } 34 | 35 | impl From<&DeviceInfo> for HidDevice { 36 | fn from(hidapi_device: &DeviceInfo) -> Self { 37 | Self { 38 | backend: HidBackendDevice::HidApiDevice(hidapi_device.clone()), 39 | } 40 | } 41 | } 42 | 43 | impl fmt::Display for HidDevice { 44 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 45 | match &self.backend { 46 | HidBackendDevice::HidApiDevice(dev) => write!( 47 | f, 48 | "{:} {:} (r{:?})", 49 | dev.manufacturer_string().unwrap(), 50 | dev.product_string().unwrap(), 51 | dev.release_number() 52 | ), 53 | #[cfg(feature = "virtual-hid-device")] 54 | HidBackendDevice::VirtualDevice(dev) => dev.fmt(f), 55 | } 56 | } 57 | } 58 | 59 | pub(crate) fn get_hidapi() -> Result { 60 | HidApi::new().or(Err(Error::Transport(TransportError::TransportUnavailable))) 61 | } 62 | 63 | #[cfg(feature = "virtual-hid-device")] 64 | #[instrument] 65 | pub async fn list_devices() -> Result, Error> { 66 | info!("Faking device list, returning virtual device"); 67 | Ok(vec![HidDevice::new_virtual()]) 68 | } 69 | 70 | #[cfg(not(feature = "virtual-hid-device"))] 71 | #[instrument] 72 | pub async fn list_devices() -> Result, Error> { 73 | let devices: Vec<_> = get_hidapi()? 74 | .device_list() 75 | .filter(|device| device.usage_page() == 0xF1D0) 76 | .filter(|device| device.usage() == 0x0001) 77 | .map(|device| device.into()) 78 | .collect(); 79 | info!({ count = devices.len() }, "Listing available HID devices"); 80 | debug!(?devices); 81 | Ok(devices) 82 | } 83 | 84 | impl HidDevice { 85 | #[cfg(feature = "virtual-hid-device")] 86 | pub fn new_virtual() -> Self { 87 | let solo = SoloVirtualKey::default(); 88 | Self { 89 | backend: HidBackendDevice::VirtualDevice(solo), 90 | } 91 | } 92 | } 93 | 94 | #[async_trait] 95 | impl<'d> Device<'d, Hid, HidChannel<'d>> for HidDevice { 96 | async fn channel(&'d mut self) -> Result<(HidChannel<'d>, mpsc::Receiver), Error> { 97 | let (send, recv) = mpsc::channel(1); 98 | let channel = HidChannel::new(self, send).await?; 99 | Ok((channel, recv)) 100 | } 101 | 102 | // async fn supported_protocols(&mut self) -> Result { 103 | // let channel = self.channel().await?; 104 | // channel.supported_protocols().await 105 | // } 106 | } 107 | 108 | #[cfg(test)] 109 | mod tests { 110 | 111 | #[cfg(feature = "hid-device-tests")] 112 | #[tokio::test] 113 | async fn test_supported_protocols() { 114 | use super::HidDevice; 115 | use crate::transport::channel::Channel; 116 | use crate::transport::Device; 117 | 118 | let mut device = HidDevice::new_virtual(); 119 | let (channel, _) = device.channel().await.unwrap(); 120 | 121 | let protocols = channel.supported_protocols().await.unwrap(); 122 | 123 | assert!(protocols.u2f); 124 | assert!(protocols.fido2); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /libwebauthn/src/transport/hid/init.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /libwebauthn/src/transport/hid/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | pub mod channel; 4 | pub mod device; 5 | pub mod framing; 6 | pub mod init; 7 | 8 | pub use device::{list_devices, HidDevice}; 9 | 10 | use super::Transport; 11 | 12 | pub struct Hid {} 13 | impl Transport for Hid {} 14 | unsafe impl Send for Hid {} 15 | unsafe impl Sync for Hid {} 16 | 17 | impl Display for Hid { 18 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 19 | write!(f, "Hid") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /libwebauthn/src/transport/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod error; 2 | 3 | pub mod ble; 4 | pub mod cable; 5 | pub mod device; 6 | pub mod hid; 7 | 8 | mod channel; 9 | mod transport; 10 | 11 | pub(crate) use channel::{AuthTokenData, Ctap2AuthTokenPermission}; 12 | pub use channel::{Channel, Ctap2AuthTokenStore}; 13 | pub use device::Device; 14 | pub use transport::Transport; 15 | -------------------------------------------------------------------------------- /libwebauthn/src/transport/transport.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | pub trait Transport: Display {} 4 | 5 | /* 6 | pub struct Ble {} 7 | impl Transport for Ble {} 8 | 9 | pub struct Hid {} 10 | impl Transport for Hid {} 11 | */ 12 | -------------------------------------------------------------------------------- /libwebauthn/src/u2f.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use tracing::{instrument, warn}; 3 | 4 | use crate::fido::FidoProtocol; 5 | use crate::ops::u2f::{RegisterRequest, SignRequest}; 6 | use crate::ops::u2f::{RegisterResponse, SignResponse}; 7 | use crate::proto::ctap1::Ctap1; 8 | use crate::transport::error::{Error, TransportError}; 9 | use crate::transport::Channel; 10 | 11 | #[async_trait] 12 | pub trait U2F { 13 | async fn u2f_negotiate_protocol(&mut self) -> Result; 14 | async fn u2f_register(&mut self, op: &RegisterRequest) -> Result; 15 | async fn u2f_sign(&mut self, op: &SignRequest) -> Result; 16 | } 17 | 18 | #[async_trait] 19 | impl U2F for C 20 | where 21 | C: Channel, 22 | { 23 | #[instrument(skip_all)] 24 | async fn u2f_negotiate_protocol(&mut self) -> Result { 25 | let supported = self.supported_protocols().await?; 26 | if !supported.u2f && !supported.fido2 { 27 | warn!("Negotiation failed: channel doesn't support U2F nor FIDO2"); 28 | return Err(Error::Transport(TransportError::NegotiationFailed)); 29 | } 30 | // Ensure CTAP1 version is reported correctly. 31 | self.ctap1_version().await?; 32 | let selected = FidoProtocol::U2F; 33 | Ok(selected) 34 | } 35 | 36 | #[instrument(skip_all, fields(dev = %self))] 37 | async fn u2f_register(&mut self, op: &RegisterRequest) -> Result { 38 | let protocol = self.u2f_negotiate_protocol().await?; 39 | match protocol { 40 | FidoProtocol::U2F => self.ctap1_register(op).await, 41 | _ => Err(Error::Transport(TransportError::NegotiationFailed)), 42 | } 43 | } 44 | 45 | #[instrument(skip_all, fields(dev = %self))] 46 | async fn u2f_sign(&mut self, op: &SignRequest) -> Result { 47 | let protocol = self.u2f_negotiate_protocol().await?; 48 | match protocol { 49 | FidoProtocol::U2F => self.ctap1_sign(op).await, 50 | _ => Err(Error::Transport(TransportError::NegotiationFailed)), 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /libwebauthn/src/ui.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::error::Error; 3 | use std::time::Duration; 4 | 5 | use dbus::arg; 6 | use dbus::arg::{RefArg, Variant}; 7 | use dbus::blocking::Connection; 8 | use dbus::blocking::Proxy; 9 | use dbus::Message; 10 | use tracing::debug; 11 | use uuid::Uuid; 12 | 13 | #[derive(Debug)] 14 | struct NotificationAction { 15 | pub id: String, 16 | pub action: String, 17 | pub parameter: Vec>>, 18 | } 19 | 20 | impl arg::AppendAll for NotificationAction { 21 | fn append(&self, i: &mut arg::IterAppend) { 22 | arg::RefArg::append(&self.id, i); 23 | arg::RefArg::append(&self.action, i); 24 | arg::RefArg::append(&self.parameter, i); 25 | } 26 | } 27 | 28 | impl arg::ReadAll for NotificationAction { 29 | fn read(i: &mut arg::Iter) -> Result { 30 | Ok(NotificationAction { 31 | id: i.read()?, 32 | action: i.read()?, 33 | parameter: i.read()?, 34 | }) 35 | } 36 | } 37 | 38 | impl dbus::message::SignalArgs for NotificationAction { 39 | const NAME: &'static str = "ActionInvoked"; 40 | const INTERFACE: &'static str = "org.freedesktop.portal.Notification"; 41 | } 42 | 43 | fn variant_str(string: &str) -> Variant> { 44 | let s = String::from(string); 45 | let b = Box::new(s) as Box; 46 | Variant(b) 47 | } 48 | 49 | pub enum CancellationResponse { 50 | UserCancel, 51 | } 52 | 53 | pub trait UI { 54 | type Handle; 55 | 56 | fn confirm_u2f_usb_register( 57 | &self, 58 | app_id: &str, 59 | timeout: Duration, 60 | callback: fn(CancellationResponse) -> (), 61 | ) -> Result>; 62 | 63 | fn confirm_u2f_usb_sign( 64 | &self, 65 | app_id: &str, 66 | timeout: Duration, 67 | callback: fn(CancellationResponse) -> (), 68 | ) -> Result>; 69 | 70 | fn cancel(&self, handle: Self::Handle) -> Result<(), Box>; 71 | } 72 | 73 | pub struct NotificationPortalUI<'conn> { 74 | dbus_proxy: Proxy<'conn, &'conn Connection>, 75 | } 76 | 77 | pub struct NotificationHandle { 78 | id: String, 79 | } 80 | 81 | impl NotificationHandle { 82 | fn new(id: &str) -> NotificationHandle { 83 | NotificationHandle { 84 | id: String::from(id), 85 | } 86 | } 87 | } 88 | 89 | impl<'conn> NotificationPortalUI<'conn> { 90 | pub fn new(conn: &'conn Connection) -> Self { 91 | let proxy: Proxy<&'conn Connection> = conn.with_proxy( 92 | "org.freedesktop.portal.Desktop", // iface 93 | "/org/freedesktop/portal/desktop", // object 94 | Duration::from_millis(5000), 95 | ); 96 | Self { dbus_proxy: proxy } 97 | } 98 | 99 | fn _action( 100 | &self, 101 | title: &str, 102 | body: &str, 103 | callback: fn(CancellationResponse) -> (), 104 | ) -> Result> { 105 | let notification_id = Uuid::new_v4().to_hyphenated().to_string(); 106 | 107 | let mut button1 = HashMap::new(); 108 | button1.insert(String::from("action"), variant_str("cancel")); 109 | button1.insert(String::from("label"), variant_str("Cancel")); 110 | let buttons = vec![button1]; 111 | 112 | let mut options = HashMap::new(); 113 | options.insert("title", variant_str(title)); 114 | options.insert("body", variant_str(body)); 115 | options.insert("priority", variant_str("urgent")); 116 | options.insert("icon", variant_str("dialog-password")); // https://developer.gnome.org/icon-naming-spec/ 117 | 118 | options.insert("default-action", variant_str("cancel")); 119 | options.insert("buttons", Variant(Box::new(buttons) as Box)); 120 | 121 | self.dbus_proxy.match_signal( 122 | move |h: NotificationAction, _: &Connection, _: &Message| { 123 | debug!("Received signal: {:?}", h); 124 | match h.action.as_str() { 125 | _ => callback(CancellationResponse::UserCancel), 126 | }; 127 | true 128 | }, 129 | )?; 130 | 131 | self.dbus_proxy.method_call( 132 | "org.freedesktop.portal.Notification", 133 | "AddNotification", 134 | (¬ification_id, options), 135 | )?; 136 | 137 | Ok(NotificationHandle::new(¬ification_id)) 138 | } 139 | } 140 | 141 | impl<'conn> UI for NotificationPortalUI<'conn> { 142 | type Handle = NotificationHandle; 143 | 144 | fn confirm_u2f_usb_register( 145 | &self, 146 | app_id: &str, 147 | timeout: Duration, 148 | callback: fn(CancellationResponse) -> (), 149 | ) -> Result> { 150 | self._action( 151 | "Touch your Security Key to register it", 152 | &format!( 153 | "\nThe application ({}) would like to register your FIDO U2F security key.\n\n\ 154 | Touch it within {:?}, or click Cancel.", 155 | app_id, timeout 156 | ), 157 | callback, 158 | ) 159 | } 160 | 161 | fn confirm_u2f_usb_sign( 162 | &self, 163 | app_id: &str, 164 | timeout: Duration, 165 | callback: fn(CancellationResponse) -> (), 166 | ) -> Result> { 167 | self._action( 168 | "Touch your Security Key to verify your identity ", 169 | &format!( 170 | "\nThe application ({}) would like to verify your\ 171 | identity using your FIDO U2F security key.\n\n\ 172 | Touch it within {:?}, or click Cancel.", 173 | app_id, timeout 174 | ), 175 | callback, 176 | ) 177 | } 178 | 179 | fn cancel(&self, handle: Self::Handle) -> Result<(), Box> { 180 | self.dbus_proxy.method_call( 181 | "org.freedesktop.portal.Notification", 182 | "RemoveNotification", 183 | (handle.id,), 184 | )?; 185 | Ok(()) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /solo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "solo" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [lib] 9 | name = "solo" 10 | path = "src/lib.rs" 11 | 12 | [dependencies] 13 | log = "0.4" 14 | env_logger = "0.11.8" 15 | tokio = {version = "1.45.1", features = ["process", "io-util", "rt"]} -------------------------------------------------------------------------------- /solo/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs::copy; 3 | use std::path::Path; 4 | use std::process::Command; 5 | 6 | pub fn main() { 7 | println!("cargo:rerun-if-changed=src"); 8 | println!("cargo:rerun-if-changed=build.rs"); 9 | 10 | let output = Command::new("make") 11 | .current_dir("src/ext") 12 | .output() 13 | .expect("failed to execute 'make'"); 14 | 15 | if !output.status.success() { 16 | panic!("make failed: {:?}", output); 17 | } 18 | 19 | let cwd = env::current_dir().unwrap(); 20 | let out_dir = env::var("OUT_DIR").unwrap(); 21 | let src = Path::new(&cwd).join("src/ext/main"); 22 | let dst = Path::new(&out_dir).join("solokey"); 23 | copy(&src, &dst).unwrap(); 24 | } 25 | -------------------------------------------------------------------------------- /solo/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | use std::path::Path; 3 | use std::process::Stdio; 4 | use std::thread::sleep; 5 | use std::time::Duration; 6 | 7 | use tokio::io::{AsyncBufReadExt, BufReader}; 8 | use tokio::process::{Child, Command}; 9 | use tokio::spawn; 10 | 11 | use log::{debug, warn}; 12 | 13 | #[allow(dead_code)] 14 | #[derive(Debug)] 15 | pub struct SoloVirtualKey { 16 | handle: Child, 17 | } 18 | 19 | impl Display for SoloVirtualKey { 20 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 21 | write!(f, "SoloVirtualKey(pid={})", self.handle.id().unwrap_or(0)) 22 | } 23 | } 24 | 25 | impl Default for SoloVirtualKey { 26 | fn default() -> Self { 27 | let key_path = env!("OUT_DIR"); 28 | let binary = Path::new(key_path).join("solokey"); 29 | if !binary.exists() { 30 | panic!("Binary not found at path {:?}", binary); 31 | } 32 | 33 | let mut handle = Command::new(binary) 34 | .args(&["-b", "udp"]) 35 | .current_dir(key_path) 36 | .stdout(Stdio::piped()) 37 | .stderr(Stdio::piped()) 38 | .kill_on_drop(true) 39 | .spawn() 40 | .expect("failed to start virtual key"); 41 | debug!("Started virtual key process: {:?}", handle); 42 | 43 | let mut stdout = BufReader::new(handle.stdout.take().unwrap()).lines(); 44 | let mut stderr = BufReader::new(handle.stderr.take().unwrap()).lines(); 45 | 46 | spawn(async move { 47 | while let Ok(Some(line)) = stderr.next_line().await { 48 | warn!("stderr: {}", line); 49 | } 50 | }); 51 | 52 | spawn(async move { 53 | while let Ok(Some(line)) = stdout.next_line().await { 54 | debug!("stdout: {}", line); 55 | } 56 | }); 57 | 58 | sleep(Duration::from_millis(50)); 59 | Self { handle } 60 | } 61 | } 62 | --------------------------------------------------------------------------------