From f0605851cfbab1481f25ac8f326ea64c9d2205ea Mon Sep 17 00:00:00 2001 From: Michael Farrell Date: Sat, 7 Jan 2023 17:40:13 +1000 Subject: [PATCH] Common API changes needed to (later) add caBLE support. --- webauthn-authenticator-rs/Cargo.toml | 6 +- .../examples/authenticate/main.rs | 113 ++++-- .../examples/conformance/core.rs | 4 +- .../examples/key_manager/main.rs | 36 +- .../examples/softtoken/main.rs | 43 +++ .../src/authenticator_hashed.rs | 363 ++++++++++++++++++ webauthn-authenticator-rs/src/crypto.rs | 141 +++++++ .../src/ctap2/commands/bio_enrollment.rs | 4 +- .../src/ctap2/commands/get_assertion.rs | 112 +++++- .../src/ctap2/commands/get_info.rs | 102 ++++- .../src/ctap2/commands/make_credential.rs | 103 ++++- .../src/ctap2/commands/mod.rs | 161 ++++---- .../src/ctap2/commands/reset.rs | 12 +- .../src/ctap2/commands/selection.rs | 12 +- webauthn-authenticator-rs/src/ctap2/ctap20.rs | 83 ++-- webauthn-authenticator-rs/src/ctap2/ctap21.rs | 43 ++- webauthn-authenticator-rs/src/ctap2/mod.rs | 46 ++- webauthn-authenticator-rs/src/ctap2/pin_uv.rs | 143 +------ webauthn-authenticator-rs/src/error.rs | 65 ++++ webauthn-authenticator-rs/src/lib.rs | 7 + webauthn-authenticator-rs/src/nfc/mod.rs | 17 +- webauthn-authenticator-rs/src/softpasskey.rs | 72 +--- webauthn-authenticator-rs/src/softtoken.rs | 349 +++++++++++++---- .../src/transport/any.rs | 9 +- .../src/transport/mod.rs | 23 +- webauthn-authenticator-rs/src/types.rs | 49 +++ webauthn-authenticator-rs/src/ui/mod.rs | 59 ++- webauthn-authenticator-rs/src/usb/mod.rs | 11 +- .../src/usb/responses.rs | 1 + webauthn-authenticator-rs/src/util.rs | 11 + webauthn-rs-core/src/crypto.rs | 9 +- 31 files changed, 1694 insertions(+), 515 deletions(-) create mode 100644 webauthn-authenticator-rs/examples/softtoken/main.rs create mode 100644 webauthn-authenticator-rs/src/authenticator_hashed.rs create mode 100644 webauthn-authenticator-rs/src/crypto.rs create mode 100644 webauthn-authenticator-rs/src/types.rs diff --git a/webauthn-authenticator-rs/Cargo.toml b/webauthn-authenticator-rs/Cargo.toml index 78d6ce29..fea07b17 100644 --- a/webauthn-authenticator-rs/Cargo.toml +++ b/webauthn-authenticator-rs/Cargo.toml @@ -11,6 +11,7 @@ repository = "https://github.com/kanidm/webauthn-rs" [features] nfc_raw_transmit = ["nfc"] u2fhid = ["authenticator"] + nfc = ["pcsc"] usb = ["hidapi"] win10 = ["windows"] @@ -50,7 +51,10 @@ num-derive = "0.3" async-trait = "0.1.58" futures = "0.3.25" +qrcode = { version = "^0.12.0", optional = true } + [dev-dependencies] tracing-subscriber = { version = "0.3", features = ["env-filter", "std", "fmt"] } -base64 = "0.13" clap = { version = "^3.2", features = ["derive", "env"] } +tokio = { version = "1.22.0", features = ["sync", "test-util", "macros", "rt-multi-thread", "time"] } +tempfile = { version = "3.3.0" } diff --git a/webauthn-authenticator-rs/examples/authenticate/main.rs b/webauthn-authenticator-rs/examples/authenticate/main.rs index cdd25393..f756969c 100644 --- a/webauthn-authenticator-rs/examples/authenticate/main.rs +++ b/webauthn-authenticator-rs/examples/authenticate/main.rs @@ -1,18 +1,29 @@ #[macro_use] extern crate tracing; +use std::fs::OpenOptions; use std::io::{stdin, stdout, Write}; +use clap::{Args, Parser, Subcommand}; use futures::executor::block_on; use webauthn_authenticator_rs::ctap2::CtapAuthenticator; use webauthn_authenticator_rs::prelude::Url; -use webauthn_authenticator_rs::softtoken::SoftToken; +use webauthn_authenticator_rs::softtoken::{SoftToken, SoftTokenFile}; use webauthn_authenticator_rs::transport::*; +use webauthn_authenticator_rs::types::CableRequestType; use webauthn_authenticator_rs::ui::{Cli, UiCallback}; use webauthn_authenticator_rs::AuthenticatorBackend; use webauthn_rs_core::proto::RequestAuthenticationExtensions; use webauthn_rs_core::WebauthnCore as Webauthn; +#[derive(Debug, clap::Parser)] +#[clap(about = "Register and authenticate test")] +pub struct CliParser { + /// Provider to use. + #[clap(subcommand)] + provider: Provider, +} + fn select_transport<'a, U: UiCallback>(ui: &'a U) -> impl AuthenticatorBackend + 'a { let mut reader = AnyTransport::new().unwrap(); info!("Using reader: {:?}", reader); @@ -34,57 +45,74 @@ fn select_transport<'a, U: UiCallback>(ui: &'a U) -> impl AuthenticatorBackend + panic!("No tokens available!"); } -fn select_provider<'a>(ui: &'a Cli) -> Box { - let mut providers: Vec<(&str, fn(&'a Cli) -> Box)> = Vec::new(); +#[derive(Debug, Args, Clone)] +pub struct SoftTokenOpt { + /// Path to serialised key data, created by the softtoken example. + /// + /// If not supplied, creates a temporary key in memory. + #[clap()] + pub path: Option, +} + +#[derive(Debug, Clone, Subcommand)] +enum Provider { + /// Software token provider + SoftToken(SoftTokenOpt), - providers.push(("SoftToken", |_| Box::new(SoftToken::new().unwrap().0))); - providers.push(("CTAP", |ui| Box::new(select_transport(ui)))); + /// CtapAuthenticator using Transport/Token backends (NFC, USB HID) + /// + /// Requires administrative permissions on Windows. + CTAP, #[cfg(feature = "u2fhid")] - providers.push(("Mozilla", |_| { - Box::new(webauthn_authenticator_rs::u2fhid::U2FHid::default()) - })); + /// Mozilla webauthn-authenticator-rs provider, supporting USB HID only. + Mozilla, #[cfg(feature = "win10")] - providers.push(("Windows 10", |_| { - Box::new(webauthn_authenticator_rs::win10::Win10::default()) - })); - - if providers.is_empty() { - panic!("oops, no providers available in this build!"); - } - - loop { - println!("Select a provider:"); - for (i, (name, _)) in providers.iter().enumerate() { - println!("({}): {}", i + 1, name); - } + /// Windows 10 WebAuthn API, supporting BTLE, NFC and USB HID. + Win10, +} - let mut buf = String::new(); - print!("? "); - stdout().flush().ok(); - stdin().read_line(&mut buf).expect("Cannot read stdin"); - let selected: Result = buf.trim().parse(); - match selected { - Ok(v) => { - if v < 1 || (v as usize) > providers.len() { - println!("Input out of range: {}", v); +impl Provider { + #[allow(unused_variables)] + async fn connect_provider<'a, U: UiCallback>( + &self, + request_type: CableRequestType, + ui: &'a U, + ) -> Box { + match self { + Provider::SoftToken(o) => { + if let Some(path) = &o.path { + let file = OpenOptions::new() + .read(true) + .write(true) + .create(false) + .open(path) + .unwrap(); + Box::new(SoftTokenFile::open(file).unwrap()) } else { - let p = providers.remove((v as usize) - 1); - println!("Using {}...", p.0); - return p.1(ui); + Box::new(SoftToken::new().unwrap().0) } } - Err(_) => println!("Input was not a number"), + Provider::CTAP => Box::new(select_transport(ui)), + #[cfg(feature = "u2fhid")] + Provider::Mozilla => Box::new(webauthn_authenticator_rs::u2fhid::U2FHid::default()), + #[cfg(feature = "win10")] + Provider::Win10 => Box::new(webauthn_authenticator_rs::win10::Win10::default()), } - println!(); } } -fn main() { +#[tokio::main] +async fn main() { + let opt = CliParser::parse(); + tracing_subscriber::fmt::init(); let ui = Cli {}; - let mut u = select_provider(&ui); + let provider = opt.provider; + let mut u = provider + .connect_provider(CableRequestType::MakeCredential, &ui) + .await; // WARNING: don't use this as an example of how to use the library! let wan = Webauthn::new_unsafe_experts_only( @@ -118,8 +146,17 @@ fn main() { let cred = wan.register_credential(&r, ®_state, None).unwrap(); trace!(?cred); + drop(u); + let mut buf = String::new(); + println!("WARNING: Some NFC keys need to be power-cycled before you can authenticate."); + println!("Press ENTER to authenticate, or Ctrl-C to abort"); + stdout().flush().ok(); + stdin().read_line(&mut buf).expect("Cannot read stdin"); loop { + u = provider + .connect_provider(CableRequestType::GetAssertion, &ui) + .await; let (chal, auth_state) = wan .generate_challenge_authenticate( vec![cred.clone()], @@ -150,6 +187,8 @@ fn main() { info!("auth_res -> {:x?}", auth_res); } + + drop(u); let mut buf = String::new(); println!("Press ENTER to try again, or Ctrl-C to abort"); stdout().flush().ok(); diff --git a/webauthn-authenticator-rs/examples/conformance/core.rs b/webauthn-authenticator-rs/examples/conformance/core.rs index 67cff55a..1bfb4734 100644 --- a/webauthn-authenticator-rs/examples/conformance/core.rs +++ b/webauthn-authenticator-rs/examples/conformance/core.rs @@ -1,5 +1,5 @@ use pcsc::Scope; -use webauthn_authenticator_rs::ctap2::{commands::*, *}; +use webauthn_authenticator_rs::ctap2::commands::*; use webauthn_authenticator_rs::nfc::*; use webauthn_authenticator_rs::transport::iso7816::*; @@ -62,7 +62,7 @@ fn test_extended_lc_info(card: &NFCCard) -> TestResult { return TestResult::Fail("Unsupported CTAP applet"); } - let mut get_info = (GetInfoRequest {}).to_extended_apdu().unwrap(); + let mut get_info = to_extended_apdu((GetInfoRequest {}).cbor().unwrap()); get_info.ne = 65536; resp = card .transmit(&get_info, &ISO7816LengthForm::Extended) diff --git a/webauthn-authenticator-rs/examples/key_manager/main.rs b/webauthn-authenticator-rs/examples/key_manager/main.rs index 70d7b45f..44a81536 100644 --- a/webauthn-authenticator-rs/examples/key_manager/main.rs +++ b/webauthn-authenticator-rs/examples/key_manager/main.rs @@ -140,23 +140,25 @@ fn main() { println!("No tokens available!"); return; } + + let token_count = tokens.len(); // let authenticator = select_transport(&ui); - let authenticator = &tokens[0]; + let authenticator = &mut tokens[0]; match opt.commands { Opt::Selection => { - let token = block_on(select_one_token(tokens.iter())); + let token = block_on(select_one_token(tokens.iter_mut())); println!("selected token: {:?}", token); } Opt::Info => { for token in &tokens { - println!("{}", token.get_info()); + println!("{:?}", token.get_info()); } } Opt::FactoryReset => { - assert_eq!(tokens.len(), 1); + assert_eq!(token_count, 1); println!("Resetting token to factory settings. Type 'yes' to continue."); let mut buf = String::new(); stdout().flush().ok(); @@ -171,18 +173,18 @@ fn main() { } Opt::ToggleAlwaysUv => { - assert_eq!(tokens.len(), 1); + assert_eq!(token_count, 1); block_on(authenticator.toggle_always_uv()).expect("Error toggling UV"); } Opt::EnableEnterpriseAttestation => { - assert_eq!(tokens.len(), 1); + assert_eq!(token_count, 1); block_on(authenticator.enable_enterprise_attestation()) .expect("Error enabling enterprise attestation"); } Opt::BioInfo => { - for token in &tokens { + for token in &mut tokens { if let CtapAuthenticator::Fido21(t) = token { let i = block_on(t.get_fingerprint_sensor_info()); println!("Fingerprint sensor info: {:?}", i); @@ -193,7 +195,7 @@ fn main() { } Opt::EnrollFingerprint(o) => { - let tokens: Vec<_> = tokens + let mut tokens: Vec<_> = tokens .drain(..) .filter_map(|t| { if let CtapAuthenticator::Fido21(t) = t { @@ -205,8 +207,7 @@ fn main() { }) .collect(); assert_eq!( - tokens.len(), - 1, + token_count, 1, "Expected exactly 1 CTAP2.1 authenticator supporting biometrics" ); let id = @@ -216,7 +217,7 @@ fn main() { } Opt::ListFingerprints => { - let tokens: Vec<_> = tokens + let mut tokens: Vec<_> = tokens .drain(..) .filter_map(|t| { if let CtapAuthenticator::Fido21(t) = t { @@ -246,7 +247,7 @@ fn main() { } Opt::RenameFingerprint(o) => { - let tokens: Vec<_> = tokens + let mut tokens: Vec<_> = tokens .drain(..) .filter_map(|t| { if let CtapAuthenticator::Fido21(t) = t { @@ -273,7 +274,7 @@ fn main() { } Opt::RemoveFingerprint(o) => { - let tokens: Vec<_> = tokens + let mut tokens: Vec<_> = tokens .drain(..) .filter_map(|t| { if let CtapAuthenticator::Fido21(t) = t { @@ -285,8 +286,7 @@ fn main() { }) .collect(); assert_eq!( - tokens.len(), - 1, + token_count, 1, "Expected exactly 1 CTAP2.1 authenticator supporting biometrics" ); let ids: Vec> = @@ -297,7 +297,7 @@ fn main() { } Opt::SetPinPolicy(o) => { - assert_eq!(tokens.len(), 1); + assert_eq!(token_count, 1); block_on(authenticator.set_min_pin_length( o.length, o.rpids.unwrap_or_default(), @@ -307,12 +307,12 @@ fn main() { } Opt::SetPin(o) => { - assert_eq!(tokens.len(), 1); + assert_eq!(token_count, 1); block_on(authenticator.set_new_pin(&o.new_pin)).expect("Error setting PIN"); } Opt::ChangePin(o) => { - assert_eq!(tokens.len(), 1); + assert_eq!(token_count, 1); block_on(authenticator.change_pin(&o.old_pin, &o.new_pin)).expect("Error changing PIN"); } } diff --git a/webauthn-authenticator-rs/examples/softtoken/main.rs b/webauthn-authenticator-rs/examples/softtoken/main.rs new file mode 100644 index 00000000..4727d2fb --- /dev/null +++ b/webauthn-authenticator-rs/examples/softtoken/main.rs @@ -0,0 +1,43 @@ +extern crate tracing; + +use std::fs::OpenOptions; + +use clap::{Args, Parser, Subcommand}; +use webauthn_authenticator_rs::softtoken::{SoftToken, SoftTokenFile}; + +#[derive(Debug, clap::Parser)] +#[clap(about = "SoftToken management tool")] +pub struct CliParser { + #[clap(subcommand)] + pub commands: Opt, +} + +#[derive(Debug, Subcommand)] +pub enum Opt { + Create(CreateArgs), +} + +#[derive(Debug, Args)] +pub struct CreateArgs { + #[clap()] + pub filename: String, +} + +fn main() { + use Opt::*; + + let opt = CliParser::parse(); + tracing_subscriber::fmt::init(); + match opt.commands { + Create(args) => { + let (token, _) = SoftToken::new().unwrap(); + let f = OpenOptions::new() + .write(true) + .create_new(true) + .open(args.filename) + .unwrap(); + let authenticator = SoftTokenFile::new(token, f); + drop(authenticator); + } + } +} diff --git a/webauthn-authenticator-rs/src/authenticator_hashed.rs b/webauthn-authenticator-rs/src/authenticator_hashed.rs new file mode 100644 index 00000000..7e4967ca --- /dev/null +++ b/webauthn-authenticator-rs/src/authenticator_hashed.rs @@ -0,0 +1,363 @@ +//! Specialized alternative traits for authenticator backends. +use std::collections::BTreeMap; + +use base64urlsafedata::Base64UrlSafeData; +use serde_cbor::{ser::to_vec_packed, Value}; +use url::Url; +use webauthn_rs_proto::{ + PublicKeyCredential, PublicKeyCredentialCreationOptions, PublicKeyCredentialDescriptor, + PublicKeyCredentialRequestOptions, RegisterPublicKeyCredential, +}; + +use crate::{ + ctap2::commands::{ + GetAssertionRequest, GetAssertionResponse, MakeCredentialRequest, MakeCredentialResponse, + }, + error::WebauthnCError, + util::{compute_sha256, creation_to_clientdata, get_to_clientdata}, + AuthenticatorBackend, +}; + +/// [AuthenticatorBackend] with a `client_data_hash` parameter, for proxying +/// requests. +/// +/// **Note:** unless you're proxying autentication requests, use the +/// [AuthenticatorBackend] trait instead. There is an implementation of +/// [AuthenticatorBackend] for `T: AuthenticatorBackendHashedClientData`. +/// +/// Normally, [AuthenticatorBackend] takes the `origin` and `options.challenge` +/// parameters, serialises it to JSON, and then hashes it to produce +/// `client_data_hash`, which the authenticator signs. That JSON and the +/// signature are returned to the relying party, which it can check contain +/// expected values and are signed correctly. +/// +/// This doesn't work when proxying an authenticator, where an initiator (web +/// browser) has *already* produced a `client_data_hash` for the authenticator +/// to sign, and changing it will cause the authenticator to sign something else +/// (and fail verification). +/// +/// This trait instead takes a `client_data_hash` directly, and ignores the +/// `options.challenge` parameter. The downside is that this *can't* return a +/// `client_data_json` (the value is unknown), because the authenticator +/// wouldn't normally get a `client_data_json`. +/// +/// This is similar to +/// [`BrowserPublicKeyCredentialCreationOptions.Builder.setClientDataHash()`][0] +/// on Android (Google Play Services FIDO API), which Chromium uses to proxy +/// caBLE requests (which only contain `client_data_json`) to an authenticator +/// stored in the device's secure element. +/// +/// [AuthenticatorBackendHashedClientData] provides a [AuthenticatorBackend] +/// implementation – so backends should only implement **one** of those APIs, +/// preferring to implement [AuthenticatorBackendHashedClientData] if possible. +/// +/// This interface won't be feasiable to implement on all platforms. For +/// example, Windows' Webauthn API takes a `client_data_json` and always hashes +/// it, and Apple's Passkey API takes `relyingPartyIdentifier` (origin) and +/// `challenge` parameters and generates the `client_data_json` for you. +/// +/// Most clients should prefer to use the [AuthenticatorBackend] trait. +/// +/// **See also:** [perform_register_with_request], [perform_auth_with_request] +/// +/// [0]: https://developers.google.com/android/reference/com/google/android/gms/fido/fido2/api/common/BrowserPublicKeyCredentialCreationOptions.Builder#public-browserpublickeycredentialcreationoptions.builder-setclientdatahash-byte[]-clientdatahash +pub trait AuthenticatorBackendHashedClientData { + fn perform_register( + &mut self, + client_data_hash: Vec, + options: PublicKeyCredentialCreationOptions, + timeout_ms: u32, + ) -> Result; + + fn perform_auth( + &mut self, + client_data_hash: Vec, + options: PublicKeyCredentialRequestOptions, + timeout_ms: u32, + ) -> Result; +} + +/// This provides a [AuthenticatorBackend] implementation for +/// [AuthenticatorBackendHashedClientData] implementations. +/// +/// This implementation creates and hashes the `client_data_json`, and inserts +/// it back into the response type as normal. +impl AuthenticatorBackend for T { + fn perform_register( + &mut self, + origin: Url, + options: PublicKeyCredentialCreationOptions, + timeout_ms: u32, + ) -> Result { + let client_data = creation_to_clientdata(origin, options.challenge.clone()); + let client_data: Vec = serde_json::to_string(&client_data) + .map_err(|_| WebauthnCError::Json)? + .into(); + let client_data_hash = compute_sha256(&client_data).to_vec(); + let mut cred = self.perform_register(client_data_hash, options, timeout_ms)?; + cred.response.client_data_json = Base64UrlSafeData(client_data); + + Ok(cred) + } + + fn perform_auth( + &mut self, + origin: Url, + options: PublicKeyCredentialRequestOptions, + timeout_ms: u32, + ) -> Result { + let client_data = get_to_clientdata(origin, options.challenge.clone()); + let client_data: Vec = serde_json::to_string(&client_data) + .map_err(|_| WebauthnCError::Json)? + .into(); + let client_data_hash = compute_sha256(&client_data).to_vec(); + let mut cred = self.perform_auth(client_data_hash, options, timeout_ms)?; + cred.response.client_data_json = Base64UrlSafeData(client_data); + Ok(cred) + } +} + +/// Performs a registration request, using a [MakeCredentialRequest]. +/// +/// All PIN/UV auth parameters will be ignored, and are processed by +/// [AuthenticatorBackendHashedClientData] in the usual way. +/// +/// Returns a [MakeCredentialResponse] as `Vec` on success. The message may +/// not be identical to what the authenticator actually returned, as it is +/// subject to deserialisation and conversion to and from another structure used +/// by [AuthenticatorBackend]. +pub fn perform_register_with_request( + backend: &mut impl AuthenticatorBackendHashedClientData, + request: MakeCredentialRequest, + timeout_ms: u32, +) -> Result, WebauthnCError> { + let options = PublicKeyCredentialCreationOptions { + rp: request.rp, + user: request.user, + challenge: Base64UrlSafeData(vec![]), + pub_key_cred_params: request.pub_key_cred_params, + timeout: Some(timeout_ms), + exclude_credentials: Some(request.exclude_list), + // TODO + attestation: None, + authenticator_selection: None, + extensions: None, + }; + let client_data_hash = request.client_data_hash; + + let cred: RegisterPublicKeyCredential = + backend.perform_register(client_data_hash, options, timeout_ms)?; + + // attestation_object is a MakeCredentialResponse, with string keys + // rather than u32, we need to convert it. + let resp: MakeCredentialResponse = + serde_cbor::de::from_slice(cred.response.attestation_object.0.as_slice()) + .map_err(|_| WebauthnCError::Cbor)?; + + // Write value with u32 keys + let resp: BTreeMap = resp.into(); + to_vec_packed(&resp).map_err(|_| WebauthnCError::Cbor) +} + +/// Performs an authentication request, using a [GetAssertionRequest]. +/// +/// All PIN/UV auth parameters will be ignored, and are processed by +/// [AuthenticatorBackendHashedClientData] in the usual way. +/// +/// Returns a [GetAssertionResponse] as `Vec` on success. The message may +/// not be identical to what the authenticator actually returned, as it is +/// subject to deserialisation and conversion to and from another structure used +/// by [AuthenticatorBackend]. +pub fn perform_auth_with_request( + backend: &mut impl AuthenticatorBackendHashedClientData, + request: GetAssertionRequest, + timeout_ms: u32, +) -> Result, WebauthnCError> { + let options = PublicKeyCredentialRequestOptions { + challenge: Base64UrlSafeData(vec![]), + timeout: Some(timeout_ms), + rp_id: request.rp_id, + allow_credentials: request.allow_list, + // TODO + user_verification: webauthn_rs_proto::UserVerificationPolicy::Preferred, + extensions: None, + }; + + let cred = backend.perform_auth(request.client_data_hash, options, timeout_ms)?; + let resp = GetAssertionResponse { + credential: Some(PublicKeyCredentialDescriptor { + type_: cred.type_, + id: cred.raw_id, + transports: None, + }), + auth_data: Some(cred.response.authenticator_data.0), + signature: Some(cred.response.signature.0), + number_of_credentials: None, + user_selected: None, + large_blob_key: None, + }; + + // Write value with u32 keys + let resp: BTreeMap = resp.into(); + to_vec_packed(&resp).map_err(|_| WebauthnCError::Cbor) +} + +#[cfg(test)] +mod test { + use openssl::{hash::MessageDigest, rand::rand_bytes, sign::Verifier, x509::X509}; + use webauthn_rs_core::proto::COSEKey; + use webauthn_rs_proto::{AllowCredentials, PubKeyCredParams, RelyingParty, User}; + + use crate::{ + ctap2::{commands::value_to_vec_u8, CBORResponse}, + softtoken::SoftToken, + }; + + use super::*; + + #[test] + fn perform_register_auth_with_command() { + let _ = tracing_subscriber::fmt::try_init(); + let (mut soft_token, _) = SoftToken::new().unwrap(); + let mut client_data_hash = vec![0; 32]; + let mut user_id = vec![0; 16]; + rand_bytes(&mut client_data_hash).unwrap(); + rand_bytes(&mut user_id).unwrap(); + + let request = MakeCredentialRequest { + client_data_hash: client_data_hash.clone(), + rp: RelyingParty { + name: "example.com".to_string(), + id: "example.com".to_string(), + }, + user: User { + id: Base64UrlSafeData(user_id), + name: "sampleuser".to_string(), + display_name: "Sample User".to_string(), + }, + pub_key_cred_params: vec![ + PubKeyCredParams { + type_: "public-key".to_string(), + alg: -7, + }, + PubKeyCredParams { + type_: "public-key".to_string(), + alg: -257, + }, + ], + exclude_list: vec![], + options: None, + pin_uv_auth_param: None, + pin_uv_auth_proto: None, + enterprise_attest: None, + }; + + let response = perform_register_with_request(&mut soft_token, request, 10000).unwrap(); + + // All keys should be ints + let m: Value = serde_cbor::from_slice(response.as_slice()).unwrap(); + let m = if let Value::Map(m) = m { + m + } else { + panic!("unexpected type") + }; + assert!(m.keys().all(|k| if let Value::Integer(_) = k { + true + } else { + false + })); + + // Try to deserialise the MakeCredentialResponse again + let response = + ::try_from(response.as_slice()).unwrap(); + trace!(?response); + + // Run packed attestation verification + // https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation + let mut att_stmt = if let Value::Map(m) = response.att_stmt.unwrap() { + m + } else { + panic!("unexpected type"); + }; + trace!(?att_stmt); + let signature = value_to_vec_u8( + att_stmt.remove(&Value::Text("sig".to_string())).unwrap(), + "att_stmt.sig", + ) + .unwrap(); + + // Extract attestation certificate + let x5c = if let Value::Array(v) = att_stmt.remove(&Value::Text("x5c".to_string())).unwrap() + { + v + } else { + panic!("Unexpected type"); + }; + let x5c = value_to_vec_u8(x5c[0].to_owned(), "x5c[0]").unwrap(); + let verification_cert = X509::from_der(&x5c).unwrap(); + let pubkey = verification_cert.public_key().unwrap(); + + // Reconstruct verification data (auth_data + client_data_hash) + let mut verification_data = + value_to_vec_u8(response.auth_data.unwrap(), "verification_data").unwrap(); + let auth_data_len = verification_data.len(); + verification_data.reserve(client_data_hash.len()); + verification_data.extend_from_slice(&client_data_hash); + + let mut verifier = Verifier::new(MessageDigest::sha256(), &pubkey).unwrap(); + assert!(verifier + .verify_oneshot(&signature, &verification_data) + .unwrap()); + + // https://www.w3.org/TR/webauthn-2/#attestation-object + let cred_id_off = /* rp_id_hash */ 32 + /* flags */ 1 + /* counter */ 4 + /* aaguid */ 16; + let cred_id_len = u16::from_be_bytes( + (&verification_data[cred_id_off..cred_id_off + 2]) + .try_into() + .unwrap(), + ) as usize; + let cred_id = Base64UrlSafeData( + (&verification_data[cred_id_off + 2..cred_id_off + 2 + cred_id_len]).to_vec(), + ); + + // Future assertions are signed with this COSEKey + let cose_key: Value = serde_cbor::from_slice( + &verification_data[cred_id_off + 2 + cred_id_len..auth_data_len], + ) + .unwrap(); + let cose_key = COSEKey::try_from(&cose_key).unwrap(); + + rand_bytes(&mut client_data_hash).unwrap(); + let request = GetAssertionRequest { + client_data_hash: client_data_hash.clone(), + rp_id: "example.com".to_string(), + allow_list: vec![AllowCredentials { + type_: "public-key".to_string(), + id: cred_id.to_owned(), + transports: None, + }], + options: None, + pin_uv_auth_param: None, + pin_uv_auth_proto: None, + }; + trace!(?request); + + let response = perform_auth_with_request(&mut soft_token, request, 10000).unwrap(); + let response = + ::try_from(response.as_slice()).unwrap(); + trace!(?response); + + // Check correct matching credential + assert_eq!(response.credential.unwrap().id, cred_id); + + // Check the signature + let signature = response.signature.unwrap(); + let mut verification_data = response.auth_data.unwrap(); + verification_data.reserve(client_data_hash.len()); + verification_data.extend_from_slice(&client_data_hash); + + assert!(cose_key + .verify_signature(&signature, &verification_data) + .unwrap()); + } +} diff --git a/webauthn-authenticator-rs/src/crypto.rs b/webauthn-authenticator-rs/src/crypto.rs new file mode 100644 index 00000000..657c2f14 --- /dev/null +++ b/webauthn-authenticator-rs/src/crypto.rs @@ -0,0 +1,141 @@ +//! Common cryptographic routines for FIDO2. + +use openssl::{ + ec::{EcGroup, EcKey}, + md::Md, + nid::Nid, + pkey::{Id, PKey, Private, Public}, + pkey_ctx::PkeyCtx, + symm::{Cipher, Crypter, Mode}, +}; + +use crate::error::WebauthnCError; + +/// Gets an [EcGroup] for P-256 +pub fn get_group() -> Result { + Ok(EcGroup::from_curve_name(Nid::X9_62_PRIME256V1)?) +} + +/// Encrypts some data using AES-256-CBC, with no padding. +/// +/// `plaintext.len()` must be a multiple of the cipher's blocksize. +pub fn encrypt(key: &[u8], iv: Option<&[u8]>, plaintext: &[u8]) -> Result, WebauthnCError> { + let cipher = Cipher::aes_256_cbc(); + let mut ct = vec![0; plaintext.len() + cipher.block_size()]; + let mut c = Crypter::new(cipher, Mode::Encrypt, key, iv)?; + c.pad(false); + let l = c.update(plaintext, &mut ct)?; + let l = l + c.finalize(&mut ct[l..])?; + ct.truncate(l); + Ok(ct) +} + +/// Decrypts some data using AES-256-CBC, with no padding. +pub fn decrypt( + key: &[u8], + iv: Option<&[u8]>, + ciphertext: &[u8], +) -> Result, WebauthnCError> { + let cipher = Cipher::aes_256_cbc(); + if ciphertext.len() % cipher.block_size() != 0 { + error!( + "ciphertext length {} is not a multiple of {} bytes", + ciphertext.len(), + cipher.block_size() + ); + return Err(WebauthnCError::Internal); + } + + let mut pt = vec![0; ciphertext.len() + cipher.block_size()]; + let mut c = Crypter::new(cipher, Mode::Decrypt, key, iv)?; + c.pad(false); + let l = c.update(ciphertext, &mut pt)?; + let l = l + c.finalize(&mut pt[l..])?; + pt.truncate(l); + Ok(pt) +} + +pub fn hkdf_sha_256( + salt: &[u8], + ikm: &[u8], + info: Option<&[u8]>, + output: &mut [u8], +) -> Result<(), WebauthnCError> { + let mut ctx = PkeyCtx::new_id(Id::HKDF)?; + ctx.derive_init()?; + ctx.set_hkdf_md(Md::sha256())?; + ctx.set_hkdf_salt(salt)?; + ctx.set_hkdf_key(ikm)?; + if let Some(info) = info { + ctx.add_hkdf_info(info)?; + } + ctx.derive(Some(output))?; + Ok(()) +} + +/// Generate a fresh, random P-256 private key +pub fn regenerate() -> Result, WebauthnCError> { + let ecgroup = get_group()?; + let eckey = EcKey::generate(&ecgroup)?; + Ok(eckey) +} + +pub fn ecdh( + private_key: EcKey, + peer_key: EcKey, + output: &mut [u8], +) -> Result<(), WebauthnCError> { + let peer_key = PKey::from_ec_key(peer_key)?; + let pkey = PKey::from_ec_key(private_key)?; + let mut ctx = PkeyCtx::new(&pkey)?; + ctx.derive_init()?; + ctx.derive_set_peer(&peer_key)?; + ctx.derive(Some(output))?; + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn hkdf() { + let _ = tracing_subscriber::fmt::try_init(); + let salt: Vec = (0..0x0d).collect(); + let ikm: [u8; 22] = [0x0b; 22]; + let info: Vec = (0xf0..0xfa).collect(); + let expected: [u8; 42] = [ + 0x3c, 0xb2, 0x5f, 0x25, 0xfa, 0xac, 0xd5, 0x7a, 0x90, 0x43, 0x4f, 0x64, 0xd0, 0x36, + 0x2f, 0x2a, 0x2d, 0x2d, 0xa, 0x90, 0xcf, 0x1a, 0x5a, 0x4c, 0x5d, 0xb0, 0x2d, 0x56, + 0xec, 0xc4, 0xc5, 0xbf, 0x34, 0x0, 0x72, 0x8, 0xd5, 0xb8, 0x87, 0x18, 0x58, 0x65, + ]; + + let mut output: [u8; 42] = [0; 42]; + + hkdf_sha_256(salt.as_slice(), &ikm, Some(info.as_slice()), &mut output) + .expect("hkdf_sha_256 fail"); + assert_eq!(expected, output); + } + + #[test] + fn hkdf_chromium() { + // Compare hkdf using values debug-logged from Chromium + let _ = tracing_subscriber::fmt::try_init(); + let ck = [ + 0x30, 0x7a, 0x70, 0x6e, 0x63, 0x38, 0x2e, 0x8e, 0x9d, 0x46, 0xcc, 0xdb, 0xc, 0xeb, + 0xed, 0x5c, 0x2b, 0x19, 0x28, 0xc5, 0xae, 0x2d, 0xee, 0x63, 0x52, 0xe1, 0x30, 0xac, + 0xe1, 0xf7, 0x4f, 0x44, + ]; + let expected = [ + 0x1f, 0xba, 0x3c, 0xce, 0x17, 0x62, 0x2c, 0x68, 0x26, 0x8d, 0x9f, 0x75, 0xb5, 0xa8, + 0xa3, 0x35, 0x1b, 0x51, 0x7f, 0x9, 0x6f, 0xb5, 0xe2, 0x94, 0x94, 0x1a, 0xf7, 0xe3, + 0xa6, 0xa8, 0xd6, 0xe1, 0xe3, 0x4f, 0x1a, 0xa3, 0x74, 0x72, 0x38, 0xc0, 0x4d, 0x3b, + 0xd2, 0x5e, 0x7, 0xef, 0x1b, 0x35, 0xfe, 0xf3, 0x59, 0x0, 0xd, 0x75, 0x56, 0x15, 0xcd, + 0x85, 0xbe, 0x27, 0xcf, 0xc8, 0x7, 0xd1, + ]; + let mut actual = [0; 64]; + + hkdf_sha_256(&ck, &[], None, &mut actual).unwrap(); + assert_eq!(expected, actual); + } +} diff --git a/webauthn-authenticator-rs/src/ctap2/commands/bio_enrollment.rs b/webauthn-authenticator-rs/src/ctap2/commands/bio_enrollment.rs index 9c11d609..f53bde54 100644 --- a/webauthn-authenticator-rs/src/ctap2/commands/bio_enrollment.rs +++ b/webauthn-authenticator-rs/src/ctap2/commands/bio_enrollment.rs @@ -220,8 +220,8 @@ pub enum BioSubCommand { /// Captures another sample of a fingerprint while enrollment is in /// progress: /// - /// * [Vec]: `template_id` of the partially-enrolled fingerprint. - /// * [Duration]: time-out for the operation. + /// * [`Vec`]: `template_id` of the partially-enrolled fingerprint. + /// * [`Duration`]: time-out for the operation. /// /// FingerprintEnrollCaptureNextSample(/* id */ Vec, /* timeout */ Duration), diff --git a/webauthn-authenticator-rs/src/ctap2/commands/get_assertion.rs b/webauthn-authenticator-rs/src/ctap2/commands/get_assertion.rs index 869d443d..71eb39bd 100644 --- a/webauthn-authenticator-rs/src/ctap2/commands/get_assertion.rs +++ b/webauthn-authenticator-rs/src/ctap2/commands/get_assertion.rs @@ -4,6 +4,8 @@ use serde_cbor::Value; use std::{collections::BTreeMap, str::FromStr}; use webauthn_rs_proto::{AllowCredentials, AuthenticatorTransport, PublicKeyCredentialDescriptor}; +use crate::ctap2::commands::{value_to_map, value_to_vec, value_to_vec_string}; + use super::{ value_to_bool, value_to_set_string, value_to_string, value_to_u32, value_to_vec_u8, CBORCommand, }; @@ -12,7 +14,7 @@ use super::{ /// /// Reference: #[derive(Serialize, Debug, Clone)] -#[serde(into = "BTreeMap")] +#[serde(into = "BTreeMap", try_from = "BTreeMap")] pub struct GetAssertionRequest { pub rp_id: String, pub client_data_hash: Vec, @@ -33,7 +35,7 @@ impl CBORCommand for GetAssertionRequest { /// Reference: // Note: this needs to have the same names as AttestationObjectInner #[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(rename_all = "camelCase", try_from = "BTreeMap")] +#[serde(rename_all = "camelCase")] pub struct GetAssertionResponse { pub credential: Option, pub auth_data: Option>, @@ -120,6 +122,111 @@ impl From for BTreeMap { } } +impl TryFrom> for GetAssertionRequest { + type Error = &'static str; + fn try_from(mut raw: BTreeMap) -> Result { + trace!("raw: {:?}", raw); + Ok(Self { + rp_id: raw + .remove(&0x01) + .and_then(|v| value_to_string(v, "0x01")) + .ok_or("parsing rpId")?, + client_data_hash: raw + .remove(&0x02) + .and_then(|v| value_to_vec_u8(v, "0x02")) + .ok_or("parsing clientDataHash")?, + allow_list: raw + .remove(&0x03) + .and_then(|v| value_to_vec(v, "0x03")) + .map(|v| { + v.into_iter() + .filter_map(|a| { + let mut a = value_to_map(a, "0x03")?; + let type_ = value_to_string( + a.remove(&Value::Text("type".to_string()))?, + "type", + )?; + let id = Base64UrlSafeData(value_to_vec_u8( + a.remove(&Value::Text("id".to_string()))?, + "id", + )?); + let transports = a + .remove(&Value::Text("transports".to_string())) + .and_then(|v| value_to_vec_string(v, "transports")) + .map(|v| { + v.into_iter() + .filter_map(|t| AuthenticatorTransport::from_str(&t).ok()) + .collect() + }); + Some(AllowCredentials { + id, + type_, + transports, + }) + }) + .collect() + }) + .unwrap_or_default(), + // TODO + options: None, + pin_uv_auth_param: None, + pin_uv_auth_proto: None, + }) + } +} + +impl From for BTreeMap { + fn from(r: GetAssertionResponse) -> Self { + let GetAssertionResponse { + credential, + auth_data, + signature, + number_of_credentials, + user_selected, + large_blob_key, + } = r; + + let mut keys = BTreeMap::new(); + if let Some(credential) = credential { + let mut m = BTreeMap::from([ + (Value::Text("id".to_string()), Value::Bytes(credential.id.0)), + ( + Value::Text("type".to_string()), + Value::Text(credential.type_), + ), + ]); + if let Some(transports) = credential.transports { + let transports = transports + .into_iter() + .map(|t| Value::Text(t.to_string())) + .collect(); + m.insert( + Value::Text("transports".to_string()), + Value::Array(transports), + ); + }; + keys.insert(0x01, Value::Map(m)); + } + if let Some(auth_data) = auth_data { + keys.insert(0x02, Value::Bytes(auth_data)); + } + if let Some(signature) = signature { + keys.insert(0x03, Value::Bytes(signature)); + } + if let Some(number_of_credentials) = number_of_credentials { + keys.insert(0x05, Value::Integer(number_of_credentials.into())); + } + if let Some(user_selected) = user_selected { + keys.insert(0x06, Value::Bool(user_selected)); + } + if let Some(large_blob_key) = large_blob_key { + keys.insert(0x07, Value::Bytes(large_blob_key)); + } + + keys + } +} + impl TryFrom> for GetAssertionResponse { type Error = &'static str; fn try_from(mut raw: BTreeMap) -> Result { @@ -164,6 +271,7 @@ impl TryFrom> for GetAssertionResponse { } } +crate::deserialize_cbor!(GetAssertionRequest); crate::deserialize_cbor!(GetAssertionResponse); #[cfg(test)] diff --git a/webauthn-authenticator-rs/src/ctap2/commands/get_info.rs b/webauthn-authenticator-rs/src/ctap2/commands/get_info.rs index 45bd9ad2..5fc61471 100644 --- a/webauthn-authenticator-rs/src/ctap2/commands/get_info.rs +++ b/webauthn-authenticator-rs/src/ctap2/commands/get_info.rs @@ -26,8 +26,8 @@ impl CBORCommand for GetInfoRequest { /// `authenticatorGetInfo` response type. /// /// Reference: -#[derive(Deserialize, Debug)] -#[serde(try_from = "BTreeMap")] +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(try_from = "BTreeMap", into = "BTreeMap")] pub struct GetInfoResponse { /// All CTAP protocol versions which the token supports. pub versions: BTreeSet, @@ -291,6 +291,92 @@ impl TryFrom> for GetInfoResponse { } } +impl From for BTreeMap { + fn from(value: GetInfoResponse) -> Self { + let GetInfoResponse { + versions, + extensions, + aaguid, + options, + max_msg_size, + pin_protocols, + max_cred_count_in_list, + max_cred_id_len, + transports, + algorithms, + min_pin_length, + } = value; + + let mut o = BTreeMap::from([ + ( + 0x01, + Value::Array(versions.into_iter().map(Value::Text).collect()), + ), + (0x03, Value::Bytes(aaguid)), + ]); + + if let Some(extensions) = extensions { + o.insert( + 0x02, + Value::Array(extensions.into_iter().map(Value::Text).collect()), + ); + } + + if let Some(options) = options { + o.insert( + 0x04, + Value::Map( + options + .into_iter() + .map(|(k, v)| (Value::Text(k), Value::Bool(v))) + .collect(), + ), + ); + } + + if let Some(max_msg_size) = max_msg_size { + o.insert(0x05, Value::Integer(max_msg_size.into())); + } + + if let Some(pin_protocols) = pin_protocols { + o.insert( + 0x06, + Value::Array( + pin_protocols + .into_iter() + .map(|v| Value::Integer(v.into())) + .collect(), + ), + ); + } + + if let Some(max_cred_count_in_list) = max_cred_count_in_list { + o.insert(0x07, Value::Integer(max_cred_count_in_list.into())); + } + + if let Some(max_cred_id_len) = max_cred_id_len { + o.insert(0x08, Value::Integer(max_cred_id_len.into())); + } + + if let Some(transports) = transports { + o.insert( + 0x09, + Value::Array(transports.into_iter().map(Value::Text).collect()), + ); + } + + if let Some(algorithms) = algorithms { + o.insert(0x0a, algorithms); + } + + if let Some(min_pin_length) = min_pin_length { + o.insert(0x0d, Value::Integer((min_pin_length as u32).into())); + } + + o + } +} + crate::deserialize_cbor!(GetInfoResponse); #[cfg(test)] @@ -354,28 +440,24 @@ mod tests { let short = vec![0x80, 0x10, 0, 0, 1, 0x4, 0]; let ext = vec![0x80, 0x10, 0, 0, 0, 0, 1, 0x4, 0, 0]; - let a = req.to_short_apdus().unwrap(); + let a = to_short_apdus(&req.cbor().unwrap()); assert_eq!(1, a.len()); assert_eq!(short, a[0].to_bytes(&ISO7816LengthForm::ShortOnly).unwrap()); assert_eq!(short, a[0].to_bytes(&ISO7816LengthForm::Extended).unwrap()); assert_eq!( ext, - req.to_extended_apdu() - .unwrap() + to_extended_apdu(req.cbor().unwrap()) .to_bytes(&ISO7816LengthForm::Extended) .unwrap() ); assert_eq!( ext, - req.to_extended_apdu() - .unwrap() + to_extended_apdu(req.cbor().unwrap()) .to_bytes(&ISO7816LengthForm::ExtendedOnly) .unwrap() ); - assert!(req - .to_extended_apdu() - .unwrap() + assert!(to_extended_apdu(req.cbor().unwrap()) .to_bytes(&ISO7816LengthForm::ShortOnly) .is_err()); } diff --git a/webauthn-authenticator-rs/src/ctap2/commands/make_credential.rs b/webauthn-authenticator-rs/src/ctap2/commands/make_credential.rs index 1c1057bd..3d96e517 100644 --- a/webauthn-authenticator-rs/src/ctap2/commands/make_credential.rs +++ b/webauthn-authenticator-rs/src/ctap2/commands/make_credential.rs @@ -1,15 +1,18 @@ +use base64urlsafedata::Base64UrlSafeData; use serde::{Deserialize, Serialize}; use serde_cbor::{value::to_value, Value}; use std::collections::BTreeMap; use webauthn_rs_proto::{PubKeyCredParams, PublicKeyCredentialDescriptor, RelyingParty, User}; +use crate::ctap2::commands::value_to_vec_u8; + use super::{value_to_bool, value_to_string, CBORCommand}; /// `authenticatorMakeCredential` request type. /// /// Reference: -#[derive(Serialize, Debug, Clone)] -#[serde(into = "BTreeMap")] +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(into = "BTreeMap", try_from = "BTreeMap")] pub struct MakeCredentialRequest { /// Hash of the ClientData binding specified by the host. pub client_data_hash: Vec, @@ -43,9 +46,20 @@ impl CBORCommand for MakeCredentialRequest { /// `authenticatorMakeCredential` response type. /// /// Reference: -// Note: this needs to have the same names as AttestationObjectInner +/// +/// ## Implementation notes +/// +/// This needs to be (de)serialisable to/from both `Map` **and** +/// `Map`: +/// +/// * The authenticator itself uses a map with `u32` keys. This is needed to get +/// the value from from the authenticator, and to re-serialise values for +/// caBLE (via `AuthenticatorBackendWithRequests`) +/// +/// * `AuthenticatorAttestationResponseRaw` uses a map with `String` keys, which +/// need the same names as `AttestationObjectInner`. #[derive(Deserialize, Serialize, Debug, Clone, Default, PartialEq, Eq)] -#[serde(rename_all = "camelCase", try_from = "BTreeMap")] +#[serde(rename_all = "camelCase")] pub struct MakeCredentialResponse { /// The attestation statement format identifier. pub fmt: Option, @@ -172,6 +186,82 @@ impl From for BTreeMap { } } +impl TryFrom> for MakeCredentialRequest { + type Error = &'static str; + fn try_from(mut raw: BTreeMap) -> Result { + trace!("raw: {:?}", raw); + Ok(Self { + client_data_hash: raw + .remove(&0x01) + .and_then(|v| value_to_vec_u8(v, "0x01")) + .ok_or("parsing clientDataHash")?, + rp: raw + .remove(&0x02) + .and_then(|v| serde_cbor::value::from_value(v).ok()) + .ok_or("parsing rp")?, + user: raw + .remove(&0x03) + .and_then(|v| if let Value::Map(v) = v { Some(v) } else { None }) + .and_then(|mut v| { + Some(User { + id: Base64UrlSafeData(value_to_vec_u8( + v.remove(&Value::Text("id".to_string()))?, + "id", + )?), + name: value_to_string(v.remove(&Value::Text("name".to_string()))?, "name")?, + display_name: value_to_string( + v.remove(&Value::Text("displayName".to_string()))?, + "displayName", + )?, + }) + }) + .ok_or("parsing user")?, + pub_key_cred_params: raw + .remove(&0x04) + .and_then(|v| serde_cbor::value::from_value(v).ok()) + .ok_or("parsing pubKeyCredParams")?, + exclude_list: raw + .remove(&0x05) + .and_then(|v| serde_cbor::value::from_value(v).ok()) + .unwrap_or_default(), + options: None, + pin_uv_auth_param: None, + pin_uv_auth_proto: None, + enterprise_attest: None, + }) + } +} + +impl From for BTreeMap { + fn from(value: MakeCredentialResponse) -> Self { + let MakeCredentialResponse { + fmt, + auth_data, + att_stmt, + epp_att, + large_blob_key, + } = value; + + let mut keys = BTreeMap::new(); + if let Some(fmt) = fmt { + keys.insert(0x01, Value::Text(fmt)); + } + if let Some(auth_data) = auth_data { + keys.insert(0x02, auth_data); + } + if let Some(att_stmt) = att_stmt { + keys.insert(0x03, att_stmt); + } + if let Some(epp_att) = epp_att { + keys.insert(0x04, epp_att.into()); + } + if let Some(large_blob_key) = large_blob_key { + keys.insert(0x05, large_blob_key); + } + keys + } +} + impl TryFrom> for MakeCredentialResponse { type Error = &'static str; fn try_from(mut raw: BTreeMap) -> Result { @@ -186,6 +276,7 @@ impl TryFrom> for MakeCredentialResponse { } } +crate::deserialize_cbor!(MakeCredentialRequest); crate::deserialize_cbor!(MakeCredentialResponse); #[cfg(test)] @@ -199,6 +290,7 @@ mod test { #[test] fn sample_make_credential_request() { + let _ = tracing_subscriber::fmt::try_init(); // https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#example-1a030b94 /* { @@ -290,6 +382,9 @@ mod test { assert_eq!(expected, req.cbor().expect("encode error")); + let decoded = ::try_from(&expected[1..]).unwrap(); + trace!(?decoded); + let r = vec![ 163, 1, 102, 112, 97, 99, 107, 101, 100, 2, 89, 0, 162, 0, 33, 245, 252, 11, 133, 205, 34, 230, 6, 35, 188, 215, 209, 202, 72, 148, 137, 9, 36, 155, 71, 118, 235, 81, 81, 84, diff --git a/webauthn-authenticator-rs/src/ctap2/commands/mod.rs b/webauthn-authenticator-rs/src/ctap2/commands/mod.rs index f38a6a2e..7dc06c8e 100644 --- a/webauthn-authenticator-rs/src/ctap2/commands/mod.rs +++ b/webauthn-authenticator-rs/src/ctap2/commands/mod.rs @@ -51,63 +51,61 @@ pub trait CBORCommand: Serialize + Sized + std::fmt::Debug + Send { /// Converts a CTAP v2 command into a binary form. fn cbor(&self) -> Result, serde_cbor::Error> { // CTAP v2.1, s8.2.9.1.2 (USB CTAPHID_CBOR), s8.3.5 (NFC framing). + // Similar form used for caBLE. // TODO: BLE is different, it includes a u16 length after the command? if !Self::HAS_PAYLOAD { return Ok(vec![Self::CMD]); } - // Canonical example returns 0x33 (PIN error) trace!("Sending: {:?}", self); - let b = /* if Self::CMD == 1 { - vec![168, 1, 88, 32, 104, 113, 52, 150, 130, 34, 236, 23, 32, 46, 66, 80, 95, 142, 210, 177, 106, 226, 47, 22, 187, 5, 184, 140, 37, 219, 158, 96, 38, 69, 241, 65, 2, 162, 98, 105, 100, 105, 116, 101, 115, 116, 46, 99, 116, 97, 112, 100, 110, 97, 109, 101, 105, 116, 101, 115, 116, 46, 99, 116, 97, 112, 3, 163, 98, 105, 100, 88, 32, 43, 102, 137, 187, 24, 244, 22, 159, 6, 159, 188, 223, 80, 203, 110, 163, 198, 10, 134, 27, 154, 123, 99, 148, 105, 131, 224, 181, 119, 183, 140, 112, 100, 110, 97, 109, 101, 113, 116, 101, 115, 116, 99, 116, 97, 112, 64, 99, 116, 97, 112, 46, 99, 111, 109, 107, 100, 105, 115, 112, 108, 97, 121, 78, 97, 109, 101, 105, 84, 101, 115, 116, 32, 67, 116, 97, 112, 4, 131, 162, 99, 97, 108, 103, 38, 100, 116, 121, 112, 101, 106, 112, 117, 98, 108, 105, 99, 45, 107, 101, 121, 162, 99, 97, 108, 103, 57, 1, 0, 100, 116, 121, 112, 101, 106, 112, 117, 98, 108, 105, 99, 45, 107, 101, 121, 162, 99, 97, 108, 103, 56, 36, 100, 116, 121, 112, 101, 106, 112, 117, 98, 108, 105, 99, 45, 107, 101, 121, 6, 161, 107, 104, 109, 97, 99, 45, 115, 101, 99, 114, 101, 116, 245, 7, 161, 98, 114, 107, 245, 8, 80, 252, 67, 170, 164, 17, 217, 72, 204, 108, 55, 6, 139, 141, 161, 213, 8, 9, 1] - } else */ { to_vec_packed(self)? }; + let mut b = to_vec_packed(self)?; trace!( - "CBOR payload: {:?}", + "CBOR: cmd={}, cbor={:?}", + Self::CMD, serde_cbor::from_slice::<'_, serde_cbor::Value>(&b[..]) ); - let mut x = Vec::with_capacity(b.len() + 1); - x.push(Self::CMD); - x.extend_from_slice(&b); - Ok(x) + + b.reserve(1); + b.insert(0, Self::CMD); + Ok(b) } +} - /// Converts a CTAP v2 command into a form suitable for transmission with - /// short ISO/IEC 7816-4 APDUs (over NFC). - fn to_short_apdus(&self) -> Result, serde_cbor::Error> { - let cbor = self.cbor()?; - let chunks = cbor.chunks(FRAG_MAX).rev(); - let mut o = Vec::with_capacity(chunks.len()); - let mut last = true; - - for chunk in chunks { - o.insert( - 0, - ISO7816RequestAPDU { - cla: if last { 0x80 } else { 0x90 }, - ins: 0x10, - p1: 0x00, - p2: 0x00, - data: chunk.to_vec(), - ne: if last { 256 } else { 0 }, - }, - ); - last = false; - } +/// Converts a CTAP v2 command into a form suitable for transmission with +/// short ISO/IEC 7816-4 APDUs (over NFC). +pub fn to_short_apdus(cbor: &[u8]) -> Vec { + let chunks = cbor.chunks(FRAG_MAX).rev(); + let mut o = Vec::with_capacity(chunks.len()); + let mut last = true; - Ok(o) + for chunk in chunks { + o.insert( + 0, + ISO7816RequestAPDU { + cla: if last { 0x80 } else { 0x90 }, + ins: 0x10, + p1: 0x00, + p2: 0x00, + data: chunk.to_vec(), + ne: if last { 256 } else { 0 }, + }, + ); + last = false; } - /// Converts a CTAP v2 command into a form suitable for transmission with - /// extended ISO/IEC 7816-4 APDUs (over NFC). - fn to_extended_apdu(&self) -> Result { - Ok(ISO7816RequestAPDU { - cla: 0x80, - ins: 0x10, - p1: 0, // 0x80, // client supports NFCCTAP_GETRESPONSE - p2: 0x00, - data: self.cbor()?, - ne: 65536, - }) + o +} + +/// Converts a CTAP v2 command into a form suitable for transmission with +/// extended ISO/IEC 7816-4 APDUs (over NFC). +pub fn to_extended_apdu(cbor: Vec) -> ISO7816RequestAPDU { + ISO7816RequestAPDU { + cla: 0x80, + ins: 0x10, + p1: 0, // 0x80, // client supports NFCCTAP_GETRESPONSE + p2: 0x00, + data: cbor, + ne: 65536, } } @@ -145,29 +143,33 @@ fn value_to_set_string(v: Value, loc: &str) -> Option> { } } -fn value_to_vec_u32(v: Value, loc: &str) -> Option> { +fn value_to_vec(v: Value, loc: &str) -> Option> { if let Value::Array(v) = v { - let x = v - .into_iter() - .filter_map(|i| { - if let Value::Integer(i) = i { - u32::try_from(i) - .map_err(|_| error!("Invalid value inside {}: {:?}", loc, i)) - .ok() - } else { - error!("Invalid type for {}: {:?}", loc, i); - None - } - }) - .collect(); - Some(x) + Some(v) + } else { + error!("Invalid type for {}: {:?}", loc, v); + None + } +} + +fn value_to_map(v: Value, loc: &str) -> Option> { + if let Value::Map(v) = v { + Some(v) } else { error!("Invalid type for {}: {:?}", loc, v); None } } -fn value_to_u32(v: &Value, loc: &str) -> Option { +fn value_to_vec_u32(v: Value, loc: &str) -> Option> { + value_to_vec(v, loc).map(|v| { + v.into_iter() + .filter_map(|i| value_to_u32(&i, loc)) + .collect() + }) +} + +pub(crate) fn value_to_u32(v: &Value, loc: &str) -> Option { if let Value::Integer(i) = v { u32::try_from(*i) .map_err(|_| error!("Invalid value inside {}: {:?}", loc, i)) @@ -178,8 +180,8 @@ fn value_to_u32(v: &Value, loc: &str) -> Option { } } -/// Converts a [Value::Bool] into [Option]. Returns `None` for other [Value] types. -fn value_to_bool(v: &Value, loc: &str) -> Option { +/// Converts a [`Value::Bool`] into [`Option`]. Returns [`Option::None`] for other [`Value`] types. +pub(crate) fn value_to_bool(v: &Value, loc: &str) -> Option { if let Value::Bool(b) = v { Some(*b) } else { @@ -188,8 +190,8 @@ fn value_to_bool(v: &Value, loc: &str) -> Option { } } -/// Converts a [Value::Bytes] into [Option>]. Returns `None` for other [Value] types. -fn value_to_vec_u8(v: Value, loc: &str) -> Option> { +/// Converts a [`Value::Bytes`] into [`Option>`]. Returns [`Option::None`] for other [`Value`] types. +pub(crate) fn value_to_vec_u8(v: Value, loc: &str) -> Option> { if let Value::Bytes(b) = v { Some(b) } else { @@ -198,7 +200,7 @@ fn value_to_vec_u8(v: Value, loc: &str) -> Option> { } } -fn value_to_string(v: Value, loc: &str) -> Option { +pub(crate) fn value_to_string(v: Value, loc: &str) -> Option { if let Value::Text(s) = v { Some(s) } else { @@ -217,6 +219,16 @@ impl CBORResponse for NoResponse { } } +fn map_int_keys(m: BTreeMap) -> Result, WebauthnCError> { + m.into_iter() + .map(|(k, v)| { + let k = value_to_u32(&k, "map_int_keys").ok_or(WebauthnCError::Internal)?; + + Ok((k, v)) + }) + .collect() +} + // TODO: switch to #derive #[macro_export] macro_rules! deserialize_cbor { @@ -224,14 +236,31 @@ macro_rules! deserialize_cbor { impl $crate::ctap2::commands::CBORResponse for $name { fn try_from(i: &[u8]) -> Result { if i.is_empty() { - TryFrom::try_from(BTreeMap::new()).map_err(|e| { + TryFrom::try_from(std::collections::BTreeMap::new()).map_err(|e| { error!("Tried to deserialise empty input, got error: {:?}", e); $crate::error::WebauthnCError::Cbor }) } else { - serde_cbor::from_slice(&i).map_err(|e| { + // Convert to Value (Value::Map) + let v = serde_cbor::from_slice::<'_, serde_cbor::Value>(&i).map_err(|e| { error!("deserialise: {:?}", e); $crate::error::WebauthnCError::Cbor + })?; + + // Extract the BTreeMap + let v = if let serde_cbor::Value::Map(v) = v { + Ok(v) + } else { + error!("deserialise: unexpected CBOR type {:?}", v); + Err($crate::error::WebauthnCError::Cbor) + }?; + + // Convert BTreeMap into BTreeMap + let v = $crate::ctap2::commands::map_int_keys(v)?; + + TryFrom::try_from(v).map_err(|_| { + error!("deserialising structure"); + $crate::error::WebauthnCError::Cbor }) } } diff --git a/webauthn-authenticator-rs/src/ctap2/commands/reset.rs b/webauthn-authenticator-rs/src/ctap2/commands/reset.rs index 2df8a743..1107b213 100644 --- a/webauthn-authenticator-rs/src/ctap2/commands/reset.rs +++ b/webauthn-authenticator-rs/src/ctap2/commands/reset.rs @@ -30,28 +30,24 @@ mod tests { let short = vec![0x80, 0x10, 0, 0, 1, 0x7, 0]; let ext = vec![0x80, 0x10, 0, 0, 0, 0, 1, 0x7, 0, 0]; - let a = req.to_short_apdus().unwrap(); + let a = to_short_apdus(&req.cbor().unwrap()); assert_eq!(1, a.len()); assert_eq!(short, a[0].to_bytes(&ISO7816LengthForm::ShortOnly).unwrap()); assert_eq!(short, a[0].to_bytes(&ISO7816LengthForm::Extended).unwrap()); assert_eq!( ext, - req.to_extended_apdu() - .unwrap() + to_extended_apdu(req.cbor().unwrap()) .to_bytes(&ISO7816LengthForm::Extended) .unwrap() ); assert_eq!( ext, - req.to_extended_apdu() - .unwrap() + to_extended_apdu(req.cbor().unwrap()) .to_bytes(&ISO7816LengthForm::ExtendedOnly) .unwrap() ); - assert!(req - .to_extended_apdu() - .unwrap() + assert!(to_extended_apdu(req.cbor().unwrap()) .to_bytes(&ISO7816LengthForm::ShortOnly) .is_err()); } diff --git a/webauthn-authenticator-rs/src/ctap2/commands/selection.rs b/webauthn-authenticator-rs/src/ctap2/commands/selection.rs index dc9b061a..96c5b98b 100644 --- a/webauthn-authenticator-rs/src/ctap2/commands/selection.rs +++ b/webauthn-authenticator-rs/src/ctap2/commands/selection.rs @@ -30,28 +30,24 @@ mod tests { let short = vec![0x80, 0x10, 0, 0, 1, 0xb, 0]; let ext = vec![0x80, 0x10, 0, 0, 0, 0, 1, 0xb, 0, 0]; - let a = req.to_short_apdus().unwrap(); + let a = to_short_apdus(&req.cbor().unwrap()); assert_eq!(1, a.len()); assert_eq!(short, a[0].to_bytes(&ISO7816LengthForm::ShortOnly).unwrap()); assert_eq!(short, a[0].to_bytes(&ISO7816LengthForm::Extended).unwrap()); assert_eq!( ext, - req.to_extended_apdu() - .unwrap() + to_extended_apdu(req.cbor().unwrap()) .to_bytes(&ISO7816LengthForm::Extended) .unwrap() ); assert_eq!( ext, - req.to_extended_apdu() - .unwrap() + to_extended_apdu(req.cbor().unwrap()) .to_bytes(&ISO7816LengthForm::ExtendedOnly) .unwrap() ); - assert!(req - .to_extended_apdu() - .unwrap() + assert!(to_extended_apdu(req.cbor().unwrap()) .to_bytes(&ISO7816LengthForm::ShortOnly) .is_err()); } diff --git a/webauthn-authenticator-rs/src/ctap2/ctap20.rs b/webauthn-authenticator-rs/src/ctap2/ctap20.rs index e43cad67..6e5c8ec7 100644 --- a/webauthn-authenticator-rs/src/ctap2/ctap20.rs +++ b/webauthn-authenticator-rs/src/ctap2/ctap20.rs @@ -1,18 +1,17 @@ use std::fmt::Debug; use crate::{ + authenticator_hashed::AuthenticatorBackendHashedClientData, ctap2::{commands::*, pin_uv::*}, error::WebauthnCError, transport::Token, ui::UiCallback, - util::{check_pin, compute_sha256, creation_to_clientdata, get_to_clientdata}, - AuthenticatorBackend, + util::check_pin, }; use base64urlsafedata::Base64UrlSafeData; use futures::executor::block_on; -use url::Url; use webauthn_rs_proto::{ AuthenticationExtensionsClientOutputs, AuthenticatorAssertionResponseRaw, AuthenticatorAttestationResponseRaw, PublicKeyCredential, RegisterPublicKeyCredential, @@ -44,18 +43,21 @@ impl<'a, T: Token, U: UiCallback> Ctap20Authenticator<'a, T, U> { } /// Perform a factory reset of the token, deleting all data. - pub async fn factory_reset(&self) -> Result<(), WebauthnCError> { + pub async fn factory_reset(&mut self) -> Result<(), WebauthnCError> { + let ui_callback = self.ui_callback; self.token - .transmit(ResetRequest {}, self.ui_callback) + .transmit(ResetRequest {}, ui_callback) .await .map(|_| ()) } async fn config( - &self, + &mut self, sub_command: ConfigSubCommand, bypass_always_uv: bool, ) -> Result<(), WebauthnCError> { + let ui_callback = self.ui_callback; + let (pin_uv_auth_proto, pin_uv_auth_param) = self .get_pin_uv_auth_token( sub_command.prf().as_slice(), @@ -69,7 +71,7 @@ impl<'a, T: Token, U: UiCallback> Ctap20Authenticator<'a, T, U> { self.token .transmit( ConfigRequest::new(sub_command, pin_uv_auth_proto, pin_uv_auth_param), - self.ui_callback, + ui_callback, ) .await .map(|_| ()) @@ -78,7 +80,7 @@ impl<'a, T: Token, U: UiCallback> Ctap20Authenticator<'a, T, U> { /// Toggles the state of the "Always Require User Verification" feature. /// /// - pub async fn toggle_always_uv(&self) -> Result<(), WebauthnCError> { + pub async fn toggle_always_uv(&mut self) -> Result<(), WebauthnCError> { self.config(ConfigSubCommand::ToggleAlwaysUv, true).await } @@ -86,7 +88,7 @@ impl<'a, T: Token, U: UiCallback> Ctap20Authenticator<'a, T, U> { /// /// pub async fn set_min_pin_length( - &self, + &mut self, new_min_pin_length: Option, min_pin_length_rpids: Vec, force_change_pin: Option, @@ -105,7 +107,7 @@ impl<'a, T: Token, U: UiCallback> Ctap20Authenticator<'a, T, U> { /// Enables the Enterprise Attestation feature. /// /// - pub async fn enable_enterprise_attestation(&self) -> Result<(), WebauthnCError> { + pub async fn enable_enterprise_attestation(&mut self) -> Result<(), WebauthnCError> { if self.info.get_option("ep").is_none() { return Err(WebauthnCError::NotSupported); } @@ -122,7 +124,8 @@ impl<'a, T: Token, U: UiCallback> Ctap20Authenticator<'a, T, U> { /// Sets a PIN on a device which does not already have one. /// /// To change a PIN, use [`change_pin()`][Self::change_pin]. - pub async fn set_new_pin(&self, pin: &str) -> Result<(), WebauthnCError> { + pub async fn set_new_pin(&mut self, pin: &str) -> Result<(), WebauthnCError> { + let ui_callback = self.ui_callback; let pin = self.validate_pin(pin)?; let mut padded_pin: [u8; 64] = [0; 64]; @@ -131,7 +134,7 @@ impl<'a, T: Token, U: UiCallback> Ctap20Authenticator<'a, T, U> { let iface = PinUvPlatformInterface::select_protocol(self.info.pin_protocols.as_ref())?; let p = iface.get_key_agreement_cmd(); - let ret = self.token.transmit(p, self.ui_callback).await?; + let ret = self.token.transmit(p, ui_callback).await?; let key_agreement = ret.key_agreement.ok_or(WebauthnCError::Internal)?; trace!(?key_agreement); @@ -141,7 +144,7 @@ impl<'a, T: Token, U: UiCallback> Ctap20Authenticator<'a, T, U> { trace!(?shared_secret); let set_pin = iface.set_pin_cmd(padded_pin, shared_secret.as_slice())?; - let ret = self.token.transmit(set_pin, self.ui_callback).await?; + let ret = self.token.transmit(set_pin, ui_callback).await?; trace!(?ret); Ok(()) } @@ -149,7 +152,9 @@ impl<'a, T: Token, U: UiCallback> Ctap20Authenticator<'a, T, U> { /// Changes a PIN on a device. /// /// To set a PIN for the first time, use [`set_new_pin()`][Self::set_new_pin]. - pub async fn change_pin(&self, old_pin: &str, new_pin: &str) -> Result<(), WebauthnCError> { + pub async fn change_pin(&mut self, old_pin: &str, new_pin: &str) -> Result<(), WebauthnCError> { + let ui_callback = self.ui_callback; + // TODO: we actually really only need this in normal form C let old_pin = self.validate_pin(old_pin)?; let new_pin = self.validate_pin(new_pin)?; @@ -159,12 +164,12 @@ impl<'a, T: Token, U: UiCallback> Ctap20Authenticator<'a, T, U> { let iface = PinUvPlatformInterface::select_protocol(self.info.pin_protocols.as_ref())?; let p = iface.get_key_agreement_cmd(); - let ret = self.token.transmit(p, self.ui_callback).await?; + let ret = self.token.transmit(p, ui_callback).await?; let key_agreement = ret.key_agreement.ok_or(WebauthnCError::Internal)?; let shared_secret = iface.encapsulate(key_agreement)?; let change_pin = iface.change_pin_cmd(&old_pin, padded_pin, &shared_secret)?; - let ret = self.token.transmit(change_pin, self.ui_callback).await?; + let ret = self.token.transmit(change_pin, ui_callback).await?; trace!(?ret); Ok(()) } @@ -192,7 +197,7 @@ impl<'a, T: Token, U: UiCallback> Ctap20Authenticator<'a, T, U> { /// References: /// * pub(super) async fn get_pin_uv_auth_token( - &self, + &mut self, client_data_hash: &[u8], permissions: Permissions, rp_id: Option, @@ -216,7 +221,7 @@ impl<'a, T: Token, U: UiCallback> Ctap20Authenticator<'a, T, U> { } pub(super) async fn get_pin_uv_auth_session( - &self, + &mut self, permissions: Permissions, rp_id: Option, bypass_always_uv: bool, @@ -232,6 +237,7 @@ impl<'a, T: Token, U: UiCallback> Ctap20Authenticator<'a, T, U> { return Err(WebauthnCError::Internal); } + let ui_callback = self.ui_callback; let client_pin = self.info.get_option("clientPin"); let always_uv = self.info.get_option("alwaysUv"); let make_cred_uv_not_required = self.info.get_option("makeCredUvNotRqd"); @@ -279,7 +285,7 @@ impl<'a, T: Token, U: UiCallback> Ctap20Authenticator<'a, T, U> { // 6.5.5.4: Obtaining the shared secret let p = iface.get_key_agreement_cmd(); - let ret = self.token.transmit(p, self.ui_callback).await?; + let ret = self.token.transmit(p, ui_callback).await?; let key_agreement = ret.key_agreement.ok_or(WebauthnCError::Internal)?; trace!(?key_agreement); @@ -312,7 +318,7 @@ impl<'a, T: Token, U: UiCallback> Ctap20Authenticator<'a, T, U> { } }; - let ret = self.token.transmit(p, self.ui_callback).await?; + let ret = self.token.transmit(p, ui_callback).await?; trace!(?ret); let pin_token = ret .pin_uv_auth_token @@ -324,38 +330,36 @@ impl<'a, T: Token, U: UiCallback> Ctap20Authenticator<'a, T, U> { Ok((Some(iface), Some(pin_token))) } - async fn request_pin(&self, pin_uv_protocol: Option) -> Result { + async fn request_pin( + &mut self, + pin_uv_protocol: Option, + ) -> Result { let p = ClientPinRequest { pin_uv_protocol, sub_command: ClientPinSubCommand::GetPinRetries, ..Default::default() }; - let ret = self.token.transmit(p, self.ui_callback).await?; + let ui_callback = self.ui_callback; + let ret = self.token.transmit(p, ui_callback).await?; trace!(?ret); // TODO: handle lockouts - self.ui_callback - .request_pin() - .ok_or(WebauthnCError::Cancelled) + ui_callback.request_pin().ok_or(WebauthnCError::Cancelled) } } -impl<'a, T: Token, U: UiCallback> AuthenticatorBackend for Ctap20Authenticator<'a, T, U> { +impl<'a, T: Token, U: UiCallback> AuthenticatorBackendHashedClientData + for Ctap20Authenticator<'a, T, U> +{ fn perform_register( &mut self, - origin: Url, + client_data_hash: Vec, options: webauthn_rs_proto::PublicKeyCredentialCreationOptions, _timeout_ms: u32, ) -> Result { - let client_data = creation_to_clientdata(origin, options.challenge.clone()); - let client_data: Vec = serde_json::to_string(&client_data) - .map_err(|_| WebauthnCError::Json)? - .into(); - let client_data_hash = compute_sha256(&client_data).to_vec(); - let (pin_uv_auth_proto, pin_uv_auth_param) = block_on(self.get_pin_uv_auth_token( client_data_hash.as_slice(), Permissions::MAKE_CREDENTIAL, @@ -405,7 +409,7 @@ impl<'a, T: Token, U: UiCallback> AuthenticatorBackend for Ctap20Authenticator<' extensions: RegistrationExtensionsClientOutputs::default(), // TODO response: AuthenticatorAttestationResponseRaw { attestation_object: Base64UrlSafeData(raw), - client_data_json: Base64UrlSafeData(client_data), + client_data_json: Base64UrlSafeData(vec![]), // All transports the token supports, as opposed to the // transport which was actually used. transports: self.info.get_transports(), @@ -415,16 +419,11 @@ impl<'a, T: Token, U: UiCallback> AuthenticatorBackend for Ctap20Authenticator<' fn perform_auth( &mut self, - origin: Url, + client_data_hash: Vec, options: webauthn_rs_proto::PublicKeyCredentialRequestOptions, _timeout_ms: u32, ) -> Result { trace!("trying to authenticate..."); - let client_data = get_to_clientdata(origin, options.challenge.clone()); - let client_data: Vec = serde_json::to_string(&client_data) - .map_err(|_| WebauthnCError::Json)? - .into(); - let client_data_hash = compute_sha256(&client_data).to_vec(); let (pin_uv_auth_proto, pin_uv_auth_param) = block_on(self.get_pin_uv_auth_token( client_data_hash.as_slice(), @@ -437,7 +436,7 @@ impl<'a, T: Token, U: UiCallback> AuthenticatorBackend for Ctap20Authenticator<' rp_id: options.rp_id, client_data_hash, allow_list: options.allow_credentials, - options: None, // TODO + options: None, pin_uv_auth_param, pin_uv_auth_proto, }; @@ -464,7 +463,7 @@ impl<'a, T: Token, U: UiCallback> AuthenticatorBackend for Ctap20Authenticator<' raw_id, response: AuthenticatorAssertionResponseRaw { authenticator_data, - client_data_json: Base64UrlSafeData(client_data), + client_data_json: Base64UrlSafeData(vec![]), signature, // TODO user_handle: None, diff --git a/webauthn-authenticator-rs/src/ctap2/ctap21.rs b/webauthn-authenticator-rs/src/ctap2/ctap21.rs index d176025d..44777b77 100644 --- a/webauthn-authenticator-rs/src/ctap2/ctap21.rs +++ b/webauthn-authenticator-rs/src/ctap2/ctap21.rs @@ -53,21 +53,22 @@ impl<'a, T: Token, U: UiCallback> Ctap21Authenticator<'a, T, U> { /// Requests user presence on a token. /// /// This feature is only available in `FIDO_V2_1`, and not available for NFC. - pub async fn selection(&self) -> Result<(), WebauthnCError> { + pub async fn selection(&mut self) -> Result<(), WebauthnCError> { if !self.token.has_button() { // The token doesn't have a button on a transport level (ie: NFC), // so immediately mark this as the "selected" token. Ok(()) } else { + let ui_callback = self.ui_callback; self.token - .transmit(SelectionRequest {}, self.ui_callback) + .transmit(SelectionRequest {}, ui_callback) .await .map(|_| ()) } } async fn bio( - &self, + &mut self, sub_command: BioSubCommand, ) -> Result { let (pin_uv_auth_proto, pin_uv_auth_param) = self @@ -79,17 +80,18 @@ impl<'a, T: Token, U: UiCallback> Ctap21Authenticator<'a, T, U> { ) .await?; + let ui_callback = self.ui_callback; self.token .transmit( BioEnrollmentRequest::new(sub_command, pin_uv_auth_proto, pin_uv_auth_param), - self.ui_callback, + ui_callback, ) .await } /// Send a [BioSubCommand] using a provided `pin_uv_auth_token` session. async fn bio_with_session( - &self, + &mut self, sub_command: BioSubCommand, iface: Option<&PinUvPlatformInterface>, pin_uv_auth_token: Option<&Vec>, @@ -108,22 +110,24 @@ impl<'a, T: Token, U: UiCallback> Ctap21Authenticator<'a, T, U> { _ => (None, None), }; + let ui_callback = self.ui_callback; self.token .transmit( BioEnrollmentRequest::new(sub_command, pin_uv_protocol, pin_uv_auth_param), - self.ui_callback, + ui_callback, ) .await } /// Checks that the device supports fingerprints. - async fn check_fingerprint_support(&self) -> Result<(), WebauthnCError> { + async fn check_fingerprint_support(&mut self) -> Result<(), WebauthnCError> { // TODO: handle CTAP_2_1_PRE version too if !self.info.supports_ctap21_biometrics() { return Err(WebauthnCError::NotSupported); } - let r = self.token.transmit(GET_MODALITY, self.ui_callback).await?; + let ui_callback = self.ui_callback; + let r = self.token.transmit(GET_MODALITY, ui_callback).await?; if r.modality != Some(Modality::Fingerprint) { return Err(WebauthnCError::NotSupported); } @@ -132,10 +136,14 @@ impl<'a, T: Token, U: UiCallback> Ctap21Authenticator<'a, T, U> { } /// Checks that a given `friendly_name` complies with authenticator limits, and returns the value in Unicode Normal Form C. - async fn check_friendly_name(&self, friendly_name: String) -> Result { + async fn check_friendly_name( + &mut self, + friendly_name: String, + ) -> Result { + let ui_callback = self.ui_callback; let r = self .token - .transmit(GET_FINGERPRINT_SENSOR_INFO, self.ui_callback) + .transmit(GET_FINGERPRINT_SENSOR_INFO, ui_callback) .await?; // Normalise into Normal Form C @@ -151,11 +159,12 @@ impl<'a, T: Token, U: UiCallback> Ctap21Authenticator<'a, T, U> { /// /// Returns [WebauthnCError::NotSupported] if the token does not support fingerprint authentication. pub async fn get_fingerprint_sensor_info( - &self, + &mut self, ) -> Result { self.check_fingerprint_support().await?; + let ui_callback = self.ui_callback; self.token - .transmit(GET_FINGERPRINT_SENSOR_INFO, self.ui_callback) + .transmit(GET_FINGERPRINT_SENSOR_INFO, ui_callback) .await } @@ -167,7 +176,7 @@ impl<'a, T: Token, U: UiCallback> Ctap21Authenticator<'a, T, U> { /// /// Returns [WebauthnCError::NotSupported] if the token does not support fingerprint authentication. pub async fn enroll_fingerprint( - &self, + &mut self, timeout: Duration, friendly_name: Option, ) -> Result, WebauthnCError> { @@ -232,7 +241,7 @@ impl<'a, T: Token, U: UiCallback> Ctap21Authenticator<'a, T, U> { /// Lists all enrolled fingerprints in the device. /// /// Returns [WebauthnCError::NotSupported] if the token does not support fingerprint authentication. - pub async fn list_fingerprints(&self) -> Result, WebauthnCError> { + pub async fn list_fingerprints(&mut self) -> Result, WebauthnCError> { // TODO: handle CTAP_2_1_PRE version too self.check_fingerprint_support().await?; @@ -246,7 +255,7 @@ impl<'a, T: Token, U: UiCallback> Ctap21Authenticator<'a, T, U> { /// Renames an enrolled fingerprint. pub async fn rename_fingerprint( - &self, + &mut self, id: Vec, friendly_name: String, ) -> Result<(), WebauthnCError> { @@ -262,7 +271,7 @@ impl<'a, T: Token, U: UiCallback> Ctap21Authenticator<'a, T, U> { } /// Removes an enrolled fingerprint. - pub async fn remove_fingerprint(&self, id: Vec) -> Result<(), WebauthnCError> { + pub async fn remove_fingerprint(&mut self, id: Vec) -> Result<(), WebauthnCError> { // TODO: handle CTAP_2_1_PRE version too self.check_fingerprint_support().await?; @@ -278,7 +287,7 @@ impl<'a, T: Token, U: UiCallback> Ctap21Authenticator<'a, T, U> { /// further processing will stop, and the request may be incomplete. /// /// Call [Self::list_fingerprints] to check what was actually done. - pub async fn remove_fingerprints(&self, ids: Vec>) -> Result<(), WebauthnCError> { + pub async fn remove_fingerprints(&mut self, ids: Vec>) -> Result<(), WebauthnCError> { let (iface, pin_uv_auth_token) = self .get_pin_uv_auth_session(Permissions::BIO_ENROLLMENT, None, false) .await?; diff --git a/webauthn-authenticator-rs/src/ctap2/mod.rs b/webauthn-authenticator-rs/src/ctap2/mod.rs index 7d17c081..22d68929 100644 --- a/webauthn-authenticator-rs/src/ctap2/mod.rs +++ b/webauthn-authenticator-rs/src/ctap2/mod.rs @@ -86,7 +86,7 @@ //! * `key_manager` will connect to a key, pull hardware information, and let //! you reconfigure the key (reset, PIN, fingerprints, etc.) //! -//! * `authenticate` works with any [AuthenticatorBackend], including +//! * `authenticate` works with any [crate::AuthenticatorBackend], including //! [CtapAuthenticator]. //! //! ## Device-specific issues @@ -185,14 +185,14 @@ use std::ops::{Deref, DerefMut}; use futures::stream::FuturesUnordered; use futures::{select, StreamExt}; +use crate::authenticator_hashed::AuthenticatorBackendHashedClientData; use crate::error::WebauthnCError; use crate::transport::Token; use crate::ui::UiCallback; -use crate::AuthenticatorBackend; pub use self::commands::EnrollSampleStatus; use self::commands::GetInfoRequest; -pub use self::commands::{CBORCommand, CBORResponse}; +pub use self::commands::{CBORCommand, CBORResponse, GetInfoResponse}; pub use self::{ctap20::Ctap20Authenticator, ctap21::Ctap21Authenticator}; /// Abstraction for different versions of the CTAP2 protocol. @@ -218,6 +218,17 @@ impl<'a, T: Token, U: UiCallback> CtapAuthenticator<'a, T, U> { token.init().await.ok()?; let info = token.transmit(GetInfoRequest {}, ui_callback).await.ok()?; + Self::new_with_info(info, token, ui_callback) + } + + /// Creates a connection to an already-initialized token, and gets a reference to the highest supported FIDO version. + /// + /// Returns `None` if we don't support any version of CTAP which the token supports. + pub(crate) fn new_with_info( + info: GetInfoResponse, + token: T, + ui_callback: &'a U, + ) -> Option> { if info.versions.contains(FIDO_2_1) { Some(Self::Fido21(Ctap21Authenticator::new( info, @@ -279,24 +290,37 @@ impl<'a, T: Token, U: UiCallback> DerefMut for CtapAuthenticator<'a, T, U> { } } -/// Wrapper for [Ctap20Authenticator]'s implementation of [AuthenticatorBackend]. -impl<'a, T: Token, U: UiCallback> AuthenticatorBackend for CtapAuthenticator<'a, T, U> { +/// Wrapper for [Ctap20Authenticator]'s implementation of +/// [AuthenticatorBackendHashedClientData]. +impl<'a, T: Token, U: UiCallback> AuthenticatorBackendHashedClientData + for CtapAuthenticator<'a, T, U> +{ fn perform_register( &mut self, - origin: url::Url, + client_data_hash: Vec, options: webauthn_rs_proto::PublicKeyCredentialCreationOptions, timeout_ms: u32, ) -> Result { - Ctap20Authenticator::perform_register(self, origin, options, timeout_ms) + as AuthenticatorBackendHashedClientData>::perform_register( + self, + client_data_hash, + options, + timeout_ms, + ) } fn perform_auth( &mut self, - origin: url::Url, + client_data_hash: Vec, options: webauthn_rs_proto::PublicKeyCredentialRequestOptions, timeout_ms: u32, ) -> Result { - Ctap20Authenticator::perform_auth(self, origin, options, timeout_ms) + as AuthenticatorBackendHashedClientData>::perform_auth( + self, + client_data_hash, + options, + timeout_ms, + ) } } @@ -305,8 +329,8 @@ impl<'a, T: Token, U: UiCallback> AuthenticatorBackend for CtapAuthenticator<'a, /// This only works on NFC authenticators and CTAP 2.1 (not "2.1 PRE") /// authenticators. pub async fn select_one_token<'a, T: Token + 'a, U: UiCallback + 'a>( - tokens: impl Iterator>, -) -> Option<&'a CtapAuthenticator<'a, T, U>> { + tokens: impl Iterator>, +) -> Option<&'a mut CtapAuthenticator<'a, T, U>> { let mut tasks: FuturesUnordered<_> = tokens .map(|token| async move { if !token.token.has_button() { diff --git a/webauthn-authenticator-rs/src/ctap2/pin_uv.rs b/webauthn-authenticator-rs/src/ctap2/pin_uv.rs index 72ab14b8..3c733d5f 100644 --- a/webauthn-authenticator-rs/src/ctap2/pin_uv.rs +++ b/webauthn-authenticator-rs/src/ctap2/pin_uv.rs @@ -1,14 +1,15 @@ -use crate::{error::WebauthnCError, util::compute_sha256}; +use crate::{ + crypto::{decrypt, ecdh, encrypt, get_group, hkdf_sha_256, regenerate}, + error::WebauthnCError, + util::compute_sha256, +}; use openssl::{ bn, - ec::{self, EcKey, EcKeyRef}, + ec::{EcKey, EcKeyRef}, hash, - md::Md, - nid, pkey::{PKey, Private}, - pkey_ctx::PkeyCtx, rand::rand_bytes, - sign, symm, + sign, }; use std::{fmt::Debug, ops::Deref}; use webauthn_rs_core::proto::{COSEEC2Key, COSEKey, COSEKeyType, ECDSACurve}; @@ -81,7 +82,6 @@ impl PinUvPlatformInterface { } } } - Err(WebauthnCError::NotSupported) } @@ -96,7 +96,7 @@ impl PinUvPlatformInterface { // 3. Let Z be the 32-byte, big-endian encoding of the x-coordinate of the shared point. let mut z: [u8; 32] = [0; 32]; if let COSEKeyType::EC_EC2(ec) = peer_cose_key.key { - ecdh(self.private_key.as_ref(), &ec, &mut z)?; + ecdh(self.private_key.to_owned(), (&ec).try_into()?, &mut z)?; } else { error!("Unexpected peer key type: {:?}", peer_cose_key); return Err(WebauthnCError::Internal); @@ -231,40 +231,6 @@ impl PinUvPlatformInterface { } } -/// Encrypts some data using AES-256-CBC, with no padding. -/// -/// `plaintext.len()` must be a multiple of the cipher's blocksize. -fn encrypt(key: &[u8], iv: Option<&[u8]>, plaintext: &[u8]) -> Result, WebauthnCError> { - let cipher = symm::Cipher::aes_256_cbc(); - let mut ct = vec![0; plaintext.len() + cipher.block_size()]; - let mut c = symm::Crypter::new(cipher, symm::Mode::Encrypt, key, iv)?; - c.pad(false); - let l = c.update(plaintext, &mut ct)?; - let l = l + c.finalize(&mut ct[l..])?; - ct.truncate(l); - Ok(ct) -} - -fn decrypt(key: &[u8], iv: Option<&[u8]>, ciphertext: &[u8]) -> Result, WebauthnCError> { - let cipher = openssl::symm::Cipher::aes_256_cbc(); - if ciphertext.len() % cipher.block_size() != 0 { - error!( - "ciphertext length {} is not a multiple of {} bytes", - ciphertext.len(), - cipher.block_size() - ); - return Err(WebauthnCError::Internal); - } - - let mut pt = vec![0; ciphertext.len() + cipher.block_size()]; - let mut c = symm::Crypter::new(cipher, symm::Mode::Decrypt, key, iv)?; - c.pad(false); - let l = c.update(ciphertext, &mut pt)?; - let l = l + c.finalize(&mut pt[l..])?; - pt.truncate(l); - Ok(pt) -} - pub trait PinUvPlatformInterfaceProtocol { fn kdf(&self, z: &[u8]) -> Result, WebauthnCError>; @@ -394,8 +360,8 @@ impl PinUvPlatformInterfaceProtocol for PinUvPlatformInterfaceProtocolTwo { // (see [RFC5869] for the definition of HKDF). let mut o: Vec = vec![0; 64]; let zero: [u8; 32] = [0; 32]; - hkdf_sha_256(&zero, z, b"CTAP2 HMAC key", &mut o[0..32])?; - hkdf_sha_256(&zero, z, b"CTAP2 AES key", &mut o[32..64])?; + hkdf_sha_256(&zero, z, Some(b"CTAP2 HMAC key"), &mut o[0..32])?; + hkdf_sha_256(&zero, z, Some(b"CTAP2 AES key"), &mut o[32..64])?; Ok(o) } @@ -405,71 +371,9 @@ impl PinUvPlatformInterfaceProtocol for PinUvPlatformInterfaceProtocolTwo { } } -fn hkdf_sha_256( - salt: &[u8], - ikm: &[u8], - info: &[u8], - output: &mut [u8], -) -> Result<(), openssl::error::ErrorStack> { - let mut ctx = PkeyCtx::new_id(openssl::pkey::Id::HKDF)?; - ctx.derive_init()?; - ctx.set_hkdf_md(Md::sha256())?; - ctx.set_hkdf_salt(salt)?; - ctx.set_hkdf_key(ikm)?; - ctx.add_hkdf_info(info)?; - ctx.derive(Some(output))?; - Ok(()) -} - -fn ecdh( - private_key: &EcKeyRef, - peer_key: &COSEEC2Key, - output: &mut [u8], -) -> Result<(), openssl::error::ErrorStack> { - // let mut ctx = BigNumContext::new()?; - - // let mut x = BigNum::new()?; - // let mut y = BigNum::new()?; - // let peer_key: EcKey = peer_key.into(); - // let peer_key_pub = peer_key.public_key(); - - // let pk = private_key.private_key(); - // let group = private_key.group(); - - // let mut pt = EcPoint::new(group)?; - // pt.mul(group, peer_key_pub, pk, &ctx)?; - // pt.affine_coordinates_gfp(group, &mut x, &mut y, &mut ctx)?; - - // let buflen = (group.degree() + 7) / 8; - // trace!(?buflen); - // let x = x.to_vec(); - // trace!(?x); - //output.copy_from_slice(x.as_slice()); - - // Both the low level and high level return same outputs. - let peer_key = PKey::from_ec_key(peer_key.try_into()?)?; - let pkey = PKey::from_ec_key(private_key.to_owned())?; - let mut ctx = PkeyCtx::new(&pkey)?; - ctx.derive_init()?; - ctx.derive_set_peer(&peer_key)?; - ctx.derive(Some(output))?; - // trace!(?output); - - Ok(()) -} - -/// Generate a fresh, random P-256 private key, x, and compute the associated public point. -fn regenerate() -> Result, openssl::error::ErrorStack> { - // Create a new key. - let ecgroup = ec::EcGroup::from_curve_name(nid::Nid::X9_62_PRIME256V1)?; - let eckey = ec::EcKey::generate(&ecgroup)?; - - Ok(eckey) -} - -/// Gets the public key for a private key as [COSEKey]. -fn get_public_key(private_key: &EcKeyRef) -> Result { - let ecgroup = ec::EcGroup::from_curve_name(nid::Nid::X9_62_PRIME256V1)?; +/// Gets the public key for a private key as [COSEKey] for PinUvProtocol. +fn get_public_key(private_key: &EcKeyRef) -> Result { + let ecgroup = get_group()?; // Extract the public x and y coords. let ecpub_points = private_key.public_key(); @@ -492,27 +396,10 @@ fn get_public_key(private_key: &EcKeyRef) -> Result = (0..0x0d).collect(); - let ikm: [u8; 22] = [0x0b; 22]; - let info: Vec = (0xf0..0xfa).collect(); - let expected: [u8; 42] = [ - 0x3c, 0xb2, 0x5f, 0x25, 0xfa, 0xac, 0xd5, 0x7a, 0x90, 0x43, 0x4f, 0x64, 0xd0, 0x36, - 0x2f, 0x2a, 0x2d, 0x2d, 0xa, 0x90, 0xcf, 0x1a, 0x5a, 0x4c, 0x5d, 0xb0, 0x2d, 0x56, - 0xec, 0xc4, 0xc5, 0xbf, 0x34, 0x0, 0x72, 0x8, 0xd5, 0xb8, 0x87, 0x18, 0x58, 0x65, - ]; - - let mut output: [u8; 42] = [0; 42]; - - hkdf_sha_256(salt.as_slice(), &ikm, info.as_slice(), &mut output) - .expect("hkdf_sha_256 fail"); - assert_eq!(expected, output); - } - #[test] fn pin_encryption_and_hashing() { // https://github.com/mozilla/authenticator-rs/blob/f2d255c48d3e3762a27873b270520072bf501d0e/src/crypto/mod.rs#L833 @@ -579,7 +466,7 @@ mod tests { }; let mut ctx = bn::BigNumContext::new().unwrap(); - let group = ec::EcGroup::from_curve_name(nid::Nid::X9_62_PRIME256V1).unwrap(); + let group = get_group().unwrap(); let x = bn::BigNum::from_hex_str( "44D78D7989B97E62EA993496C9EF6E8FD58B8B00715F9A89153DDD9C4657E47F", ) diff --git a/webauthn-authenticator-rs/src/error.rs b/webauthn-authenticator-rs/src/error.rs index f4391205..6805df06 100644 --- a/webauthn-authenticator-rs/src/error.rs +++ b/webauthn-authenticator-rs/src/error.rs @@ -19,6 +19,8 @@ pub enum WebauthnCError { InvalidAssertion, MessageTooLarge, MessageTooShort, + /// Message was an unexpected length + InvalidMessageLength, Cancelled, Ctap(CtapError), /// The PIN was too short. @@ -53,6 +55,7 @@ pub enum WebauthnCError { Checksum, /// The card reported as a PC/SC storage card, rather than a smart card. StorageCard, + IoError(String), } #[cfg(feature = "nfc")] @@ -91,6 +94,12 @@ impl From for WebauthnCError { } } +impl From for WebauthnCError { + fn from(v: std::io::Error) -> Self { + Self::IoError(v.to_string()) + } +} + /// #[derive(Debug, PartialEq, Eq)] pub enum CtapError { @@ -217,6 +226,62 @@ impl From for CtapError { } } +impl From for u8 { + fn from(e: CtapError) -> Self { + use CtapError::*; + match e { + Ok => 0x00, + Ctap1InvalidCommand => 0x01, + Ctap1InvalidParameter => 0x02, + Ctap1InvalidLength => 0x03, + Ctap1InvalidSeq => 0x04, + Ctap1Timeout => 0x05, + Ctap1ChannelBusy => 0x06, + Ctap1LockRequired => 0x0a, + Ctap1InvalidChannel => 0x0b, + Ctap2CborUnexpectedType => 0x11, + Ctap2InvalidCBOR => 0x12, + Ctap2MissingParameter => 0x14, + Ctap2LimitExceeded => 0x15, + Ctap2FingerprintDatabaseFull => 0x17, + Ctap2LargeBlobStorageFull => 0x18, + Ctap2CredentialExcluded => 0x19, + Ctap2Processing => 0x21, + Ctap2InvalidCredential => 0x22, + Ctap2UserActionPending => 0x23, + Ctap2OperationPending => 0x24, + Ctap2NoOperations => 0x25, + Ctap2UnsupportedAlgorithm => 0x26, + Ctap2OperationDenied => 0x27, + Ctap2KeyStoreFull => 0x28, + Ctap2UnsupportedOption => 0x2b, + Ctap2InvalidOption => 0x2c, + Ctap2KeepAliveCancel => 0x2d, + Ctap2NoCredentials => 0x2e, + Ctap2UserActionTimeout => 0x2f, + Ctap2NotAllowed => 0x30, + Ctap2PinInvalid => 0x31, + Ctap2PinBlocked => 0x32, + Ctap2PinAuthInvalid => 0x33, + Ctap2PinAuthBlocked => 0x34, + Ctap2PinNotSet => 0x35, + Ctap2PUATRequired => 0x36, + Ctap2PinPolicyViolation => 0x37, + Ctap2RequestTooLarge => 0x39, + Ctap2ActionTimeout => 0x3a, + Ctap2UserPresenceRequired => 0x3b, + Ctap2UserVerificationBlocked => 0x3c, + Ctap2IntegrityFailure => 0x3d, + Ctap2InvalidSubcommand => 0x3e, + Ctap2UserVerificationInvalid => 0x3f, + Ctap2UnauthorizedPermission => 0x40, + Ctap1Unspecified => 0x7f, + Ctap2LastError => 0xdf, + Unknown(e) => e, + } + } +} + impl From for WebauthnCError { fn from(e: CtapError) -> Self { Self::Ctap(e) diff --git a/webauthn-authenticator-rs/src/lib.rs b/webauthn-authenticator-rs/src/lib.rs index 85cfdf9b..4df0efe9 100644 --- a/webauthn-authenticator-rs/src/lib.rs +++ b/webauthn-authenticator-rs/src/lib.rs @@ -35,11 +35,14 @@ pub mod prelude { }; } +mod authenticator_hashed; +mod crypto; pub mod ctap2; pub mod error; pub mod softpasskey; pub mod softtoken; pub mod transport; +pub mod types; pub mod ui; mod util; @@ -55,6 +58,10 @@ pub mod u2fhid; #[cfg(feature = "win10")] pub mod win10; +pub use crate::authenticator_hashed::{ + perform_auth_with_request, perform_register_with_request, AuthenticatorBackendHashedClientData, +}; + pub struct WebauthnAuthenticator where T: AuthenticatorBackend, diff --git a/webauthn-authenticator-rs/src/nfc/mod.rs b/webauthn-authenticator-rs/src/nfc/mod.rs index 9a8bb66b..ddc665f9 100644 --- a/webauthn-authenticator-rs/src/nfc/mod.rs +++ b/webauthn-authenticator-rs/src/nfc/mod.rs @@ -1,4 +1,5 @@ //! [NFCReader] communicates with a FIDO token over NFC, using the [pcsc] API. +use crate::ctap2::commands::to_short_apdus; use crate::error::{CtapError, WebauthnCError}; use crate::ui::UiCallback; @@ -16,7 +17,6 @@ mod atr; mod tlv; pub use self::atr::*; -use super::ctap2::*; use crate::transport::iso7816::*; use crate::transport::*; @@ -300,7 +300,11 @@ impl NFCCard { let card = reader .ctx - .connect(reader_name, ShareMode::Shared, Protocols::ANY)?; + .connect(reader_name, ShareMode::Exclusive, Protocols::ANY) + .map_err(|e| { + error!("Error connecting to card: {:?}", e); + e + })?; Ok(NFCCard { card: Mutex::new(card), @@ -329,9 +333,8 @@ impl Token for NFCCard { false } - async fn transmit_raw(&self, cmd: C, _ui: &U) -> Result, WebauthnCError> + async fn transmit_raw(&mut self, cmd: &[u8], _ui: &U) -> Result, WebauthnCError> where - C: CBORCommand, U: UiCallback, { // let apdu = cmd.to_extended_apdu().map_err(|_| WebauthnCError::Cbor)?; @@ -343,7 +346,7 @@ impl Token for NFCCard { // resp = self.transmit(&NFCCTAP_GETRESPONSE, &ISO7816LengthForm::ExtendedOnly)?; // }; - let apdus = cmd.to_short_apdus().map_err(|_| WebauthnCError::Cbor)?; + let apdus = to_short_apdus(cmd); let guard = self.card.lock()?; let resp = transmit_chunks(guard.deref(), &apdus)?; let mut data = resp.data; @@ -368,7 +371,7 @@ impl Token for NFCCard { let resp = transmit( guard.deref(), &select_by_df_name(&APPLET_DF), - &ISO7816LengthForm::ExtendedOnly, + &ISO7816LengthForm::ShortOnly, )?; if !resp.is_ok() { @@ -384,7 +387,7 @@ impl Token for NFCCard { Ok(()) } - fn close(&self) -> Result<(), WebauthnCError> { + async fn close(&mut self) -> Result<(), WebauthnCError> { let guard = self.card.lock()?; let resp = transmit( guard.deref(), diff --git a/webauthn-authenticator-rs/src/softpasskey.rs b/webauthn-authenticator-rs/src/softpasskey.rs index 4050c856..83eca00b 100644 --- a/webauthn-authenticator-rs/src/softpasskey.rs +++ b/webauthn-authenticator-rs/src/softpasskey.rs @@ -1,8 +1,8 @@ +use crate::authenticator_hashed::AuthenticatorBackendHashedClientData; +use crate::crypto::get_group; use crate::error::WebauthnCError; use crate::util::compute_sha256; -use crate::AuthenticatorBackend; -use crate::Url; -use openssl::{bn, ec, hash, nid, pkey, rand, sign}; +use openssl::{bn, ec, hash, pkey, rand, sign}; use serde_cbor::value::Value; use std::collections::BTreeMap; use std::collections::HashMap; @@ -12,8 +12,8 @@ use base64urlsafedata::Base64UrlSafeData; use webauthn_rs_proto::{ AllowCredentials, AuthenticationExtensionsClientOutputs, AuthenticatorAssertionResponseRaw, - AuthenticatorAttachment, AuthenticatorAttestationResponseRaw, CollectedClientData, - PublicKeyCredential, PublicKeyCredentialCreationOptions, PublicKeyCredentialRequestOptions, + AuthenticatorAttachment, AuthenticatorAttestationResponseRaw, PublicKeyCredential, + PublicKeyCredentialCreationOptions, PublicKeyCredentialRequestOptions, RegisterPublicKeyCredential, RegistrationExtensionsClientOutputs, UserVerificationPolicy, }; @@ -45,10 +45,10 @@ pub struct U2FSignData { user_present: u8, } -impl AuthenticatorBackend for SoftPasskey { +impl AuthenticatorBackendHashedClientData for SoftPasskey { fn perform_register( &mut self, - origin: Url, + client_data_json_hash: Vec, options: PublicKeyCredentialCreationOptions, _timeout_ms: u32, ) -> Result { @@ -90,36 +90,6 @@ impl AuthenticatorBackend for SoftPasskey { // Set authenticatorExtensions[extensionId] to the base64url encoding of authenticatorExtensionInput. */ - // Let collectedClientData be a new CollectedClientData instance whose fields are: - // type - // The string "webauthn.create". - // challenge - // The base64url encoding of options.challenge. - // origin - // The serialization of callerOrigin. - - // Not Supported Yet. - // tokenBinding - // The status of Token Binding between the client and the callerOrigin, as well as the Token Binding ID associated with callerOrigin, if one is available. - let collected_client_data = CollectedClientData { - type_: "webauthn.create".to_string(), - challenge: options.challenge.clone(), - origin, - token_binding: None, - cross_origin: None, - unknown_keys: BTreeMap::new(), - }; - - // Let clientDataJSON be the JSON-serialized client data constructed from collectedClientData. - let client_data_json = - serde_json::to_string(&collected_client_data).map_err(|_| WebauthnCError::Json)?; - - // Let clientDataHash be the hash of the serialized client data represented by clientDataJSON. - let client_data_json_hash = compute_sha256(client_data_json.as_bytes()).to_vec(); - - trace!("client_data_json -> {:x?}", client_data_json); - trace!("client_data_json_hash -> {:x?}", client_data_json_hash); - // Not required. // If the options.signal is present and its aborted flag is set to true, return a DOMException whose name is "AbortError" and terminate this algorithm. @@ -232,7 +202,7 @@ impl AuthenticatorBackend for SoftPasskey { rand::rand_bytes(key_handle.as_mut_slice())?; // Create a new key. - let ecgroup = ec::EcGroup::from_curve_name(nid::Nid::X9_62_PRIME256V1)?; + let ecgroup = get_group()?; let eckey = ec::EcKey::generate(&ecgroup)?; @@ -384,7 +354,7 @@ impl AuthenticatorBackend for SoftPasskey { raw_id: Base64UrlSafeData(key_handle), response: AuthenticatorAttestationResponseRaw { attestation_object: Base64UrlSafeData(ao_bytes), - client_data_json: Base64UrlSafeData(client_data_json.as_bytes().to_vec()), + client_data_json: Base64UrlSafeData(vec![]), transports: None, }, type_: "public-key".to_string(), @@ -397,7 +367,7 @@ impl AuthenticatorBackend for SoftPasskey { fn perform_auth( &mut self, - origin: Url, + client_data_json_hash: Vec, options: PublicKeyCredentialRequestOptions, timeout_ms: u32, ) -> Result { @@ -406,26 +376,6 @@ impl AuthenticatorBackend for SoftPasskey { // If the extensions member of options is present, then for each extensionId → clientExtensionInput of options.extensions: // ... - // Let collectedClientData be a new CollectedClientData instance whose fields are: - let collected_client_data = CollectedClientData { - type_: "webauthn.get".to_string(), - challenge: options.challenge.clone(), - origin, - token_binding: None, - cross_origin: None, - unknown_keys: BTreeMap::new(), - }; - - // Let clientDataJSON be the JSON-serialized client data constructed from collectedClientData. - let client_data_json = - serde_json::to_string(&collected_client_data).map_err(|_| WebauthnCError::Json)?; - - // Let clientDataHash be the hash of the serialized client data represented by clientDataJSON. - let client_data_json_hash = compute_sha256(client_data_json.as_bytes()).to_vec(); - - trace!("client_data_json -> {:x?}", client_data_json); - trace!("client_data_json_hash -> {:x?}", client_data_json_hash); - // This is where we deviate from the spec, since we aren't a browser. let user_verification = options.user_verification == UserVerificationPolicy::Required; @@ -462,7 +412,7 @@ impl AuthenticatorBackend for SoftPasskey { raw_id: Base64UrlSafeData(u2sd.key_handle.clone()), response: AuthenticatorAssertionResponseRaw { authenticator_data: Base64UrlSafeData(authdata), - client_data_json: Base64UrlSafeData(client_data_json.as_bytes().to_vec()), + client_data_json: Base64UrlSafeData(vec![]), signature: Base64UrlSafeData(u2sd.signature), user_handle: None, }, diff --git a/webauthn-authenticator-rs/src/softtoken.rs b/webauthn-authenticator-rs/src/softtoken.rs index dfee1ec6..28a33caa 100644 --- a/webauthn-authenticator-rs/src/softtoken.rs +++ b/webauthn-authenticator-rs/src/softtoken.rs @@ -1,40 +1,89 @@ -use crate::error::WebauthnCError; -use crate::util::compute_sha256; -use crate::AuthenticatorBackend; -use crate::Url; +use crate::{ + authenticator_hashed::AuthenticatorBackendHashedClientData, + crypto::get_group, + ctap2::commands::{value_to_vec_u8, GetInfoResponse}, + error::WebauthnCError, + util::compute_sha256, +}; use openssl::x509::{ extension::{AuthorityKeyIdentifier, BasicConstraints, KeyUsage, SubjectKeyIdentifier}, X509NameBuilder, X509Ref, X509ReqBuilder, X509, }; -use openssl::{asn1, bn, ec, hash, nid, pkey, rand, sign}; +use openssl::{asn1, bn, ec, hash, pkey, rand, sign}; +use serde::{Deserialize, Serialize}; use serde_cbor::value::Value; -use std::collections::BTreeMap; use std::collections::HashMap; use std::iter; +use std::{collections::BTreeMap, fs::File, io::Read}; +use std::{ + collections::BTreeSet, + io::{Seek, SeekFrom, Write}, +}; use uuid::Uuid; use base64urlsafedata::Base64UrlSafeData; use webauthn_rs_proto::{ AllowCredentials, AuthenticationExtensionsClientOutputs, AuthenticatorAssertionResponseRaw, - AuthenticatorAttachment, AuthenticatorAttestationResponseRaw, CollectedClientData, - PublicKeyCredential, PublicKeyCredentialCreationOptions, PublicKeyCredentialRequestOptions, + AuthenticatorAttachment, AuthenticatorAttestationResponseRaw, PublicKeyCredential, + PublicKeyCredentialCreationOptions, PublicKeyCredentialRequestOptions, RegisterPublicKeyCredential, RegistrationExtensionsClientOutputs, UserVerificationPolicy, }; pub const AAGUID: Uuid = uuid::uuid!("0fb9bcbc-a0d4-4042-bbb0-559bc1631e28"); +#[derive(Serialize, Deserialize)] pub struct SoftToken { + #[serde(with = "PKeyPrivateDef")] _ca_key: pkey::PKey, + #[serde(with = "X509Def")] _ca_cert: X509, + #[serde(with = "PKeyPrivateDef")] intermediate_key: pkey::PKey, + #[serde(with = "X509Def")] intermediate_cert: X509, tokens: HashMap, Vec>, counter: u32, } -fn build_ca() -> Result<(pkey::PKey, X509), openssl::error::ErrorStack> { - let ecgroup = ec::EcGroup::from_curve_name(nid::Nid::X9_62_PRIME256V1)?; +#[derive(Serialize, Deserialize)] +#[serde(remote = "pkey::PKey")] +struct PKeyPrivateDef { + #[serde(getter = "private_key_to_der")] + der: Value, +} + +fn private_key_to_der(k: &pkey::PKeyRef) -> Value { + Value::Bytes(k.private_key_to_der().unwrap()) +} + +impl From for pkey::PKey { + fn from(def: PKeyPrivateDef) -> Self { + let b = value_to_vec_u8(def.der, "der").unwrap(); + Self::private_key_from_der(&b).unwrap() + } +} + +#[derive(Serialize, Deserialize)] +#[serde(remote = "X509")] +struct X509Def { + #[serde(getter = "x509_to_der")] + der: Value, +} + +fn x509_to_der(k: &X509Ref) -> Value { + Value::Bytes(k.to_der().unwrap()) +} + +impl From for X509 { + fn from(def: X509Def) -> Self { + let b = value_to_vec_u8(def.der, "der").unwrap(); + Self::from_der(&b).unwrap() + } +} + +fn build_ca() -> Result<(pkey::PKey, X509), WebauthnCError> { + let ecgroup = get_group()?; let eckey = ec::EcKey::generate(&ecgroup)?; let ca_key = pkey::PKey::from_ec_key(eckey)?; let mut x509_name = X509NameBuilder::new()?; @@ -84,8 +133,8 @@ fn build_ca() -> Result<(pkey::PKey, X509), openssl::error::Error fn build_intermediate( ca_key: &pkey::PKeyRef, ca_cert: &X509Ref, -) -> Result<(pkey::PKey, X509), openssl::error::ErrorStack> { - let ecgroup = ec::EcGroup::from_curve_name(nid::Nid::X9_62_PRIME256V1)?; +) -> Result<(pkey::PKey, X509), WebauthnCError> { + let ecgroup = get_group()?; let eckey = ec::EcKey::generate(&ecgroup)?; let int_key = pkey::PKey::from_ec_key(eckey)?; @@ -200,6 +249,36 @@ impl SoftToken { ca, )) } + + pub fn get_info(&self) -> GetInfoResponse { + GetInfoResponse { + versions: BTreeSet::from(["FIDO_2_0".to_string()]), + extensions: None, + aaguid: AAGUID.to_bytes_le().to_vec(), + options: None, + max_msg_size: None, + pin_protocols: None, + max_cred_count_in_list: None, + max_cred_id_len: None, + transports: Some(vec!["internal".to_string()]), + algorithms: None, + min_pin_length: None, + } + } + + pub fn to_cbor(&self) -> Result, WebauthnCError> { + serde_cbor::ser::to_vec(self).map_err(|e| { + error!("SoftToken.to_cbor: {:?}", e); + WebauthnCError::Cbor + }) + } + + pub fn from_cbor(v: &[u8]) -> Result { + serde_cbor::from_slice(v).map_err(|e| { + error!("SoftToken::from_cbor: {:?}", e); + WebauthnCError::Cbor + }) + } } #[derive(Debug)] @@ -210,10 +289,10 @@ pub struct U2FSignData { flags: u8, } -impl AuthenticatorBackend for SoftToken { +impl AuthenticatorBackendHashedClientData for SoftToken { fn perform_register( &mut self, - origin: Url, + client_data_json_hash: Vec, options: PublicKeyCredentialCreationOptions, _timeout_ms: u32, ) -> Result { @@ -254,36 +333,6 @@ impl AuthenticatorBackend for SoftToken { // Set authenticatorExtensions[extensionId] to the base64url encoding of authenticatorExtensionInput. */ - // Let collectedClientData be a new CollectedClientData instance whose fields are: - // type - // The string "webauthn.create". - // challenge - // The base64url encoding of options.challenge. - // origin - // The serialization of callerOrigin. - - // Not Supported Yet. - // tokenBinding - // The status of Token Binding between the client and the callerOrigin, as well as the Token Binding ID associated with callerOrigin, if one is available. - let collected_client_data = CollectedClientData { - type_: "webauthn.create".to_string(), - challenge: options.challenge.clone(), - origin, - token_binding: None, - cross_origin: None, - unknown_keys: BTreeMap::new(), - }; - - // Let clientDataJSON be the JSON-serialized client data constructed from collectedClientData. - let client_data_json = - serde_json::to_string(&collected_client_data).map_err(|_| WebauthnCError::Json)?; - - // Let clientDataHash be the hash of the serialized client data represented by clientDataJSON. - let client_data_json_hash = compute_sha256(client_data_json.as_bytes()).to_vec(); - - trace!("client_data_json -> {:x?}", client_data_json); - trace!("client_data_json_hash -> {:x?}", client_data_json_hash); - // Not required. // If the options.signal is present and its aborted flag is set to true, return a DOMException whose name is "AbortError" and terminate this algorithm. @@ -392,7 +441,7 @@ impl AuthenticatorBackend for SoftToken { rand::rand_bytes(key_handle.as_mut_slice())?; // Create a new key. - let ecgroup = ec::EcGroup::from_curve_name(nid::Nid::X9_62_PRIME256V1)?; + let ecgroup = get_group()?; let eckey = ec::EcKey::generate(&ecgroup)?; @@ -537,7 +586,7 @@ impl AuthenticatorBackend for SoftToken { raw_id: Base64UrlSafeData(key_handle), response: AuthenticatorAttestationResponseRaw { attestation_object: Base64UrlSafeData(ao_bytes), - client_data_json: Base64UrlSafeData(client_data_json.as_bytes().to_vec()), + client_data_json: Base64UrlSafeData(vec![]), transports: None, }, type_: "public-key".to_string(), @@ -550,7 +599,7 @@ impl AuthenticatorBackend for SoftToken { fn perform_auth( &mut self, - origin: Url, + client_data_json_hash: Vec, options: PublicKeyCredentialRequestOptions, timeout_ms: u32, ) -> Result { @@ -559,26 +608,6 @@ impl AuthenticatorBackend for SoftToken { // If the extensions member of options is present, then for each extensionId → clientExtensionInput of options.extensions: // ... - // Let collectedClientData be a new CollectedClientData instance whose fields are: - let collected_client_data = CollectedClientData { - type_: "webauthn.get".to_string(), - challenge: options.challenge.clone(), - origin, - token_binding: None, - cross_origin: None, - unknown_keys: BTreeMap::new(), - }; - - // Let clientDataJSON be the JSON-serialized client data constructed from collectedClientData. - let client_data_json = - serde_json::to_string(&collected_client_data).map_err(|_| WebauthnCError::Json)?; - - // Let clientDataHash be the hash of the serialized client data represented by clientDataJSON. - let client_data_json_hash = compute_sha256(client_data_json.as_bytes()).to_vec(); - - trace!("client_data_json -> {:x?}", client_data_json); - trace!("client_data_json_hash -> {:x?}", client_data_json_hash); - // This is where we deviate from the spec, since we aren't a browser. let user_verification = options.user_verification == UserVerificationPolicy::Required; @@ -615,7 +644,7 @@ impl AuthenticatorBackend for SoftToken { raw_id: Base64UrlSafeData(u2sd.key_handle.clone()), response: AuthenticatorAssertionResponseRaw { authenticator_data: Base64UrlSafeData(authdata), - client_data_json: Base64UrlSafeData(client_data_json.as_bytes().to_vec()), + client_data_json: Base64UrlSafeData(vec![]), signature: Base64UrlSafeData(u2sd.signature), user_handle: None, }, @@ -696,6 +725,7 @@ impl U2FToken for SoftToken { .copied() .collect(); + trace!("Signing: {:?}", verification_data.as_slice()); let signature = signer .update(verification_data.as_slice()) .and_then(|_| signer.sign_to_vec())?; @@ -709,11 +739,96 @@ impl U2FToken for SoftToken { } } +/// [SoftToken] which is read form, and automatically saved to a [File] when +/// dropped. +pub struct SoftTokenFile { + token: SoftToken, + file: File, +} + +impl SoftTokenFile { + /// Creates a new [SoftTokenFile] which will be saved when dropped. + pub fn new(token: SoftToken, file: File) -> Self { + Self { token, file } + } + + /// Reads a [SoftToken] from a [File]. + pub fn open(mut file: File) -> Result { + let mut buf = Vec::new(); + file.read_to_end(&mut buf)?; + + let token: SoftToken = serde_cbor::from_slice(&buf).map_err(|e| { + error!("Error reading SoftToken: {:?}", e); + WebauthnCError::Cbor + })?; + + Ok(Self { token, file }) + } + + /// Saves the [SoftToken] to a [File]. + fn save(&mut self) -> Result<(), WebauthnCError> { + trace!("Saving SoftToken to {:?}", self.file); + let d = self.token.to_cbor()?; + self.file.set_len(0)?; + self.file.seek(SeekFrom::Start(0))?; + self.file.write_all(&d)?; + self.file.flush()?; + Ok(()) + } +} + +/// Extracts the [File] handle from this [SoftTokenFile], dropping (and saving) +/// the [SoftTokenFile] in the process. +impl TryFrom for File { + type Error = WebauthnCError; + fn try_from(value: SoftTokenFile) -> Result { + Ok(value.file.try_clone()?) + } +} + +impl AsRef for SoftTokenFile { + fn as_ref(&self) -> &SoftToken { + &self.token + } +} + +/// Drops the [SoftTokenFile], automatically saving it to disk. +impl Drop for SoftTokenFile { + fn drop(&mut self) { + self.save().unwrap_or_else(|e| { + error!("Error saving SoftToken: {:?}", e); + }); + } +} + +impl AuthenticatorBackendHashedClientData for SoftTokenFile { + fn perform_register( + &mut self, + client_data_hash: Vec, + options: PublicKeyCredentialCreationOptions, + timeout_ms: u32, + ) -> Result { + self.token + .perform_register(client_data_hash, options, timeout_ms) + } + + fn perform_auth( + &mut self, + client_data_hash: Vec, + options: PublicKeyCredentialRequestOptions, + timeout_ms: u32, + ) -> Result { + self.token + .perform_auth(client_data_hash, options, timeout_ms) + } +} + #[cfg(test)] mod tests { - use super::{SoftToken, AAGUID}; + use super::*; use crate::prelude::{Url, WebauthnAuthenticator}; use std::collections::BTreeSet; + use tempfile::tempfile; use webauthn_rs_core::proto::{AttestationCa, AttestationCaList}; use webauthn_rs_core::WebauthnCore as Webauthn; use webauthn_rs_proto::{ @@ -798,4 +913,100 @@ mod tests { .expect("webauth authentication denied"); info!("auth_res -> {:x?}", auth_res); } + + #[test] + fn softtoken_persistence() { + let _ = tracing_subscriber::fmt::try_init(); + let wan = Webauthn::new_unsafe_experts_only( + "https://localhost:8080/auth", + "localhost", + vec![url::Url::parse("https://localhost:8080").unwrap()], + None, + None, + None, + ); + + let (soft_token, ca_root) = SoftToken::new().unwrap(); + let file = tempfile().unwrap(); + let soft_token = SoftTokenFile::new(soft_token, file); + assert_eq!(soft_token.token.tokens.len(), 0); + + let mut wa = WebauthnAuthenticator::new(soft_token); + + let unique_id = [ + 158, 170, 228, 89, 68, 28, 73, 194, 134, 19, 227, 153, 107, 220, 150, 238, + ]; + let name = "william"; + + let (chal, reg_state) = wan + .generate_challenge_register_options( + &unique_id, + name, + name, + AttestationConveyancePreference::Direct, + Some(UserVerificationPolicy::Preferred), + None, + None, + COSEAlgorithm::secure_algs(), + false, + None, + false, + ) + .unwrap(); + + info!("🍿 challenge -> {:x?}", chal); + + let r = wa + .do_registration(Url::parse("https://localhost:8080").unwrap(), chal) + .map_err(|e| { + error!("Error -> {:x?}", e); + e + }) + .expect("Failed to register"); + + let mut aaguids = BTreeSet::new(); + aaguids.insert(AAGUID); + let att_ca_list: AttestationCaList = AttestationCa { + ca: ca_root, + aaguids, + } + .try_into() + .expect("Failed to build attestation ca list"); + let cred = wan + .register_credential(&r, ®_state, Some(&att_ca_list)) + .unwrap(); + + info!("Credential -> {:?}", cred); + + assert_eq!(wa.backend.token.tokens.len(), 1); + + // Save the credential to disk + let mut file: File = wa.backend.try_into().unwrap(); + assert!(file.stream_position().unwrap() > 0); + + // Rewind and reload + file.seek(SeekFrom::Start(0)).unwrap(); + + let soft_token = SoftTokenFile::open(file).unwrap(); + assert_eq!(soft_token.token.tokens.len(), 1); + + let mut wa = WebauthnAuthenticator::new(soft_token); + + let (chal, auth_state) = wan + .generate_challenge_authenticate(vec![cred], None) + .unwrap(); + + let r = wa + .do_authentication(Url::parse("https://localhost:8080").unwrap(), chal) + .map_err(|e| { + error!("Error -> {:x?}", e); + e + }) + .expect("Failed to auth"); + + let auth_res = wan + .authenticate_credential(&r, &auth_state) + .expect("webauth authentication denied"); + info!("auth_res -> {:x?}", auth_res); + } } diff --git a/webauthn-authenticator-rs/src/transport/any.rs b/webauthn-authenticator-rs/src/transport/any.rs index 0f94954f..df3883ca 100644 --- a/webauthn-authenticator-rs/src/transport/any.rs +++ b/webauthn-authenticator-rs/src/transport/any.rs @@ -68,9 +68,8 @@ impl<'b> Transport<'b> for AnyTransport { #[allow(clippy::unimplemented)] impl Token for AnyToken { #[allow(unused_variables)] - async fn transmit_raw(&self, cmd: C, ui: &U) -> Result, WebauthnCError> + async fn transmit_raw(&mut self, cmd: &[u8], ui: &U) -> Result, WebauthnCError> where - C: CBORCommand, U: UiCallback, { match self { @@ -92,13 +91,13 @@ impl Token for AnyToken { } } - fn close(&self) -> Result<(), WebauthnCError> { + async fn close(&mut self) -> Result<(), WebauthnCError> { match self { AnyToken::Stub => unimplemented!(), #[cfg(feature = "nfc")] - AnyToken::Nfc(n) => n.close(), + AnyToken::Nfc(n) => n.close().await, #[cfg(feature = "usb")] - AnyToken::Usb(u) => u.close(), + AnyToken::Usb(u) => u.close().await, } } diff --git a/webauthn-authenticator-rs/src/transport/mod.rs b/webauthn-authenticator-rs/src/transport/mod.rs index 0c6b8072..b9821145 100644 --- a/webauthn-authenticator-rs/src/transport/mod.rs +++ b/webauthn-authenticator-rs/src/transport/mod.rs @@ -35,6 +35,17 @@ pub trait Transport<'b>: Sized + fmt::Debug + Send { .filter_map(|token| block_on(CtapAuthenticator::new(token, ui))) .collect()) } + + fn connect_one<'a, U: UiCallback>( + &mut self, + ui: &'a U, + ) -> Result, WebauthnCError> { + self.tokens()? + .drain(..) + .filter_map(|token| block_on(CtapAuthenticator::new(token, ui))) + .next() + .ok_or(WebauthnCError::NoSelectedToken) + } } /// Represents a connection to a single FIDO token over a [Transport]. @@ -51,13 +62,14 @@ pub trait Token: Sized + fmt::Debug + Sync + Send { fn get_transport(&self) -> AuthenticatorTransport; /// Transmit a CBOR message to a token, and deserialises the response. - async fn transmit<'a, C, R, U>(&self, cmd: C, ui: &U) -> Result + async fn transmit<'a, C, R, U>(&mut self, cmd: C, ui: &U) -> Result where C: CBORCommand, R: CBORResponse, U: UiCallback, { - let resp = self.transmit_raw(cmd, ui).await?; + let cbor = cmd.cbor().map_err(|_| WebauthnCError::Cbor)?; + let resp = self.transmit_raw(&cbor, ui).await?; R::try_from(resp.as_slice()).map_err(|_| { //error!("error: {:?}", e); @@ -67,12 +79,13 @@ pub trait Token: Sized + fmt::Debug + Sync + Send { /// Transmits a command on the underlying transport. /// + /// `cbor` is a CBOR-encoded command. + /// /// Interfaces need to check for and return any transport-layer-specific /// error code [WebauthnCError::Ctap], but don't need to worry about /// deserialising CBOR. - async fn transmit_raw(&self, cmd: C, ui: &U) -> Result, WebauthnCError> + async fn transmit_raw(&mut self, cbor: &[u8], ui: &U) -> Result, WebauthnCError> where - C: CBORCommand, U: UiCallback; /// Cancels a pending request. @@ -82,5 +95,5 @@ pub trait Token: Sized + fmt::Debug + Sync + Send { async fn init(&mut self) -> Result<(), WebauthnCError>; /// Closes the [Token] - fn close(&self) -> Result<(), WebauthnCError>; + async fn close(&mut self) -> Result<(), WebauthnCError>; } diff --git a/webauthn-authenticator-rs/src/types.rs b/webauthn-authenticator-rs/src/types.rs new file mode 100644 index 00000000..3ab91f6c --- /dev/null +++ b/webauthn-authenticator-rs/src/types.rs @@ -0,0 +1,49 @@ +//! Types used in a public API. +//! +//! These types need to be present regardless of which features were selected +//! at build time, because they are part of some other API which doesn't change. + +/// caBLE request type. +#[derive(Debug, PartialEq, Eq, Clone, Default, Copy)] +pub enum CableRequestType { + /// Logging in with an existing credential. + #[default] + GetAssertion, + + /// Creating a new, non-discoverable credential. + MakeCredential, + + /// Creating a new, discoverable credential. + DiscoverableMakeCredential, +} + +/// States that a caBLE connection can be in for +/// [crate::ui::UiCallback::cable_status_update]. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum CableState { + /// The initiator or authenticator is connecting to the tunnel server. + ConnectingToTunnelServer, + + /// The authenticator is waiting for the initiator to connect to the tunnel + /// server, and send a challenge. + WaitingForInitiatorConnection, + + /// The initiator or authenticator is establishing an encrypted channel. + Handshaking, + + /// The authenticator is waiting for the initiator to respond. + WaitingForInitiatorResponse, + + /// The authenticator is waiting for a command from the initiator. + WaitingForInitiatorCommand, + + /// The initiator or authenticator is processing what it received from the + /// other side. + Processing, + + /// The initiator has sent a message to the authenticator, and waiting for + /// a response. This may be that the device is waiting for some sort of + /// user verification action (like entering PIN or biometrics) to complete + /// the operation. + WaitingForAuthenticatorResponse, +} diff --git a/webauthn-authenticator-rs/src/ui/mod.rs b/webauthn-authenticator-rs/src/ui/mod.rs index f6ee30bc..53fbc78c 100644 --- a/webauthn-authenticator-rs/src/ui/mod.rs +++ b/webauthn-authenticator-rs/src/ui/mod.rs @@ -1,7 +1,12 @@ +#[cfg(feature = "qrcode")] +use qrcode::{render::unicode::Dense1x2, QrCode}; use std::fmt::Debug; use std::io::{stderr, Write}; -use crate::ctap2::EnrollSampleStatus; +use crate::{ + ctap2::EnrollSampleStatus, + types::{CableRequestType, CableState}, +}; pub trait UiCallback: Sync + Send + Debug { /// Prompts the user to enter their PIN. @@ -21,6 +26,19 @@ pub trait UiCallback: Sync + Send + Debug { remaining_samples: u32, feedback: Option, ); + + /// Prompt the user to scan a QR code with their mobile device to start the + /// caBLE linking process. + /// + /// This method will be called synchronously, and must not block. + fn cable_qr_code(&self, request_type: CableRequestType, url: String); + + /// Dismiss a displayed QR code from the screen. + /// + /// This method will be called synchronously, and must not block. + fn dismiss_qr_code(&self); + + fn cable_status_update(&self, state: CableState); } /// Basic CLI [UiCallback] implementation. @@ -52,4 +70,43 @@ impl UiCallback for Cli { writeln!(stderr, "Last impression was {:?}", feedback).ok(); } } + + fn cable_qr_code(&self, request_type: CableRequestType, url: String) { + match request_type { + CableRequestType::DiscoverableMakeCredential | CableRequestType::MakeCredential => { + println!("Scan the QR code with your mobile device to create a new credential with caBLE:"); + } + CableRequestType::GetAssertion => { + println!("Scan the QR code with your mobile device to sign in with caBLE:"); + } + } + println!("This feature requires Android with Google Play, or iOS 16 or later."); + + #[cfg(feature = "qrcode")] + { + let qr = QrCode::new(&url).expect("Could not create QR code"); + + let code = qr + .render::() + .dark_color(Dense1x2::Light) + .light_color(Dense1x2::Dark) + .build(); + + println!("{}", code); + } + + #[cfg(not(feature = "qrcode"))] + { + println!("QR code support not available in this build!") + } + println!("{}", url); + } + + fn dismiss_qr_code(&self) { + println!("caBLE authenticator detected, connecting..."); + } + + fn cable_status_update(&self, state: CableState) { + println!("caBLE status: {:?}", state); + } } diff --git a/webauthn-authenticator-rs/src/usb/mod.rs b/webauthn-authenticator-rs/src/usb/mod.rs index 96f78728..e2040c37 100644 --- a/webauthn-authenticator-rs/src/usb/mod.rs +++ b/webauthn-authenticator-rs/src/usb/mod.rs @@ -9,7 +9,6 @@ mod framing; mod responses; -use crate::ctap2::*; use crate::error::WebauthnCError; use crate::transport::*; use crate::ui::UiCallback; @@ -206,17 +205,15 @@ impl USBToken { #[async_trait] impl Token for USBToken { - async fn transmit_raw(&self, cmd: C, ui: &U) -> Result, WebauthnCError> + async fn transmit_raw(&mut self, cmd: &[u8], ui: &U) -> Result, WebauthnCError> where - C: CBORCommand, U: UiCallback, { - let cbor = cmd.cbor().map_err(|_| WebauthnCError::Cbor)?; let cmd = U2FHIDFrame { cid: self.cid, cmd: U2FHID_CBOR, - len: cbor.len() as u16, - data: cbor, + len: cmd.len() as u16, + data: cmd.to_vec(), }; self.send(&cmd)?; @@ -282,7 +279,7 @@ impl Token for USBToken { } } - fn close(&self) -> Result<(), WebauthnCError> { + async fn close(&mut self) -> Result<(), WebauthnCError> { Ok(()) } diff --git a/webauthn-authenticator-rs/src/usb/responses.rs b/webauthn-authenticator-rs/src/usb/responses.rs index 4cca9b8b..97f343d4 100644 --- a/webauthn-authenticator-rs/src/usb/responses.rs +++ b/webauthn-authenticator-rs/src/usb/responses.rs @@ -197,6 +197,7 @@ impl TryFrom<&U2FHIDFrame> for Response { mod tests { use super::*; use crate::ctap2::commands::GetInfoResponse; + use crate::ctap2::CBORResponse; #[test] fn init() { diff --git a/webauthn-authenticator-rs/src/util.rs b/webauthn-authenticator-rs/src/util.rs index bd11e894..2e5238e5 100644 --- a/webauthn-authenticator-rs/src/util.rs +++ b/webauthn-authenticator-rs/src/util.rs @@ -15,6 +15,17 @@ pub fn compute_sha256(data: &[u8]) -> [u8; 32] { } pub fn creation_to_clientdata(origin: Url, challenge: Base64UrlSafeData) -> CollectedClientData { + // Let collectedClientData be a new CollectedClientData instance whose fields are: + // type + // The string "webauthn.create". + // challenge + // The base64url encoding of options.challenge. + // origin + // The serialization of callerOrigin. + + // Not Supported Yet. + // tokenBinding + // The status of Token Binding between the client and the callerOrigin, as well as the Token Binding ID associated with callerOrigin, if one is available. CollectedClientData { type_: "webauthn.create".to_string(), challenge, diff --git a/webauthn-rs-core/src/crypto.rs b/webauthn-rs-core/src/crypto.rs index 6c470931..111f6450 100644 --- a/webauthn-rs-core/src/crypto.rs +++ b/webauthn-rs-core/src/crypto.rs @@ -45,9 +45,9 @@ fn pkey_verify_signature( Ok(verifier) } COSEAlgorithm::INSECURE_RS1 => { - error!("INSECURE SHA1 USAGE DETECTED"); - Err(WebauthnError::CredentialInsecureCryptography) - } + error!("INSECURE SHA1 USAGE DETECTED"); + Err(WebauthnError::CredentialInsecureCryptography) + } c_alg => { debug!(?c_alg, "WebauthnError::COSEKeyInvalidType"); Err(WebauthnError::COSEKeyInvalidType) @@ -767,7 +767,8 @@ impl COSEKey { } } - pub(crate) fn verify_signature( + /// Verifies data was signed with this [COSEKey]. + pub fn verify_signature( &self, signature: &[u8], verification_data: &[u8],