diff --git a/Cargo.toml b/Cargo.toml index 392d84e..d4e00cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,10 @@ untrusted = "0.6" rust-crypto = "0.2" csv-core = "0.1.6" derive_builder = "0.9.0" +crossbeam = { version = "0.7.3", optional = true } [dev-dependencies] crossbeam = "0.7.3" hex = "0.4.0" + +[features] +assert_devices = ["crossbeam"] diff --git a/examples/multiple.rs b/examples/multiple.rs index f428f0b..5ce5159 100644 --- a/examples/multiple.rs +++ b/examples/multiple.rs @@ -1,8 +1,12 @@ extern crate ctap_hmac as ctap; +use crossbeam::thread; use crypto::digest::Digest; use crypto::sha2::Sha256; -use ctap::{FidoCredential, FidoCredentialRequestBuilder, FidoAssertionRequestBuilder, AuthenticatorOptions, FidoDevice, FidoError, FidoResult}; +use ctap::{ + FidoAssertionRequestBuilder, FidoCredential, FidoCredentialRequestBuilder, FidoDevice, + FidoError, FidoResult, +}; use failure::_core::time::Duration; use hex; use std::env::args; @@ -11,42 +15,41 @@ use std::io::stdin; use std::io::stdout; use std::sync::mpsc::channel; use std::sync::Mutex; -use crossbeam::thread; const RP_ID: &str = "ctap_demo"; fn run() -> ctap::FidoResult<()> { - let mut credentials = args().skip(1).map(|id| FidoCredential { - id: hex::decode(&id).expect("Invalid credential"), - public_key: None, - }).collect::>(); + let mut credentials = args() + .skip(1) + .map(|id| FidoCredential { + id: hex::decode(&id).expect("Invalid credential"), + public_key: None, + }) + .collect::>(); if credentials.len() == 0 { - credentials = ctap::get_devices()?.map(|h| FidoDevice::new(&h).and_then(|mut dev| FidoCredentialRequestBuilder::default() - .rp_id(RP_ID).build().unwrap().make_credential(&mut dev))).collect::>>()?; - } - let credentials = credentials.iter().collect::>(); - let (s, r) = channel(); - thread::scope(|scope| { - let handles = ctap::get_devices()?.map(|h| { - let req = FidoAssertionRequestBuilder::default().rp_id(RP_ID).credentials(&credentials[..]).build().unwrap(); - let s = s.clone(); - scope.spawn(move |_| { + credentials = ctap::get_devices()? + .map(|h| { FidoDevice::new(&h).and_then(|mut dev| { - req.get_assertion(&mut dev).map(|res| { - s.send(res.clone()); - res - }) + FidoCredentialRequestBuilder::default() + .rp_id(RP_ID) + .build() + .unwrap() + .make_credential(&mut dev) }) }) - }).collect::>(); - for h in handles { - h.join(); - } - Ok::<(), FidoError>(()) - }).unwrap(); - for res in r.iter().take(credentials.len()) { - dbg!(res); + .collect::>>()?; } + let credentials = credentials.iter().collect::>(); + let req = FidoAssertionRequestBuilder::default() + .rp_id(RP_ID) + .credentials(&credentials[..]) + .build() + .unwrap(); + let mut devices = ctap::get_devices()? + .map(|handle| FidoDevice::new(&handle)) + .collect::>>()?; + let (cred, _) = ctap::get_assertion_devices(&req, devices.iter_mut())?; + println!("Success, got assertion for: {}", hex::encode(&cred.id)); Ok(()) } diff --git a/src/error.rs b/src/error.rs index 3cd5ef6..05d944f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -45,9 +45,9 @@ pub enum FidoErrorKind { EncryptPin, #[fail(display = "Failed to decrypt PIN.")] DecryptPin, - #[fail(display = "Supplied key has incorrect type.")] - VerifySignature, #[fail(display = "Failed to verify response signature.")] + VerifySignature, + #[fail(display = "Supplied key has incorrect type.")] KeyType, #[fail(display = "Device returned error: {}", _0)] CborError(CborErrorCode), diff --git a/src/lib.rs b/src/lib.rs index 121a6ce..8e041fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,6 +61,7 @@ pub mod extensions; mod hid_common; mod hid_linux; mod packet; +mod util; use std::cmp; use std::fs; @@ -68,13 +69,11 @@ use std::io::{Cursor, Write}; use std::u16; use std::u8; -use self::cbor::{ - PublicKeyCredentialDescriptor, -}; +use self::cbor::{AuthenticatorOptions, PublicKeyCredentialDescriptor}; pub use self::error::*; -pub use self::cbor::AuthenticatorOptions; use self::hid_linux as hid; use self::packet::CtapCommand; +pub use self::util::*; use crate::cbor::{AuthenticatorData, GetAssertionRequest}; use failure::{Fail, ResultExt}; use num_traits::FromPrimitive; @@ -99,7 +98,6 @@ pub struct FidoCredential { /// The public key provided by the authenticator, in uncompressed form. pub public_key: Option>, } - /// An opened FIDO authenticator. pub struct FidoDevice { device: fs::File, @@ -111,6 +109,46 @@ pub struct FidoDevice { aaguid: [u8; 16], } +pub struct FidoCancelHandle { + device: fs::File, + packet_size: u16, + channel_id: [u8; 4], +} + +impl FidoCancelHandle { + pub fn cancel(&mut self) -> FidoResult<()> { + let payload = &[1u8]; + let to_send = payload.len() as u16; + let max_payload = (self.packet_size - 7) as usize; + let (frame, payload) = payload.split_at(cmp::min(payload.len(), max_payload)); + packet::write_init_packet( + &mut self.device, + 64, + &self.channel_id, + &CtapCommand::Cancel, + to_send, + frame, + )?; + if payload.is_empty() { + return Ok(()); + } + let max_payload = (self.packet_size - 5) as usize; + for (seq, frame) in (0..u8::MAX).zip(payload.chunks(max_payload)) { + packet::write_cont_packet(&mut self.device, 64, &self.channel_id, seq, frame)?; + } + self.device.flush().context(FidoErrorKind::WritePacket)?; + Ok(()) + } + + pub fn cancel_after(&mut self, body: impl Fn(()) -> T) -> FidoResult { + let res = body(()); + match self.cancel() { + Ok(_) => Ok(res), + Err(e) => Err(e), + } + } +} + /// Request a new credential from the authenticator. The `rp_id` should be /// a stable string used to identify the party for whom the credential is /// created, for convenience it will be returned with the credential. @@ -198,13 +236,8 @@ pub struct FidoAssertionRequest<'a> { } impl<'a> FidoAssertionRequest<'a> { - pub fn get_assertion( - &self, - device: &mut FidoDevice, - ) -> FidoResult<&'a FidoCredential> { - device - .get_assertion(self) - .map(|res| res.0) + pub fn get_assertion(&self, device: &mut FidoDevice) -> FidoResult<&'a FidoCredential> { + device.get_assertion(self).map(|res| res.0) } } @@ -324,10 +357,21 @@ impl FidoDevice { } } + pub fn cancel_handle(&mut self) -> FidoResult { + Ok(self + .device + .try_clone() + .map(|device| FidoCancelHandle { + device, + packet_size: self.packet_size, + channel_id: self.channel_id, + }) + .context(FidoErrorKind::Io)?) + } pub fn make_credential( &mut self, - request: &FidoCredentialRequest<'_> + request: &FidoCredentialRequest<'_>, ) -> FidoResult { let rp = cbor::PublicKeyCredentialRpEntity { id: request.rp_id, @@ -343,7 +387,8 @@ impl FidoDevice { let options = Some(AuthenticatorOptions { up: false, - uv: request.uv, rk: request.rk + uv: request.uv, + rk: request.rk, }); if self.needs_pin && self.pin_token.is_none() { Err(FidoErrorKind::PinRequired)? @@ -364,13 +409,17 @@ impl FidoDevice { rp, user, pub_key_cred_params: &pub_key_cred_params, - exclude_list: &request.exclude_list.iter() + exclude_list: &request + .exclude_list + .iter() .map(|cred| PublicKeyCredentialDescriptor { cred_type: "public-key".into(), id: cred.id.clone(), }) .collect::>()[..], - extensions: &request.extension_data.iter() + extensions: &request + .extension_data + .iter() .map(|(name, data)| (*name, *data)) .collect::>()[..], options, @@ -388,7 +437,7 @@ impl FidoDevice { .attested_credential_data .credential_public_key, )? - .bytes(); + .bytes(); Ok(FidoCredential { id: response.auth_data.attested_credential_data.credential_id, public_key: Some(Vec::from(&public_key[..])), @@ -409,7 +458,6 @@ impl FidoDevice { /// This method will fail if a PIN is required but the device is not /// unlocked or if the device returns malformed data. - pub fn get_assertion<'a>( &mut self, assertion: &FidoAssertionRequest<'a>, @@ -446,7 +494,7 @@ impl FidoDevice { options: Some(AuthenticatorOptions { rk: assertion.rk, uv: assertion.uv, - up: assertion.up + up: assertion.up, }), pin_auth: pin_auth, pin_protocol: pin_auth.and(Some(0x01)), @@ -467,19 +515,28 @@ impl FidoDevice { }) .next(); - credential.and_then(|cred| { - cred.public_key.as_ref().map(|public_key| - Some(crypto::verify_signature( + credential + .and_then(|cred| { + if cred + .public_key + .as_ref() + .map(|public_key| { + crypto::verify_signature( &public_key, &assertion.client_data_hash, &response.auth_data_bytes, &response.signature, ) - ).unwrap_or(true)).iter().filter_map( |valid| match valid { - true => Some(cred), - false => None, - }).next() - }).ok_or(FidoError::from(FidoErrorKind::VerifySignature)).map(|cred| (cred, response.auth_data)) + }) + .unwrap_or(true) + { + Some(cred) + } else { + None + } + }) + .ok_or(FidoError::from(FidoErrorKind::VerifySignature)) + .map(|cred| (cred, response.auth_data)) } fn cbor(&mut self, request: cbor::Request) -> FidoResult { diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..e13526a --- /dev/null +++ b/src/util.rs @@ -0,0 +1,44 @@ +use crate::cbor::AuthenticatorData; +use crate::{FidoAssertionRequest, FidoCredential, FidoDevice, FidoErrorKind, FidoResult}; +use std::sync::mpsc::channel; +#[cfg(feature = "assert_devices")] +use crossbeam::thread; + +/// Will send the `assertion` to all supplied `devices` and return either the first successful assertion or the last error +#[cfg(feature = "assert_devices")] +pub fn get_assertion_devices<'a>( + assertion: &'a FidoAssertionRequest, + devices: impl Iterator, +) -> FidoResult<(&'a FidoCredential, AuthenticatorData)> { + thread::scope( + |scope| -> FidoResult<(&'a FidoCredential, AuthenticatorData)> { + let (tx, rx) = channel(); + let handles = devices + .map(|device| { + let cancel = device.cancel_handle()?; + let tx = tx.clone(); + let thread_handle = + scope.spawn(move |_| tx.send(device.get_assertion(assertion))); + Ok((cancel, thread_handle)) + }) + .collect::>>()?; + + let mut err = None; + for res in rx.iter().take(handles.len()) { + match res { + Ok(_) => { + for (mut cancel, join) in handles { + // Canceling out of courtesy don't care if it fails + let _ = cancel.cancel(); + let _ = join.join(); + } + return res; + } + e => err = Some(e), + } + } + err.unwrap_or(Err(FidoErrorKind::DeviceUnsupported.into())) + }, + ) + .unwrap() +}