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 ec38060..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,11 +69,11 @@ use std::io::{Cursor, Write}; use std::u16; use std::u8; -pub use self::cbor::AuthenticatorOptions; -use self::cbor::PublicKeyCredentialDescriptor; +use self::cbor::{AuthenticatorOptions, PublicKeyCredentialDescriptor}; pub use self::error::*; 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; @@ -97,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, @@ -517,23 +517,23 @@ impl FidoDevice { credential .and_then(|cred| { - cred.public_key + if cred + .public_key .as_ref() .map(|public_key| { - Some(crypto::verify_signature( + 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() + .unwrap_or(true) + { + Some(cred) + } else { + None + } }) .ok_or(FidoError::from(FidoErrorKind::VerifySignature)) .map(|cred| (cred, response.auth_data)) 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() +}