From cc48719cfa1375909877265905264fad875d1fa3 Mon Sep 17 00:00:00 2001 From: shimun Date: Fri, 3 Apr 2020 23:05:38 +0200 Subject: [PATCH] support sending requests to multiple devices --- Cargo.toml | 8 ++- examples/hmac.rs | 27 ++++++--- examples/multiple.rs | 73 +++++++++++------------- src/error.rs | 2 +- src/extensions/hmac.rs | 72 +++++++++++------------- src/lib.rs | 124 ++++++++++++++++++++++++++++++----------- src/util.rs | 67 ++++++++++++++++++++++ 7 files changed, 249 insertions(+), 124 deletions(-) create mode 100644 src/util.rs diff --git a/Cargo.toml b/Cargo.toml index 392d84e..e56c1aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "ctap_hmac" description = "A Rust implementation of the FIDO2 CTAP protocol, including the HMAC extension" -version = "0.3.0" +version = "0.4.0" license = "Apache-2.0/MIT" -homepage = "https://github.com/ArdaXi/ctap/pull/2" +homepage = "https://github.com/shimunn/ctap" repository = "https://github.com/shimunn/ctap" authors = ["Arda Xi ", "shimun "] edition = "2018" @@ -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] +request_multiple = ["crossbeam"] \ No newline at end of file diff --git a/examples/hmac.rs b/examples/hmac.rs index b2dc792..ac8834e 100644 --- a/examples/hmac.rs +++ b/examples/hmac.rs @@ -3,7 +3,7 @@ extern crate ctap_hmac as ctap; use crypto::digest::Digest; use crypto::sha2::Sha256; use ctap::extensions::hmac::HmacExtension; -use ctap::{FidoCredential, FidoCredentialRequestBuilder, AuthenticatorOptions}; +use ctap::{FidoAssertionRequestBuilder, FidoCredential, FidoCredentialRequestBuilder}; use hex; use std::env::args; use std::io::prelude::*; @@ -17,23 +17,30 @@ fn main() -> ctap::FidoResult<()> { let device_info = &mut devices.next().expect("No authenticator found"); let mut device = ctap::FidoDevice::new(device_info)?; - let mut credential = match args().skip(1).next().map(|h| FidoCredential { + let credential = match args().skip(1).next().map(|h| FidoCredential { id: hex::decode(&h).expect("Invalid credential"), public_key: None, }) { Some(cred) => cred, _ => { - let req = FidoCredentialRequestBuilder::default().rp_id(RP_ID).rp_name("ctap_hmac crate").user_name("example").uv(false).build().unwrap(); + let req = FidoCredentialRequestBuilder::default() + .rp_id(RP_ID) + .rp_name("ctap_hmac crate") + .user_name("example") + .uv(false) + .build() + .unwrap(); println!("Authorize using your device"); - let cred = device.make_hmac_credential(req).expect("Failed to request credential"); + let cred = device + .make_hmac_credential(&req) + .expect("Failed to request credential"); println!("Credential: {}\nNote: You can pass this credential as first argument in order to reproduce results", hex::encode(&cred.id)); cred } }; - let credential = credential; print!("Type in your message: "); - stdout().flush(); + stdout().flush().unwrap(); let mut message = String::new(); stdin() .read_line(&mut message) @@ -44,7 +51,13 @@ fn main() -> ctap::FidoResult<()> { let mut digest = Sha256::new(); digest.input(&message.as_bytes()); digest.result(&mut salt); - let (cred, (hash1, _hash2)) = device.get_hmac_assertion(RP_ID, &[&credential], &salt, None, None)?; + let credential = &&credential; + let request = FidoAssertionRequestBuilder::default() + .rp_id(RP_ID) + .credential(credential) + .build() + .unwrap(); + let (_cred, (hash1, _hash2)) = device.get_hmac_assertion(&request, &salt, None)?; println!("Hash: {}", hex::encode(&hash1)); Ok(()) } diff --git a/examples/multiple.rs b/examples/multiple.rs index f428f0b..cf090d9 100644 --- a/examples/multiple.rs +++ b/examples/multiple.rs @@ -1,55 +1,46 @@ extern crate ctap_hmac as ctap; +use ctap::{ + FidoAssertionRequestBuilder, FidoCredential, FidoCredentialRequestBuilder, FidoDevice, + FidoResult, +}; -use crypto::digest::Digest; -use crypto::sha2::Sha256; -use ctap::{FidoCredential, FidoCredentialRequestBuilder, FidoAssertionRequestBuilder, AuthenticatorOptions, FidoDevice, FidoError, FidoResult}; -use failure::_core::time::Duration; use hex; use std::env::args; -use std::io::prelude::*; -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::>(); +fn main() -> ctap::FidoResult<()> { + 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::>>()?; + // run with --features request_multiple + let (cred, _) = ctap::get_assertion_devices(&req, devices.iter_mut())?; + println!("Success, got assertion for: {}", hex::encode(&cred.id)); Ok(()) } - -fn main() { - dbg!(run()); -} diff --git a/src/error.rs b/src/error.rs index 3cd5ef6..def7432 100644 --- a/src/error.rs +++ b/src/error.rs @@ -45,7 +45,7 @@ pub enum FidoErrorKind { EncryptPin, #[fail(display = "Failed to decrypt PIN.")] DecryptPin, - #[fail(display = "Supplied key has incorrect type.")] + #[fail(display = "Failed to verify response signature.")] VerifySignature, #[fail(display = "Failed to verify response signature.")] KeyType, diff --git a/src/extensions/hmac.rs b/src/extensions/hmac.rs index d3026a6..cd8d900 100644 --- a/src/extensions/hmac.rs +++ b/src/extensions/hmac.rs @@ -1,7 +1,4 @@ -use crate::{ - AuthenticatorOptions, FidoAssertionRequestBuilder, - FidoCredentialRequest, -}; +use crate::{FidoAssertionRequest, FidoAssertionRequestBuilder, FidoCredentialRequest}; use crate::{FidoCredential, FidoDevice, FidoErrorKind, FidoResult}; use cbor_codec::value::{Bytes, Int, Key, Text, Value}; use cbor_codec::Encoder; @@ -13,7 +10,6 @@ use rust_crypto::mac::Mac; use rust_crypto::sha2::Sha256; use std::collections::BTreeMap; use std::io::Cursor; -use std::iter::FromIterator; pub trait HmacExtension { fn extension_name() -> &'static str { @@ -40,8 +36,10 @@ pub trait HmacExtension { /// Convenience function to create an credential which includes extension specific data /// Use `FidoDevice::make_credential` if you need more control - fn make_hmac_credential(&mut self, request: FidoCredentialRequest) -> FidoResult; - + fn make_hmac_credential( + &mut self, + request: &FidoCredentialRequest, + ) -> FidoResult; /// Request an assertion from the authenticator for a given credential and salt(s). /// at least one `salt` must be provided, consider using a hashing function like SHA256 @@ -53,13 +51,11 @@ pub trait HmacExtension { /// provided, and will fail if a PIN is required but not provided or if the /// device returns malformed data. /// - fn get_hmac_assertion<'a>( + fn get_hmac_assertion<'a: 'b, 'b>( &mut self, - rp_id: &str, - credentials: &'a [&'a FidoCredential], + assertion: &FidoAssertionRequest<'a, 'b>, salt: &[u8; 32], salt2: Option<&[u8; 32]>, - options: Option, ) -> FidoResult<(&'a FidoCredential, ([u8; 32], Option<[u8; 32]>))>; /// Convenience function for `get_hmac_assertion` that will accept arbitrary @@ -75,11 +71,13 @@ pub trait HmacExtension { digest.input(input); digest.result(&mut salt); self.get_hmac_assertion( - rp_id, - &[credential], + &FidoAssertionRequestBuilder::default() + .rp_id(rp_id) + .credential(&credential) + .build() + .unwrap(), &salt, None, - Some(AuthenticatorOptions { uv: true, rk: true, up: false }), ) .map(|(_cred, secret)| secret.0) } @@ -138,43 +136,35 @@ impl HmacExtension for FidoDevice { Ok(Value::Map(map)) } - fn make_hmac_credential(&mut self, request: FidoCredentialRequest) -> FidoResult { - let mut request = request; + fn make_hmac_credential( + &mut self, + request: &FidoCredentialRequest, + ) -> FidoResult { + let mut request = request.clone(); request.rk = true; - request.extension_data.insert(::extension_name(), ::extension_input()); + request.extension_data.insert( + ::extension_name(), + ::extension_input(), + ); self.make_credential(&request) } - fn get_hmac_assertion<'a>( + fn get_hmac_assertion<'a: 'b, 'b>( &mut self, - rp_id: &str, - credentials: &'a [&'a FidoCredential], + request: &FidoAssertionRequest<'a, 'b>, salt: &[u8; 32], salt2: Option<&[u8; 32]>, - options: Option, ) -> FidoResult<(&'a FidoCredential, ([u8; 32], Option<[u8; 32]>))> { while self.shared_secret.is_none() { self.init_shared_secret()?; } let ext_data: Value = self.get_data(salt, salt2)?; + let mut request = request.clone(); + request + .extension_data + .insert(::extension_name(), &ext_data); - let ext_data: BTreeMap<&str, &Value> = BTreeMap::from_iter( - [(::extension_name(), &ext_data)] - .iter() - .cloned(), - ); - - let mut builder = FidoAssertionRequestBuilder::default() - .credentials(credentials) - .rp_id(rp_id) - .extension_data(ext_data); - - if let Some(opts) = options { - builder = builder.uv(opts.uv).up(opts.up); - } - - let (cred, auth_data) = - self.get_assertion(&builder.build().unwrap())?; + let (cred, auth_data) = self.get_assertion(&request)?; let shared_secret = self.shared_secret.as_ref().unwrap(); let mut decryptor = shared_secret.decryptor(); let mut hmac_secret_combined = [0u8; 64]; @@ -206,7 +196,11 @@ impl HmacExtension for FidoDevice { let mut hmac_secret_1 = [0u8; 32]; hmac_secret_0.copy_from_slice(&hmac_secret[0..32]); hmac_secret_1.copy_from_slice(&hmac_secret[32..]); - let cred = credentials.into_iter().find(|c| c.id == cred.id).unwrap(); + let cred = request + .credentials + .into_iter() + .find(|c| c.id == cred.id) + .unwrap(); Ok((cred, (hmac_secret_0, salt2.and(Some(hmac_secret_1))))) } } diff --git a/src/lib.rs b/src/lib.rs index 121a6ce..d30b36f 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. @@ -179,7 +217,7 @@ impl<'a> FidoCredentialRequest<'a> { #[derive(Clone, Debug, Builder)] #[builder(setter(into))] #[builder(pattern = "owned")] -pub struct FidoAssertionRequest<'a> { +pub struct FidoAssertionRequest<'a, 'b> { #[builder(default)] up: bool, #[builder(default)] @@ -194,21 +232,16 @@ pub struct FidoAssertionRequest<'a> { #[builder(default = "&[0u8; 32]")] client_data_hash: &'a [u8], #[builder(default)] - extension_data: BTreeMap<&'a str, &'a cbor_codec::value::Value>, + extension_data: BTreeMap<&'b str, &'b cbor_codec::value::Value>, } -impl<'a> FidoAssertionRequest<'a> { - pub fn get_assertion( - &self, - device: &mut FidoDevice, - ) -> FidoResult<&'a FidoCredential> { - device - .get_assertion(self) - .map(|res| res.0) +impl<'a, 'b> FidoAssertionRequest<'a, 'b> { + pub fn get_assertion(&self, device: &mut FidoDevice) -> FidoResult<&'a FidoCredential> { + device.get_assertion(self).map(|res| res.0) } } -impl<'a> FidoAssertionRequestBuilder<'a> { +impl<'a, 'b> FidoAssertionRequestBuilder<'a, 'b> { pub fn credential(mut self, credential: &'a &'a FidoCredential) -> Self { self.credentials = Some(std::slice::from_ref(credential)); self @@ -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[..])), @@ -408,11 +457,9 @@ 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>( + pub fn get_assertion<'a, 'b>( &mut self, - assertion: &FidoAssertionRequest<'a>, + assertion: &FidoAssertionRequest<'a, 'b>, ) -> FidoResult<(&'a FidoCredential, AuthenticatorData)> { while self.shared_secret.is_none() { self.init_shared_secret()?; @@ -446,7 +493,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 +514,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..d739866 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,67 @@ +#[cfg(feature = "request_multiple")] +use crate::{ + cbor::AuthenticatorData, FidoAssertionRequest, FidoCredential, FidoCredentialRequest, + FidoDevice, FidoErrorKind, FidoResult, +}; +#[cfg(feature = "request_multiple")] +use crossbeam::thread; +#[cfg(feature = "request_multiple")] +use std::sync::mpsc::channel; + +#[cfg(feature = "request_multiple")] +pub fn request_multiple_devices< + 'a, + T: Send + 'a, + F: Fn(&mut FidoDevice) -> FidoResult + 'a + Sync, +>( + devices: impl Iterator, +) -> FidoResult { + thread::scope(|scope| -> FidoResult { + let (tx, rx) = channel(); + let handles = devices + .map(|(device, fn_)| { + let cancel = device.cancel_handle()?; + let tx = tx.clone(); + let thread_handle = scope.spawn(move |_| tx.send(fn_(device))); + 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() +} + +/// Will send the `assertion_request` to all supplied `devices` and return either the first successful assertion or the last error +#[cfg(feature = "request_multiple")] +pub fn get_assertion_devices<'a>( + assertion_request: &'a FidoAssertionRequest, + devices: impl Iterator, +) -> FidoResult<(&'a FidoCredential, AuthenticatorData)> { + let get_assertion = |device: &mut FidoDevice| device.get_assertion(assertion_request); + request_multiple_devices(devices.map(|device| (device, &get_assertion))) +} + +/// Will send the `credential_request` to all supplied `devices` and return either the first credential or the last error +#[cfg(feature = "request_multiple")] +pub fn make_credential_devices<'a>( + credential_request: &'a FidoCredentialRequest, + devices: impl Iterator, +) -> FidoResult { + let make_credential = |device: &mut FidoDevice| device.make_credential(credential_request); + request_multiple_devices(devices.map(|device| (device, &make_credential))) +}