diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /third_party/rust/authenticator/src | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/authenticator/src')
95 files changed, 20826 insertions, 0 deletions
diff --git a/third_party/rust/authenticator/src/authenticatorservice.rs b/third_party/rust/authenticator/src/authenticatorservice.rs new file mode 100644 index 0000000000..93195cd833 --- /dev/null +++ b/third_party/rust/authenticator/src/authenticatorservice.rs @@ -0,0 +1,1023 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::consts::PARAMETER_SIZE; +use crate::ctap2::commands::client_pin::Pin; +pub use crate::ctap2::commands::get_assertion::{ + GetAssertionExtensions, GetAssertionOptions, HmacSecretExtension, +}; +pub use crate::ctap2::commands::make_credentials::{ + MakeCredentialsExtensions, MakeCredentialsOptions, +}; +use crate::ctap2::server::{ + PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, User, +}; +use crate::errors::*; +use crate::manager::Manager; +use crate::statecallback::StateCallback; +use std::sync::{mpsc::Sender, Arc, Mutex}; + +// TODO(MS): Once U2FManager gets completely removed, this can be removed as well +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CtapVersion { + CTAP1, + CTAP2, +} + +#[derive(Debug, Clone)] +pub struct RegisterArgsCtap1 { + pub flags: crate::RegisterFlags, + pub challenge: Vec<u8>, + pub application: crate::AppId, + pub key_handles: Vec<crate::KeyHandle>, +} + +#[derive(Debug, Clone)] +pub struct RegisterArgsCtap2 { + pub challenge: Vec<u8>, + pub relying_party: RelyingParty, + pub origin: String, + pub user: User, + pub pub_cred_params: Vec<PublicKeyCredentialParameters>, + pub exclude_list: Vec<PublicKeyCredentialDescriptor>, + pub options: MakeCredentialsOptions, + pub extensions: MakeCredentialsExtensions, + pub pin: Option<Pin>, +} + +#[derive(Debug)] +pub enum RegisterArgs { + CTAP1(RegisterArgsCtap1), + CTAP2(RegisterArgsCtap2), +} + +impl From<RegisterArgsCtap1> for RegisterArgs { + fn from(args: RegisterArgsCtap1) -> Self { + RegisterArgs::CTAP1(args) + } +} + +impl From<RegisterArgsCtap2> for RegisterArgs { + fn from(args: RegisterArgsCtap2) -> Self { + RegisterArgs::CTAP2(args) + } +} + +#[derive(Debug, Clone)] +pub struct SignArgsCtap1 { + pub flags: crate::SignFlags, + pub challenge: Vec<u8>, + pub app_ids: Vec<crate::AppId>, + pub key_handles: Vec<crate::KeyHandle>, +} + +#[derive(Debug, Clone)] +pub struct SignArgsCtap2 { + pub challenge: Vec<u8>, + pub origin: String, + pub relying_party_id: String, + pub allow_list: Vec<PublicKeyCredentialDescriptor>, + pub options: GetAssertionOptions, + pub extensions: GetAssertionExtensions, + pub pin: Option<Pin>, + // Todo: Extensions +} + +#[derive(Debug)] +pub enum SignArgs { + CTAP1(SignArgsCtap1), + CTAP2(SignArgsCtap2), +} + +impl From<SignArgsCtap1> for SignArgs { + fn from(args: SignArgsCtap1) -> Self { + SignArgs::CTAP1(args) + } +} + +impl From<SignArgsCtap2> for SignArgs { + fn from(args: SignArgsCtap2) -> Self { + SignArgs::CTAP2(args) + } +} + +#[derive(Debug, Clone, Default)] +pub struct AssertionExtensions { + pub hmac_secret: Option<HmacSecretExtension>, +} + +pub trait AuthenticatorTransport { + /// The implementation of this method must return quickly and should + /// report its status via the status and callback methods + fn register( + &mut self, + timeout: u64, + ctap_args: RegisterArgs, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::RegisterResult>>, + ) -> crate::Result<()>; + + /// The implementation of this method must return quickly and should + /// report its status via the status and callback methods + fn sign( + &mut self, + timeout: u64, + ctap_args: SignArgs, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::SignResult>>, + ) -> crate::Result<()>; + + fn cancel(&mut self) -> crate::Result<()>; + fn reset( + &mut self, + timeout: u64, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::ResetResult>>, + ) -> crate::Result<()>; + fn set_pin( + &mut self, + timeout: u64, + new_pin: Pin, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::ResetResult>>, + ) -> crate::Result<()>; +} + +pub struct AuthenticatorService { + transports: Vec<Arc<Mutex<Box<dyn AuthenticatorTransport + Send>>>>, + ctap_version: CtapVersion, +} + +fn clone_and_configure_cancellation_callback<T>( + mut callback: StateCallback<T>, + transports_to_cancel: Vec<Arc<Mutex<Box<dyn AuthenticatorTransport + Send>>>>, +) -> StateCallback<T> { + callback.add_uncloneable_observer(Box::new(move || { + debug!( + "Callback observer is running, cancelling \ + {} unchosen transports...", + transports_to_cancel.len() + ); + for transport_mutex in &transports_to_cancel { + if let Err(e) = transport_mutex.lock().unwrap().cancel() { + error!("Cancellation failed: {:?}", e); + } + } + })); + callback +} + +impl AuthenticatorService { + pub fn new(ctap_version: CtapVersion) -> crate::Result<Self> { + Ok(Self { + transports: Vec::new(), + ctap_version, + }) + } + + /// Add any detected platform transports + pub fn add_detected_transports(&mut self) { + self.add_u2f_usb_hid_platform_transports(); + } + + fn add_transport(&mut self, boxed_token: Box<dyn AuthenticatorTransport + Send>) { + self.transports.push(Arc::new(Mutex::new(boxed_token))) + } + + pub fn add_u2f_usb_hid_platform_transports(&mut self) { + if self.ctap_version == CtapVersion::CTAP1 { + match crate::U2FManager::new() { + Ok(token) => self.add_transport(Box::new(token)), + Err(e) => error!("Could not add U2F HID transport: {}", e), + } + } else { + match Manager::new() { + Ok(token) => self.add_transport(Box::new(token)), + Err(e) => error!("Could not add CTAP2 HID transport: {}", e), + } + } + } + + #[cfg(feature = "webdriver")] + pub fn add_webdriver_virtual_bus(&mut self) { + match crate::virtualdevices::webdriver::VirtualManager::new() { + Ok(token) => { + println!("WebDriver ready, listening at {}", &token.url()); + self.add_transport(Box::new(token)); + } + Err(e) => error!("Could not add WebDriver virtual bus: {}", e), + } + } + + pub fn register( + &mut self, + timeout: u64, + ctap_args: RegisterArgs, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::RegisterResult>>, + ) -> crate::Result<()> { + match ctap_args { + RegisterArgs::CTAP1(a) => self.register_ctap1(timeout, a, status, callback), + RegisterArgs::CTAP2(a) => self.register_ctap2(timeout, a, status, callback), + } + } + + fn register_ctap1( + &mut self, + timeout: u64, + args: RegisterArgsCtap1, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::RegisterResult>>, + ) -> crate::Result<()> { + if args.challenge.len() != PARAMETER_SIZE || args.application.len() != PARAMETER_SIZE { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + + for key_handle in &args.key_handles { + if key_handle.credential.len() >= 256 { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + } + + let iterable_transports = self.transports.clone(); + if iterable_transports.is_empty() { + return Err(AuthenticatorError::NoConfiguredTransports); + } + + debug!( + "register called with {} transports, iterable is {}", + self.transports.len(), + iterable_transports.len() + ); + + for (idx, transport_mutex) in iterable_transports.iter().enumerate() { + let mut transports_to_cancel = iterable_transports.clone(); + transports_to_cancel.remove(idx); + + debug!( + "register transports_to_cancel {}", + transports_to_cancel.len() + ); + + transport_mutex.lock().unwrap().register( + timeout, + RegisterArgs::CTAP1(args.clone()), + status.clone(), + clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel), + )?; + } + + Ok(()) + } + + fn register_ctap2( + &mut self, + timeout: u64, + args: RegisterArgsCtap2, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::RegisterResult>>, + ) -> crate::Result<()> { + let iterable_transports = self.transports.clone(); + if iterable_transports.is_empty() { + return Err(AuthenticatorError::NoConfiguredTransports); + } + + debug!( + "register called with {} transports, iterable is {}", + self.transports.len(), + iterable_transports.len() + ); + + for (idx, transport_mutex) in iterable_transports.iter().enumerate() { + let mut transports_to_cancel = iterable_transports.clone(); + transports_to_cancel.remove(idx); + + debug!( + "register transports_to_cancel {}", + transports_to_cancel.len() + ); + + transport_mutex.lock().unwrap().register( + timeout, + RegisterArgs::CTAP2(args.clone()), + status.clone(), + clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel), + )?; + } + + Ok(()) + } + + pub fn sign( + &mut self, + timeout: u64, + ctap_args: SignArgs, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::SignResult>>, + ) -> crate::Result<()> { + match ctap_args { + SignArgs::CTAP1(a) => self.sign_ctap1(timeout, a, status, callback), + SignArgs::CTAP2(a) => self.sign_ctap2(timeout, a, status, callback), + } + } + + pub fn sign_ctap1( + &mut self, + timeout: u64, + args: SignArgsCtap1, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::SignResult>>, + ) -> crate::Result<()> { + if args.challenge.len() != PARAMETER_SIZE { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + + if args.app_ids.is_empty() { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + + for app_id in &args.app_ids { + if app_id.len() != PARAMETER_SIZE { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + } + + for key_handle in &args.key_handles { + if key_handle.credential.len() >= 256 { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + } + + let iterable_transports = self.transports.clone(); + if iterable_transports.is_empty() { + return Err(AuthenticatorError::NoConfiguredTransports); + } + + for (idx, transport_mutex) in iterable_transports.iter().enumerate() { + let mut transports_to_cancel = iterable_transports.clone(); + transports_to_cancel.remove(idx); + + transport_mutex.lock().unwrap().sign( + timeout, + SignArgs::CTAP1(args.clone()), + status.clone(), + clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel), + )?; + } + + Ok(()) + } + + pub fn sign_ctap2( + &mut self, + timeout: u64, + args: SignArgsCtap2, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::SignResult>>, + ) -> crate::Result<()> { + let iterable_transports = self.transports.clone(); + if iterable_transports.is_empty() { + return Err(AuthenticatorError::NoConfiguredTransports); + } + + for (idx, transport_mutex) in iterable_transports.iter().enumerate() { + let mut transports_to_cancel = iterable_transports.clone(); + transports_to_cancel.remove(idx); + + transport_mutex.lock().unwrap().sign( + timeout, + SignArgs::CTAP2(args.clone()), + status.clone(), + clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel), + )?; + } + + Ok(()) + } + + pub fn cancel(&mut self) -> crate::Result<()> { + if self.transports.is_empty() { + return Err(AuthenticatorError::NoConfiguredTransports); + } + + for transport_mutex in &mut self.transports { + transport_mutex.lock().unwrap().cancel()?; + } + + Ok(()) + } + + pub fn reset( + &mut self, + timeout: u64, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::ResetResult>>, + ) -> crate::Result<()> { + let iterable_transports = self.transports.clone(); + if iterable_transports.is_empty() { + return Err(AuthenticatorError::NoConfiguredTransports); + } + + debug!( + "reset called with {} transports, iterable is {}", + self.transports.len(), + iterable_transports.len() + ); + + for (idx, transport_mutex) in iterable_transports.iter().enumerate() { + let mut transports_to_cancel = iterable_transports.clone(); + transports_to_cancel.remove(idx); + + debug!("reset transports_to_cancel {}", transports_to_cancel.len()); + + transport_mutex.lock().unwrap().reset( + timeout, + status.clone(), + clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel), + )?; + } + + Ok(()) + } + + pub fn set_pin( + &mut self, + timeout: u64, + new_pin: Pin, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::ResetResult>>, + ) -> crate::Result<()> { + let iterable_transports = self.transports.clone(); + if iterable_transports.is_empty() { + return Err(AuthenticatorError::NoConfiguredTransports); + } + + debug!( + "reset called with {} transports, iterable is {}", + self.transports.len(), + iterable_transports.len() + ); + + for (idx, transport_mutex) in iterable_transports.iter().enumerate() { + let mut transports_to_cancel = iterable_transports.clone(); + transports_to_cancel.remove(idx); + + debug!("reset transports_to_cancel {}", transports_to_cancel.len()); + + transport_mutex.lock().unwrap().set_pin( + timeout, + new_pin.clone(), + status.clone(), + clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel), + )?; + } + + Ok(()) + } +} + +//////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////// + +#[cfg(test)] +mod tests { + use super::{ + AuthenticatorService, AuthenticatorTransport, CtapVersion, Pin, + PublicKeyCredentialDescriptor, RegisterArgs, RegisterArgsCtap1, RegisterArgsCtap2, + SignArgs, SignArgsCtap1, SignArgsCtap2, User, + }; + use crate::consts::Capability; + use crate::consts::PARAMETER_SIZE; + use crate::ctap2::server::RelyingParty; + use crate::statecallback::StateCallback; + use crate::{AuthenticatorTransports, KeyHandle, RegisterFlags, SignFlags, StatusUpdate}; + use crate::{RegisterResult, SignResult}; + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::mpsc::{channel, Sender}; + use std::sync::Arc; + use std::{io, thread}; + + fn init() { + let _ = env_logger::builder().is_test(true).try_init(); + } + + pub struct TestTransportDriver { + consent: bool, + was_cancelled: Arc<AtomicBool>, + } + + impl TestTransportDriver { + pub fn new(consent: bool) -> io::Result<Self> { + Ok(Self { + consent, + was_cancelled: Arc::new(AtomicBool::new(false)), + }) + } + } + + impl TestTransportDriver { + fn dev_info(&self) -> crate::u2ftypes::U2FDeviceInfo { + crate::u2ftypes::U2FDeviceInfo { + vendor_name: String::from("Mozilla").into_bytes(), + device_name: String::from("Test Transport Token").into_bytes(), + version_interface: 0, + version_major: 1, + version_minor: 2, + version_build: 3, + cap_flags: Capability::empty(), + } + } + } + + impl AuthenticatorTransport for TestTransportDriver { + fn register( + &mut self, + _timeout: u64, + _args: RegisterArgs, + _status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::RegisterResult>>, + ) -> crate::Result<()> { + if self.consent { + let rv = Ok(RegisterResult::CTAP1(vec![0u8; 16], self.dev_info())); + thread::spawn(move || callback.call(rv)); + } + Ok(()) + } + + fn sign( + &mut self, + _timeout: u64, + _ctap_args: SignArgs, + _status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::SignResult>>, + ) -> crate::Result<()> { + if self.consent { + let rv = Ok(SignResult::CTAP1( + vec![0u8; 0], + vec![0u8; 0], + vec![0u8; 0], + self.dev_info(), + )); + thread::spawn(move || callback.call(rv)); + } + Ok(()) + } + + fn cancel(&mut self) -> crate::Result<()> { + self.was_cancelled + .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) + .map_or( + Err(crate::errors::AuthenticatorError::U2FToken( + crate::errors::U2FTokenError::InvalidState, + )), + |_| Ok(()), + ) + } + + fn reset( + &mut self, + _timeout: u64, + _status: Sender<crate::StatusUpdate>, + _callback: StateCallback<crate::Result<crate::ResetResult>>, + ) -> crate::Result<()> { + unimplemented!(); + } + + fn set_pin( + &mut self, + _timeout: u64, + _new_pin: Pin, + _status: Sender<crate::StatusUpdate>, + _callback: StateCallback<crate::Result<crate::ResetResult>>, + ) -> crate::Result<()> { + unimplemented!(); + } + } + + fn mk_key() -> KeyHandle { + KeyHandle { + credential: vec![0], + transports: AuthenticatorTransports::USB, + } + } + + fn mk_challenge() -> Vec<u8> { + vec![0x11; PARAMETER_SIZE] + } + + fn mk_appid() -> Vec<u8> { + vec![0x22; PARAMETER_SIZE] + } + + #[test] + fn test_no_challenge() { + init(); + let (status_tx, _) = channel::<StatusUpdate>(); + + let mut s = AuthenticatorService::new(CtapVersion::CTAP1).unwrap(); + s.add_transport(Box::new(TestTransportDriver::new(true).unwrap())); + + assert_matches!( + s.register( + 1_000, + RegisterArgsCtap1 { + challenge: vec![], + flags: RegisterFlags::empty(), + application: mk_appid(), + key_handles: vec![mk_key()], + } + .into(), + status_tx.clone(), + StateCallback::new(Box::new(move |_rv| {})), + ) + .unwrap_err(), + crate::errors::AuthenticatorError::InvalidRelyingPartyInput + ); + + assert_matches!( + s.sign( + 1_000, + SignArgsCtap1 { + flags: SignFlags::empty(), + challenge: vec![], + app_ids: vec![mk_appid()], + key_handles: vec![mk_key()] + } + .into(), + status_tx, + StateCallback::new(Box::new(move |_rv| {})), + ) + .unwrap_err(), + crate::errors::AuthenticatorError::InvalidRelyingPartyInput + ); + } + + #[test] + fn test_no_appids() { + init(); + let (status_tx, _) = channel::<StatusUpdate>(); + + let mut s = AuthenticatorService::new(CtapVersion::CTAP1).unwrap(); + s.add_transport(Box::new(TestTransportDriver::new(true).unwrap())); + + assert_matches!( + s.register( + 1_000, + RegisterArgsCtap1 { + challenge: mk_challenge(), + flags: RegisterFlags::empty(), + application: vec![], + key_handles: vec![mk_key()], + } + .into(), + status_tx.clone(), + StateCallback::new(Box::new(move |_rv| {})), + ) + .unwrap_err(), + crate::errors::AuthenticatorError::InvalidRelyingPartyInput + ); + + assert_matches!( + s.sign( + 1_000, + SignArgsCtap1 { + flags: SignFlags::empty(), + challenge: mk_challenge(), + app_ids: vec![], + key_handles: vec![mk_key()] + } + .into(), + status_tx, + StateCallback::new(Box::new(move |_rv| {})), + ) + .unwrap_err(), + crate::errors::AuthenticatorError::InvalidRelyingPartyInput + ); + } + + #[test] + fn test_no_keys() { + init(); + // No Keys is a resident-key use case. For U2F this would time out, + // but the actual reactions are up to the service implementation. + // This test yields OKs. + let (status_tx, _) = channel::<StatusUpdate>(); + + let mut s = AuthenticatorService::new(CtapVersion::CTAP1).unwrap(); + s.add_transport(Box::new(TestTransportDriver::new(true).unwrap())); + + assert_matches!( + s.register( + 100, + RegisterArgsCtap1 { + challenge: mk_challenge(), + flags: RegisterFlags::empty(), + application: mk_appid(), + key_handles: vec![], + } + .into(), + status_tx.clone(), + StateCallback::new(Box::new(move |_rv| {})), + ), + Ok(()) + ); + + assert_matches!( + s.sign( + 100, + SignArgsCtap1 { + flags: SignFlags::empty(), + challenge: mk_challenge(), + app_ids: vec![mk_appid()], + key_handles: vec![] + } + .into(), + status_tx, + StateCallback::new(Box::new(move |_rv| {})), + ), + Ok(()) + ); + } + + #[test] + fn test_large_keys() { + init(); + let (status_tx, _) = channel::<StatusUpdate>(); + + let large_key = KeyHandle { + credential: vec![0; 257], + transports: AuthenticatorTransports::USB, + }; + + let mut s = AuthenticatorService::new(CtapVersion::CTAP1).unwrap(); + s.add_transport(Box::new(TestTransportDriver::new(true).unwrap())); + + assert_matches!( + s.register( + 1_000, + RegisterArgsCtap1 { + challenge: mk_challenge(), + flags: RegisterFlags::empty(), + application: mk_appid(), + key_handles: vec![large_key.clone()], + } + .into(), + status_tx.clone(), + StateCallback::new(Box::new(move |_rv| {})), + ) + .unwrap_err(), + crate::errors::AuthenticatorError::InvalidRelyingPartyInput + ); + + assert_matches!( + s.sign( + 1_000, + SignArgsCtap1 { + flags: SignFlags::empty(), + challenge: mk_challenge(), + app_ids: vec![mk_appid()], + key_handles: vec![large_key] + } + .into(), + status_tx, + StateCallback::new(Box::new(move |_rv| {})), + ) + .unwrap_err(), + crate::errors::AuthenticatorError::InvalidRelyingPartyInput + ); + } + + #[test] + fn test_large_keys_ctap2() { + init(); + let (status_tx, _) = channel::<StatusUpdate>(); + + let large_key = KeyHandle { + credential: vec![0; 1000], + transports: AuthenticatorTransports::USB, + }; + + let mut s = AuthenticatorService::new(CtapVersion::CTAP2).unwrap(); + s.add_transport(Box::new(TestTransportDriver::new(true).unwrap())); + + let ctap2_register_args = RegisterArgsCtap2 { + challenge: mk_challenge(), + relying_party: RelyingParty { + id: "example.com".to_string(), + name: None, + icon: None, + }, + origin: "example.com".to_string(), + user: User { + id: "user_id".as_bytes().to_vec(), + icon: None, + name: Some("A. User".to_string()), + display_name: None, + }, + pub_cred_params: vec![], + exclude_list: vec![(&large_key).into()], + options: Default::default(), + extensions: Default::default(), + pin: None, + }; + + assert!(s + .register( + 1_000, + ctap2_register_args.into(), + status_tx.clone(), + StateCallback::new(Box::new(move |_rv| {})), + ) + .is_ok(),); + + let ctap2_sign_args = SignArgsCtap2 { + challenge: mk_challenge(), + origin: "example.com".to_string(), + relying_party_id: "example.com".to_string(), + allow_list: vec![(&large_key).into()], + options: Default::default(), + extensions: Default::default(), + pin: None, + }; + assert!(s + .sign( + 1_000, + ctap2_sign_args.into(), + status_tx, + StateCallback::new(Box::new(move |_rv| {})), + ) + .is_ok(),); + } + + #[test] + fn test_no_transports() { + init(); + let (status_tx, _) = channel::<StatusUpdate>(); + + let mut s = AuthenticatorService::new(CtapVersion::CTAP1).unwrap(); + assert_matches!( + s.register( + 1_000, + RegisterArgsCtap1 { + challenge: mk_challenge(), + flags: RegisterFlags::empty(), + application: mk_appid(), + key_handles: vec![mk_key()], + } + .into(), + status_tx.clone(), + StateCallback::new(Box::new(move |_rv| {})), + ) + .unwrap_err(), + crate::errors::AuthenticatorError::NoConfiguredTransports + ); + + assert_matches!( + s.sign( + 1_000, + SignArgsCtap1 { + flags: SignFlags::empty(), + challenge: mk_challenge(), + app_ids: vec![mk_appid()], + key_handles: vec![mk_key()] + } + .into(), + status_tx, + StateCallback::new(Box::new(move |_rv| {})), + ) + .unwrap_err(), + crate::errors::AuthenticatorError::NoConfiguredTransports + ); + + assert_matches!( + s.cancel().unwrap_err(), + crate::errors::AuthenticatorError::NoConfiguredTransports + ); + } + + #[test] + fn test_cancellation_register() { + init(); + let (status_tx, _) = channel::<StatusUpdate>(); + + let mut s = AuthenticatorService::new(CtapVersion::CTAP1).unwrap(); + let ttd_one = TestTransportDriver::new(true).unwrap(); + let ttd_two = TestTransportDriver::new(false).unwrap(); + let ttd_three = TestTransportDriver::new(false).unwrap(); + + let was_cancelled_one = ttd_one.was_cancelled.clone(); + let was_cancelled_two = ttd_two.was_cancelled.clone(); + let was_cancelled_three = ttd_three.was_cancelled.clone(); + + s.add_transport(Box::new(ttd_one)); + s.add_transport(Box::new(ttd_two)); + s.add_transport(Box::new(ttd_three)); + + let callback = StateCallback::new(Box::new(move |_rv| {})); + assert!(s + .register( + 1_000, + RegisterArgsCtap1 { + challenge: mk_challenge(), + flags: RegisterFlags::empty(), + application: mk_appid(), + key_handles: vec![], + } + .into(), + status_tx, + callback.clone(), + ) + .is_ok()); + callback.wait(); + + assert_eq!(was_cancelled_one.load(Ordering::SeqCst), false); + assert_eq!(was_cancelled_two.load(Ordering::SeqCst), true); + assert_eq!(was_cancelled_three.load(Ordering::SeqCst), true); + } + + #[test] + fn test_cancellation_sign() { + init(); + let (status_tx, _) = channel::<StatusUpdate>(); + + let mut s = AuthenticatorService::new(CtapVersion::CTAP1).unwrap(); + let ttd_one = TestTransportDriver::new(true).unwrap(); + let ttd_two = TestTransportDriver::new(false).unwrap(); + let ttd_three = TestTransportDriver::new(false).unwrap(); + + let was_cancelled_one = ttd_one.was_cancelled.clone(); + let was_cancelled_two = ttd_two.was_cancelled.clone(); + let was_cancelled_three = ttd_three.was_cancelled.clone(); + + s.add_transport(Box::new(ttd_one)); + s.add_transport(Box::new(ttd_two)); + s.add_transport(Box::new(ttd_three)); + + let callback = StateCallback::new(Box::new(move |_rv| {})); + assert!(s + .sign( + 1_000, + SignArgsCtap1 { + flags: SignFlags::empty(), + challenge: mk_challenge(), + app_ids: vec![mk_appid()], + key_handles: vec![mk_key()] + } + .into(), + status_tx, + callback.clone(), + ) + .is_ok()); + callback.wait(); + + assert_eq!(was_cancelled_one.load(Ordering::SeqCst), false); + assert_eq!(was_cancelled_two.load(Ordering::SeqCst), true); + assert_eq!(was_cancelled_three.load(Ordering::SeqCst), true); + } + + #[test] + fn test_cancellation_race() { + init(); + let (status_tx, _) = channel::<StatusUpdate>(); + + let mut s = AuthenticatorService::new(CtapVersion::CTAP1).unwrap(); + // Let both of these race which one provides consent. + let ttd_one = TestTransportDriver::new(true).unwrap(); + let ttd_two = TestTransportDriver::new(true).unwrap(); + + let was_cancelled_one = ttd_one.was_cancelled.clone(); + let was_cancelled_two = ttd_two.was_cancelled.clone(); + + s.add_transport(Box::new(ttd_one)); + s.add_transport(Box::new(ttd_two)); + + let callback = StateCallback::new(Box::new(move |_rv| {})); + assert!(s + .register( + 1_000, + RegisterArgsCtap1 { + challenge: mk_challenge(), + flags: RegisterFlags::empty(), + application: mk_appid(), + key_handles: vec![], + } + .into(), + status_tx, + callback.clone(), + ) + .is_ok()); + callback.wait(); + + let one = was_cancelled_one.load(Ordering::SeqCst); + let two = was_cancelled_two.load(Ordering::SeqCst); + assert_eq!( + one ^ two, + true, + "asserting that one={} xor two={} is true", + one, + two + ); + } +} diff --git a/third_party/rust/authenticator/src/capi.rs b/third_party/rust/authenticator/src/capi.rs new file mode 100644 index 0000000000..078f976e6c --- /dev/null +++ b/third_party/rust/authenticator/src/capi.rs @@ -0,0 +1,402 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::authenticatorservice::{ + AuthenticatorService, CtapVersion, RegisterArgsCtap1, SignArgsCtap1, +}; +use crate::errors; +use crate::statecallback::StateCallback; +use crate::{RegisterResult, SignResult}; +use libc::size_t; +use rand::{thread_rng, Rng}; +use std::collections::HashMap; +use std::sync::mpsc::channel; +use std::thread; +use std::{ptr, slice}; + +type U2FAppIds = Vec<crate::AppId>; +type U2FKeyHandles = Vec<crate::KeyHandle>; +type U2FCallback = extern "C" fn(u64, *mut U2FResult); + +pub enum U2FResult { + Success(HashMap<u8, Vec<u8>>), + Error(errors::AuthenticatorError), +} + +const RESBUF_ID_REGISTRATION: u8 = 0; +const RESBUF_ID_KEYHANDLE: u8 = 1; +const RESBUF_ID_SIGNATURE: u8 = 2; +const RESBUF_ID_APPID: u8 = 3; +const RESBUF_ID_VENDOR_NAME: u8 = 4; +const RESBUF_ID_DEVICE_NAME: u8 = 5; +const RESBUF_ID_FIRMWARE_MAJOR: u8 = 6; +const RESBUF_ID_FIRMWARE_MINOR: u8 = 7; +const RESBUF_ID_FIRMWARE_BUILD: u8 = 8; + +// Generates a new 64-bit transaction id with collision probability 2^-32. +fn new_tid() -> u64 { + thread_rng().gen::<u64>() +} + +unsafe fn from_raw(ptr: *const u8, len: usize) -> Vec<u8> { + slice::from_raw_parts(ptr, len).to_vec() +} + +/// # Safety +/// +/// The handle returned by this method must be freed by the caller. +#[no_mangle] +pub extern "C" fn rust_u2f_mgr_new() -> *mut AuthenticatorService { + if let Ok(mut mgr) = AuthenticatorService::new(CtapVersion::CTAP1) { + mgr.add_detected_transports(); + Box::into_raw(Box::new(mgr)) + } else { + ptr::null_mut() + } +} + +/// # Safety +/// +/// This method must not be called on a handle twice, and the handle is unusable +/// after. +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_mgr_free(mgr: *mut AuthenticatorService) { + if !mgr.is_null() { + drop(Box::from_raw(mgr)); + } +} + +/// # Safety +/// +/// The handle returned by this method must be freed by the caller. +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_app_ids_new() -> *mut U2FAppIds { + Box::into_raw(Box::new(vec![])) +} + +/// # Safety +/// +/// This method must be used on an actual U2FAppIds handle +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_app_ids_add( + ids: *mut U2FAppIds, + id_ptr: *const u8, + id_len: usize, +) { + (*ids).push(from_raw(id_ptr, id_len)); +} + +/// # Safety +/// +/// This method must not be called on a handle twice, and the handle is unusable +/// after. +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_app_ids_free(ids: *mut U2FAppIds) { + if !ids.is_null() { + drop(Box::from_raw(ids)); + } +} + +/// # Safety +/// +/// The handle returned by this method must be freed by the caller. +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_khs_new() -> *mut U2FKeyHandles { + Box::into_raw(Box::new(vec![])) +} + +/// # Safety +/// +/// This method must be used on an actual U2FKeyHandles handle +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_khs_add( + khs: *mut U2FKeyHandles, + key_handle_ptr: *const u8, + key_handle_len: usize, + transports: u8, +) { + (*khs).push(crate::KeyHandle { + credential: from_raw(key_handle_ptr, key_handle_len), + transports: crate::AuthenticatorTransports::from_bits_truncate(transports), + }); +} + +/// # Safety +/// +/// This method must not be called on a handle twice, and the handle is unusable +/// after. +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_khs_free(khs: *mut U2FKeyHandles) { + if !khs.is_null() { + drop(Box::from_raw(khs)); + } +} + +/// # Safety +/// +/// This method must be used on an actual U2FResult handle +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_result_error(res: *const U2FResult) -> u8 { + if res.is_null() { + return errors::U2FTokenError::Unknown as u8; + } + + if let U2FResult::Error(ref err) = *res { + return err.as_u2f_errorcode(); + } + + 0 /* No error, the request succeeded. */ +} + +/// # Safety +/// +/// This method must be used before rust_u2f_resbuf_copy +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_resbuf_contains(res: *const U2FResult, bid: u8) -> bool { + if res.is_null() { + return false; + } + + if let U2FResult::Success(ref bufs) = *res { + return bufs.contains_key(&bid); + } + + false +} + +/// # Safety +/// +/// This method must be used before rust_u2f_resbuf_copy +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_resbuf_length( + res: *const U2FResult, + bid: u8, + len: *mut size_t, +) -> bool { + if res.is_null() { + return false; + } + + if let U2FResult::Success(ref bufs) = *res { + if let Some(buf) = bufs.get(&bid) { + *len = buf.len(); + return true; + } + } + + false +} + +/// # Safety +/// +/// This method does not ensure anything about dst before copying, so +/// ensure it is long enough (using rust_u2f_resbuf_length) +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_resbuf_copy( + res: *const U2FResult, + bid: u8, + dst: *mut u8, +) -> bool { + if res.is_null() { + return false; + } + + if let U2FResult::Success(ref bufs) = *res { + if let Some(buf) = bufs.get(&bid) { + ptr::copy_nonoverlapping(buf.as_ptr(), dst, buf.len()); + return true; + } + } + + false +} + +/// # Safety +/// +/// This method should not be called on U2FResult handles after they've been +/// freed or a double-free will occur +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_res_free(res: *mut U2FResult) { + if !res.is_null() { + drop(Box::from_raw(res)); + } +} + +/// # Safety +/// +/// This method should not be called on AuthenticatorService handles after +/// they've been freed +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_mgr_register( + mgr: *mut AuthenticatorService, + flags: u64, + timeout: u64, + callback: U2FCallback, + challenge_ptr: *const u8, + challenge_len: usize, + application_ptr: *const u8, + application_len: usize, + khs: *const U2FKeyHandles, +) -> u64 { + if mgr.is_null() { + return 0; + } + + // Check buffers. + if challenge_ptr.is_null() || application_ptr.is_null() { + return 0; + } + + let flags = crate::RegisterFlags::from_bits_truncate(flags); + let challenge = from_raw(challenge_ptr, challenge_len); + let application = from_raw(application_ptr, application_len); + let key_handles = (*khs).clone(); + + let (status_tx, status_rx) = channel::<crate::StatusUpdate>(); + thread::spawn(move || loop { + // Issue https://github.com/mozilla/authenticator-rs/issues/132 will + // plumb the status channel through to the actual C API signatures + match status_rx.recv() { + Ok(_) => {} + Err(_recv_error) => return, + } + }); + + let tid = new_tid(); + + let state_callback = StateCallback::<crate::Result<RegisterResult>>::new(Box::new(move |rv| { + let result = match rv { + Ok(RegisterResult::CTAP1(registration, dev_info)) => { + let mut bufs = HashMap::new(); + bufs.insert(RESBUF_ID_REGISTRATION, registration); + bufs.insert(RESBUF_ID_VENDOR_NAME, dev_info.vendor_name); + bufs.insert(RESBUF_ID_DEVICE_NAME, dev_info.device_name); + bufs.insert(RESBUF_ID_FIRMWARE_MAJOR, vec![dev_info.version_major]); + bufs.insert(RESBUF_ID_FIRMWARE_MINOR, vec![dev_info.version_minor]); + bufs.insert(RESBUF_ID_FIRMWARE_BUILD, vec![dev_info.version_build]); + U2FResult::Success(bufs) + } + Ok(RegisterResult::CTAP2(..)) => U2FResult::Error( + errors::AuthenticatorError::VersionMismatch("rust_u2f_mgr_register", 1), + ), + Err(e) => U2FResult::Error(e), + }; + + callback(tid, Box::into_raw(Box::new(result))); + })); + let ctap_args = RegisterArgsCtap1 { + flags, + challenge, + application, + key_handles, + }; + + let res = (*mgr).register(timeout, ctap_args.into(), status_tx, state_callback); + + if res.is_ok() { + tid + } else { + 0 + } +} + +/// # Safety +/// +/// This method should not be called on AuthenticatorService handles after +/// they've been freed +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_mgr_sign( + mgr: *mut AuthenticatorService, + flags: u64, + timeout: u64, + callback: U2FCallback, + challenge_ptr: *const u8, + challenge_len: usize, + app_ids: *const U2FAppIds, + khs: *const U2FKeyHandles, +) -> u64 { + if mgr.is_null() || khs.is_null() { + return 0; + } + + // Check buffers. + if challenge_ptr.is_null() { + return 0; + } + + // Need at least one app_id. + if (*app_ids).is_empty() { + return 0; + } + + let flags = crate::SignFlags::from_bits_truncate(flags); + let challenge = from_raw(challenge_ptr, challenge_len); + let app_ids = (*app_ids).clone(); + let key_handles = (*khs).clone(); + + let (status_tx, status_rx) = channel::<crate::StatusUpdate>(); + thread::spawn(move || loop { + // Issue https://github.com/mozilla/authenticator-rs/issues/132 will + // plumb the status channel through to the actual C API signatures + match status_rx.recv() { + Ok(_) => {} + Err(_recv_error) => return, + } + }); + + let tid = new_tid(); + let state_callback = StateCallback::<crate::Result<SignResult>>::new(Box::new(move |rv| { + let result = match rv { + Ok(SignResult::CTAP1(app_id, key_handle, signature, dev_info)) => { + let mut bufs = HashMap::new(); + bufs.insert(RESBUF_ID_KEYHANDLE, key_handle); + bufs.insert(RESBUF_ID_SIGNATURE, signature); + bufs.insert(RESBUF_ID_APPID, app_id); + bufs.insert(RESBUF_ID_VENDOR_NAME, dev_info.vendor_name); + bufs.insert(RESBUF_ID_DEVICE_NAME, dev_info.device_name); + bufs.insert(RESBUF_ID_FIRMWARE_MAJOR, vec![dev_info.version_major]); + bufs.insert(RESBUF_ID_FIRMWARE_MINOR, vec![dev_info.version_minor]); + bufs.insert(RESBUF_ID_FIRMWARE_BUILD, vec![dev_info.version_build]); + U2FResult::Success(bufs) + } + Ok(SignResult::CTAP2(..)) => U2FResult::Error( + errors::AuthenticatorError::VersionMismatch("rust_u2f_mgr_sign", 1), + ), + Err(e) => U2FResult::Error(e), + }; + + callback(tid, Box::into_raw(Box::new(result))); + })); + + let res = (*mgr).sign( + timeout, + SignArgsCtap1 { + flags, + challenge, + app_ids, + key_handles, + } + .into(), + status_tx, + state_callback, + ); + + if res.is_ok() { + tid + } else { + 0 + } +} + +/// # Safety +/// +/// This method should not be called AuthenticatorService handles after they've +/// been freed +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_mgr_cancel(mgr: *mut AuthenticatorService) { + if !mgr.is_null() { + // Ignore return value. + let _ = (*mgr).cancel(); + } +} diff --git a/third_party/rust/authenticator/src/consts.rs b/third_party/rust/authenticator/src/consts.rs new file mode 100644 index 0000000000..e96bd202c6 --- /dev/null +++ b/third_party/rust/authenticator/src/consts.rs @@ -0,0 +1,160 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Allow dead code in this module, since it's all packet consts anyways. +#![allow(dead_code)] + +use serde::Serialize; + +pub const MAX_HID_RPT_SIZE: usize = 64; + +/// Minimum size of the U2F Raw Message header (FIDO v1.x) in extended mode, +/// including expected response length (L<sub>e</sub>). +/// +/// Fields `CLA`, `INS`, `P1` and `P2` are 1 byte each, and L<sub>e</sub> is 3 +/// bytes. If there is a data payload, add 2 bytes (L<sub>c</sub> is 3 bytes, +/// and L<sub>e</sub> is 2 bytes). +pub const U2FAPDUHEADER_SIZE: usize = 7; + +pub const CID_BROADCAST: [u8; 4] = [0xff, 0xff, 0xff, 0xff]; +pub const TYPE_MASK: u8 = 0x80; +pub const TYPE_INIT: u8 = 0x80; +pub const TYPE_CONT: u8 = 0x80; + +// Size of header in U2F Init USB HID Packets +pub const INIT_HEADER_SIZE: usize = 7; +// Size of header in U2F Cont USB HID Packets +pub const CONT_HEADER_SIZE: usize = 5; + +pub const PARAMETER_SIZE: usize = 32; + +pub const FIDO_USAGE_PAGE: u16 = 0xf1d0; // FIDO alliance HID usage page +pub const FIDO_USAGE_U2FHID: u16 = 0x01; // U2FHID usage for top-level collection +pub const FIDO_USAGE_DATA_IN: u8 = 0x20; // Raw IN data report +pub const FIDO_USAGE_DATA_OUT: u8 = 0x21; // Raw OUT data report + +// General pub constants + +pub const U2FHID_IF_VERSION: u32 = 2; // Current interface implementation version +pub const U2FHID_FRAME_TIMEOUT: u32 = 500; // Default frame timeout in ms +pub const U2FHID_TRANS_TIMEOUT: u32 = 3000; // Default message timeout in ms + +// CTAPHID native commands +const CTAPHID_PING: u8 = TYPE_INIT | 0x01; // Echo data through local processor only +const CTAPHID_MSG: u8 = TYPE_INIT | 0x03; // Send U2F message frame +const CTAPHID_LOCK: u8 = TYPE_INIT | 0x04; // Send lock channel command +const CTAPHID_INIT: u8 = TYPE_INIT | 0x06; // Channel initialization +const CTAPHID_WINK: u8 = TYPE_INIT | 0x08; // Send device identification wink +const CTAPHID_CBOR: u8 = TYPE_INIT | 0x10; // Encapsulated CBOR encoded message +const CTAPHID_CANCEL: u8 = TYPE_INIT | 0x11; // Cancel outstanding requests +const CTAPHID_KEEPALIVE: u8 = TYPE_INIT | 0x3b; // Keepalive sent to authenticator every 100ms and whenever a status changes +const CTAPHID_ERROR: u8 = TYPE_INIT | 0x3f; // Error response + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[repr(u8)] +pub enum HIDCmd { + Ping, + Msg, + Lock, + Init, + Wink, + Cbor, + Cancel, + Keepalive, + Error, + Unknown(u8), +} + +impl Into<u8> for HIDCmd { + fn into(self) -> u8 { + match self { + HIDCmd::Ping => CTAPHID_PING, + HIDCmd::Msg => CTAPHID_MSG, + HIDCmd::Lock => CTAPHID_LOCK, + HIDCmd::Init => CTAPHID_INIT, + HIDCmd::Wink => CTAPHID_WINK, + HIDCmd::Cbor => CTAPHID_CBOR, + HIDCmd::Cancel => CTAPHID_CANCEL, + HIDCmd::Keepalive => CTAPHID_KEEPALIVE, + HIDCmd::Error => CTAPHID_ERROR, + HIDCmd::Unknown(v) => v, + } + } +} + +impl From<u8> for HIDCmd { + fn from(v: u8) -> HIDCmd { + match v { + CTAPHID_PING => HIDCmd::Ping, + CTAPHID_MSG => HIDCmd::Msg, + CTAPHID_LOCK => HIDCmd::Lock, + CTAPHID_INIT => HIDCmd::Init, + CTAPHID_WINK => HIDCmd::Wink, + CTAPHID_CBOR => HIDCmd::Cbor, + CTAPHID_CANCEL => HIDCmd::Cancel, + CTAPHID_KEEPALIVE => HIDCmd::Keepalive, + CTAPHID_ERROR => HIDCmd::Error, + v => HIDCmd::Unknown(v), + } + } +} + +// U2FHID_MSG commands +pub const U2F_VENDOR_FIRST: u8 = TYPE_INIT | 0x40; // First vendor defined command +pub const U2F_VENDOR_LAST: u8 = TYPE_INIT | 0x7f; // Last vendor defined command +pub const U2F_REGISTER: u8 = 0x01; // Registration command +pub const U2F_AUTHENTICATE: u8 = 0x02; // Authenticate/sign command +pub const U2F_VERSION: u8 = 0x03; // Read version string command + +pub const YKPIV_INS_GET_VERSION: u8 = 0xfd; // Get firmware version, yubico ext + +// U2F_REGISTER command defines +pub const U2F_REGISTER_ID: u8 = 0x05; // Version 2 registration identifier +pub const U2F_REGISTER_HASH_ID: u8 = 0x00; // Version 2 hash identintifier + +// U2F_AUTHENTICATE command defines +pub const U2F_REQUEST_USER_PRESENCE: u8 = 0x03; // Verify user presence and sign +pub const U2F_CHECK_IS_REGISTERED: u8 = 0x07; // Check if the key handle is registered + +// U2FHID_INIT command defines +pub const INIT_NONCE_SIZE: usize = 8; // Size of channel initialization challenge + +bitflags! { + #[derive(Serialize)] + pub struct Capability: u8 { + const WINK = 0x01; + const LOCK = 0x02; + const CBOR = 0x04; + const NMSG = 0x08; + } +} + +impl Capability { + pub fn has_fido1(self) -> bool { + !self.contains(Capability::NMSG) + } + + pub fn has_fido2(self) -> bool { + self.contains(Capability::CBOR) + } +} + +// Low-level error codes. Return as negatives. + +pub const ERR_NONE: u8 = 0x00; // No error +pub const ERR_INVALID_CMD: u8 = 0x01; // Invalid command +pub const ERR_INVALID_PAR: u8 = 0x02; // Invalid parameter +pub const ERR_INVALID_LEN: u8 = 0x03; // Invalid message length +pub const ERR_INVALID_SEQ: u8 = 0x04; // Invalid message sequencing +pub const ERR_MSG_TIMEOUT: u8 = 0x05; // Message has timed out +pub const ERR_CHANNEL_BUSY: u8 = 0x06; // Channel busy +pub const ERR_LOCK_REQUIRED: u8 = 0x0a; // Command requires channel lock +pub const ERR_INVALID_CID: u8 = 0x0b; // Command not allowed on this cid +pub const ERR_OTHER: u8 = 0x7f; // Other unspecified error + +// These are ISO 7816-4 defined response status words. +pub const SW_NO_ERROR: [u8; 2] = [0x90, 0x00]; +pub const SW_CONDITIONS_NOT_SATISFIED: [u8; 2] = [0x69, 0x85]; +pub const SW_WRONG_DATA: [u8; 2] = [0x6A, 0x80]; +pub const SW_WRONG_LENGTH: [u8; 2] = [0x67, 0x00]; diff --git a/third_party/rust/authenticator/src/crypto/dummy.rs b/third_party/rust/authenticator/src/crypto/dummy.rs new file mode 100644 index 0000000000..c37b2cbc21 --- /dev/null +++ b/third_party/rust/authenticator/src/crypto/dummy.rs @@ -0,0 +1,42 @@ +use super::{ByteBuf, COSEKey, ECDHSecret, ECDSACurve}; +use serde::Serialize; +/* +This is a dummy implementation for CI, to avoid having to install NSS or openSSL in the CI-pipeline +*/ + +pub type Result<T> = std::result::Result<T, BackendError>; + +#[derive(Clone, Debug, PartialEq, Serialize)] +pub enum BackendError {} + +pub(crate) fn serialize_key(_curve: ECDSACurve, key: &[u8]) -> Result<(ByteBuf, ByteBuf)> { + // Copy from NSS + let length = key[1..].len() / 2; + let chunks: Vec<_> = key[1..].chunks_exact(length).collect(); + Ok(( + ByteBuf::from(chunks[0].to_vec()), + ByteBuf::from(chunks[1].to_vec()), + )) +} + +pub(crate) fn encapsulate(_key: &COSEKey) -> Result<ECDHSecret> { + unimplemented!() +} + +pub(crate) fn encrypt( + _key: &[u8], + _plain_text: &[u8], /*PlainText*/ +) -> Result<Vec<u8> /*CypherText*/> { + unimplemented!() +} + +pub(crate) fn decrypt( + _key: &[u8], + _cypher_text: &[u8], /*CypherText*/ +) -> Result<Vec<u8> /*PlainText*/> { + unimplemented!() +} + +pub(crate) fn authenticate(_token: &[u8], _input: &[u8]) -> Result<Vec<u8>> { + unimplemented!() +} diff --git a/third_party/rust/authenticator/src/crypto/mod.rs b/third_party/rust/authenticator/src/crypto/mod.rs new file mode 100644 index 0000000000..627ab32088 --- /dev/null +++ b/third_party/rust/authenticator/src/crypto/mod.rs @@ -0,0 +1,924 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::errors::AuthenticatorError; +use crate::{ctap2::commands::CommandError, transport::errors::HIDError}; +use serde::{ + de::{Error as SerdeError, MapAccess, Unexpected, Visitor}, + ser::SerializeMap, + Deserialize, Deserializer, Serialize, Serializer, +}; +use serde_bytes::ByteBuf; +use serde_cbor::Value; +use std::convert::TryFrom; +use std::fmt; + +cfg_if::cfg_if! { + if #[cfg(feature = "crypto_ring")] { + #[path = "ring.rs"] + pub mod imp; + } else if #[cfg(feature = "crypto_openssl")] { + #[path = "openssl.rs"] + pub mod imp; + } else if #[cfg(feature = "crypto_dummy")] { + #[path = "dummy.rs"] + pub mod imp; + } else { + #[path = "nss.rs"] + pub mod imp; + } +} + +pub(crate) use imp::{authenticate, decrypt, encapsulate, encrypt, serialize_key, BackendError}; + +/// An ECDSACurve identifier. You probably will never need to alter +/// or use this value, as it is set inside the Credential for you. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum ECDSACurve { + // +---------+-------+----------+------------------------------------+ + // | Name | Value | Key Type | Description | + // +---------+-------+----------+------------------------------------+ + // | P-256 | 1 | EC2 | NIST P-256 also known as secp256r1 | + // | P-384 | 2 | EC2 | NIST P-384 also known as secp384r1 | + // | P-521 | 3 | EC2 | NIST P-521 also known as secp521r1 | + // | X25519 | 4 | OKP | X25519 for use w/ ECDH only | + // | X448 | 5 | OKP | X448 for use w/ ECDH only | + // | Ed25519 | 6 | OKP | Ed25519 for use w/ EdDSA only | + // | Ed448 | 7 | OKP | Ed448 for use w/ EdDSA only | + // +---------+-------+----------+------------------------------------+ + /// Identifies this curve as SECP256R1 (X9_62_PRIME256V1 in OpenSSL) + SECP256R1 = 1, + /// Identifies this curve as SECP384R1 + SECP384R1 = 2, + /// Identifies this curve as SECP521R1 + SECP521R1 = 3, + /// Identifieds this as OKP X25519 for use w/ ECDH only + X25519 = 4, + /// Identifieds this as OKP X448 for use w/ ECDH only + X448 = 5, + /// Identifieds this as OKP Ed25519 for use w/ EdDSA only + Ed25519 = 6, + /// Identifieds this as OKP Ed448 for use w/ EdDSA only + Ed448 = 7, +} + +impl TryFrom<u64> for ECDSACurve { + type Error = CryptoError; + fn try_from(i: u64) -> Result<Self, Self::Error> { + match i { + 1 => Ok(ECDSACurve::SECP256R1), + 2 => Ok(ECDSACurve::SECP384R1), + 3 => Ok(ECDSACurve::SECP521R1), + 4 => Ok(ECDSACurve::X25519), + 5 => Ok(ECDSACurve::X448), + 6 => Ok(ECDSACurve::Ed25519), + 7 => Ok(ECDSACurve::Ed448), + _ => Err(CryptoError::UnknownKeyType), + } + } +} +/// A COSE signature algorithm, indicating the type of key and hash type +/// that should be used. +/// see: https://www.iana.org/assignments/cose/cose.xhtml#table-algorithms +#[allow(non_camel_case_types)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum COSEAlgorithm { + // /// Identifies this key as ECDSA (recommended SECP256R1) with SHA256 hashing + // //#[serde(alias = "ECDSA_SHA256")] + // ES256 = -7, // recommends curve SECP256R1 + // /// Identifies this key as ECDSA (recommended SECP384R1) with SHA384 hashing + // //#[serde(alias = "ECDSA_SHA384")] + // ES384 = -35, // recommends curve SECP384R1 + // /// Identifies this key as ECDSA (recommended SECP521R1) with SHA512 hashing + // //#[serde(alias = "ECDSA_SHA512")] + // ES512 = -36, // recommends curve SECP521R1 + // /// Identifies this key as RS256 aka RSASSA-PKCS1-v1_5 w/ SHA-256 + // RS256 = -257, + // /// Identifies this key as RS384 aka RSASSA-PKCS1-v1_5 w/ SHA-384 + // RS384 = -258, + // /// Identifies this key as RS512 aka RSASSA-PKCS1-v1_5 w/ SHA-512 + // RS512 = -259, + // /// Identifies this key as PS256 aka RSASSA-PSS w/ SHA-256 + // PS256 = -37, + // /// Identifies this key as PS384 aka RSASSA-PSS w/ SHA-384 + // PS384 = -38, + // /// Identifies this key as PS512 aka RSASSA-PSS w/ SHA-512 + // PS512 = -39, + // /// Identifies this key as EdDSA (likely curve ed25519) + // EDDSA = -8, + // /// Identifies this as an INSECURE RS1 aka RSASSA-PKCS1-v1_5 using SHA-1. This is not + // /// used by validators, but can exist in some windows hello tpm's + // INSECURE_RS1 = -65535, + INSECURE_RS1 = -65535, // RSASSA-PKCS1-v1_5 using SHA-1 + RS512 = -259, // RSASSA-PKCS1-v1_5 using SHA-512 + RS384 = -258, // RSASSA-PKCS1-v1_5 using SHA-384 + RS256 = -257, // RSASSA-PKCS1-v1_5 using SHA-256 + ES256K = -47, // ECDSA using secp256k1 curve and SHA-256 + HSS_LMS = -46, // HSS/LMS hash-based digital signature + SHAKE256 = -45, // SHAKE-256 512-bit Hash Value + SHA512 = -44, // SHA-2 512-bit Hash + SHA384 = -43, // SHA-2 384-bit Hash + RSAES_OAEP_SHA_512 = -42, // RSAES-OAEP w/ SHA-512 + RSAES_OAEP_SHA_256 = -41, // RSAES-OAEP w/ SHA-256 + RSAES_OAEP_RFC_8017_default = -40, // RSAES-OAEP w/ SHA-1 + PS512 = -39, // RSASSA-PSS w/ SHA-512 + PS384 = -38, // RSASSA-PSS w/ SHA-384 + PS256 = -37, // RSASSA-PSS w/ SHA-256 + ES512 = -36, // ECDSA w/ SHA-512 + ES384 = -35, // ECDSA w/ SHA-384 + ECDH_SS_A256KW = -34, // ECDH SS w/ Concat KDF and AES Key Wrap w/ 256-bit key + ECDH_SS_A192KW = -33, // ECDH SS w/ Concat KDF and AES Key Wrap w/ 192-bit key + ECDH_SS_A128KW = -32, // ECDH SS w/ Concat KDF and AES Key Wrap w/ 128-bit key + ECDH_ES_A256KW = -31, // ECDH ES w/ Concat KDF and AES Key Wrap w/ 256-bit key + ECDH_ES_A192KW = -30, // ECDH ES w/ Concat KDF and AES Key Wrap w/ 192-bit key + ECDH_ES_A128KW = -29, // ECDH ES w/ Concat KDF and AES Key Wrap w/ 128-bit key + ECDH_SS_HKDF512 = -28, // ECDH SS w/ HKDF - generate key directly + ECDH_SS_HKDF256 = -27, // ECDH SS w/ HKDF - generate key directly + ECDH_ES_HKDF512 = -26, // ECDH ES w/ HKDF - generate key directly + ECDH_ES_HKDF256 = -25, // ECDH ES w/ HKDF - generate key directly + SHAKE128 = -18, // SHAKE-128 256-bit Hash Value + SHA512_256 = -17, // SHA-2 512-bit Hash truncated to 256-bits + SHA256 = -16, // SHA-2 256-bit Hash + SHA256_64 = -15, // SHA-2 256-bit Hash truncated to 64-bits + SHA1 = -14, // SHA-1 Hash + Direct_HKDF_AES256 = -13, // Shared secret w/ AES-MAC 256-bit key + Direct_HKDF_AES128 = -12, // Shared secret w/ AES-MAC 128-bit key + Direct_HKDF_SHA512 = -11, // Shared secret w/ HKDF and SHA-512 + Direct_HKDF_SHA256 = -10, // Shared secret w/ HKDF and SHA-256 + EDDSA = -8, // EdDSA + ES256 = -7, // ECDSA w/ SHA-256 + Direct = -6, // Direct use of CEK + A256KW = -5, // AES Key Wrap w/ 256-bit key + A192KW = -4, // AES Key Wrap w/ 192-bit key + A128KW = -3, // AES Key Wrap w/ 128-bit key + A128GCM = 1, // AES-GCM mode w/ 128-bit key, 128-bit tag + A192GCM = 2, // AES-GCM mode w/ 192-bit key, 128-bit tag + A256GCM = 3, // AES-GCM mode w/ 256-bit key, 128-bit tag + HMAC256_64 = 4, // HMAC w/ SHA-256 truncated to 64 bits + HMAC256_256 = 5, // HMAC w/ SHA-256 + HMAC384_384 = 6, // HMAC w/ SHA-384 + HMAC512_512 = 7, // HMAC w/ SHA-512 + AES_CCM_16_64_128 = 10, // AES-CCM mode 128-bit key, 64-bit tag, 13-byte nonce + AES_CCM_16_64_256 = 11, // AES-CCM mode 256-bit key, 64-bit tag, 13-byte nonce + AES_CCM_64_64_128 = 12, // AES-CCM mode 128-bit key, 64-bit tag, 7-byte nonce + AES_CCM_64_64_256 = 13, // AES-CCM mode 256-bit key, 64-bit tag, 7-byte nonce + AES_MAC_128_64 = 14, // AES-MAC 128-bit key, 64-bit tag + AES_MAC_256_64 = 15, // AES-MAC 256-bit key, 64-bit tag + ChaCha20_Poly1305 = 24, // ChaCha20/Poly1305 w/ 256-bit key, 128-bit tag + AES_MAC_128_128 = 25, // AES-MAC 128-bit key, 128-bit tag + AES_MAC_256_128 = 26, // AES-MAC 256-bit key, 128-bit tag + AES_CCM_16_128_128 = 30, // AES-CCM mode 128-bit key, 128-bit tag, 13-byte nonce + AES_CCM_16_128_256 = 31, // AES-CCM mode 256-bit key, 128-bit tag, 13-byte nonce + AES_CCM_64_128_128 = 32, // AES-CCM mode 128-bit key, 128-bit tag, 7-byte nonce + AES_CCM_64_128_256 = 33, // AES-CCM mode 256-bit key, 128-bit tag, 7-byte nonce + IV_GENERATION = 34, // For doing IV generation for symmetric algorithms. +} + +impl Serialize for COSEAlgorithm { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + match *self { + COSEAlgorithm::RS512 => serializer.serialize_i16(-259), + COSEAlgorithm::RS384 => serializer.serialize_i16(-258), + COSEAlgorithm::RS256 => serializer.serialize_i16(-257), + COSEAlgorithm::ES256K => serializer.serialize_i8(-47), + COSEAlgorithm::HSS_LMS => serializer.serialize_i8(-46), + COSEAlgorithm::SHAKE256 => serializer.serialize_i8(-45), + COSEAlgorithm::SHA512 => serializer.serialize_i8(-44), + COSEAlgorithm::SHA384 => serializer.serialize_i8(-43), + COSEAlgorithm::RSAES_OAEP_SHA_512 => serializer.serialize_i8(-42), + COSEAlgorithm::RSAES_OAEP_SHA_256 => serializer.serialize_i8(-41), + COSEAlgorithm::RSAES_OAEP_RFC_8017_default => serializer.serialize_i8(-40), + COSEAlgorithm::PS512 => serializer.serialize_i8(-39), + COSEAlgorithm::PS384 => serializer.serialize_i8(-38), + COSEAlgorithm::PS256 => serializer.serialize_i8(-37), + COSEAlgorithm::ES512 => serializer.serialize_i8(-36), + COSEAlgorithm::ES384 => serializer.serialize_i8(-35), + COSEAlgorithm::ECDH_SS_A256KW => serializer.serialize_i8(-34), + COSEAlgorithm::ECDH_SS_A192KW => serializer.serialize_i8(-33), + COSEAlgorithm::ECDH_SS_A128KW => serializer.serialize_i8(-32), + COSEAlgorithm::ECDH_ES_A256KW => serializer.serialize_i8(-31), + COSEAlgorithm::ECDH_ES_A192KW => serializer.serialize_i8(-30), + COSEAlgorithm::ECDH_ES_A128KW => serializer.serialize_i8(-29), + COSEAlgorithm::ECDH_SS_HKDF512 => serializer.serialize_i8(-28), + COSEAlgorithm::ECDH_SS_HKDF256 => serializer.serialize_i8(-27), + COSEAlgorithm::ECDH_ES_HKDF512 => serializer.serialize_i8(-26), + COSEAlgorithm::ECDH_ES_HKDF256 => serializer.serialize_i8(-25), + COSEAlgorithm::SHAKE128 => serializer.serialize_i8(-18), + COSEAlgorithm::SHA512_256 => serializer.serialize_i8(-17), + COSEAlgorithm::SHA256 => serializer.serialize_i8(-16), + COSEAlgorithm::SHA256_64 => serializer.serialize_i8(-15), + COSEAlgorithm::SHA1 => serializer.serialize_i8(-14), + COSEAlgorithm::Direct_HKDF_AES256 => serializer.serialize_i8(-13), + COSEAlgorithm::Direct_HKDF_AES128 => serializer.serialize_i8(-12), + COSEAlgorithm::Direct_HKDF_SHA512 => serializer.serialize_i8(-11), + COSEAlgorithm::Direct_HKDF_SHA256 => serializer.serialize_i8(-10), + COSEAlgorithm::EDDSA => serializer.serialize_i8(-8), + COSEAlgorithm::ES256 => serializer.serialize_i8(-7), + COSEAlgorithm::Direct => serializer.serialize_i8(-6), + COSEAlgorithm::A256KW => serializer.serialize_i8(-5), + COSEAlgorithm::A192KW => serializer.serialize_i8(-4), + COSEAlgorithm::A128KW => serializer.serialize_i8(-3), + COSEAlgorithm::A128GCM => serializer.serialize_i8(1), + COSEAlgorithm::A192GCM => serializer.serialize_i8(2), + COSEAlgorithm::A256GCM => serializer.serialize_i8(3), + COSEAlgorithm::HMAC256_64 => serializer.serialize_i8(4), + COSEAlgorithm::HMAC256_256 => serializer.serialize_i8(5), + COSEAlgorithm::HMAC384_384 => serializer.serialize_i8(6), + COSEAlgorithm::HMAC512_512 => serializer.serialize_i8(7), + COSEAlgorithm::AES_CCM_16_64_128 => serializer.serialize_i8(10), + COSEAlgorithm::AES_CCM_16_64_256 => serializer.serialize_i8(11), + COSEAlgorithm::AES_CCM_64_64_128 => serializer.serialize_i8(12), + COSEAlgorithm::AES_CCM_64_64_256 => serializer.serialize_i8(13), + COSEAlgorithm::AES_MAC_128_64 => serializer.serialize_i8(14), + COSEAlgorithm::AES_MAC_256_64 => serializer.serialize_i8(15), + COSEAlgorithm::ChaCha20_Poly1305 => serializer.serialize_i8(24), + COSEAlgorithm::AES_MAC_128_128 => serializer.serialize_i8(25), + COSEAlgorithm::AES_MAC_256_128 => serializer.serialize_i8(26), + COSEAlgorithm::AES_CCM_16_128_128 => serializer.serialize_i8(30), + COSEAlgorithm::AES_CCM_16_128_256 => serializer.serialize_i8(31), + COSEAlgorithm::AES_CCM_64_128_128 => serializer.serialize_i8(32), + COSEAlgorithm::AES_CCM_64_128_256 => serializer.serialize_i8(33), + COSEAlgorithm::IV_GENERATION => serializer.serialize_i8(34), + COSEAlgorithm::INSECURE_RS1 => serializer.serialize_i32(-65535), + } + } +} + +impl<'de> Deserialize<'de> for COSEAlgorithm { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + struct COSEAlgorithmVisitor; + + impl<'de> Visitor<'de> for COSEAlgorithmVisitor { + type Value = COSEAlgorithm; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a signed integer") + } + + fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E> + where + E: SerdeError, + { + COSEAlgorithm::try_from(v).map_err(|_| { + SerdeError::invalid_value(Unexpected::Signed(v), &"valid COSEAlgorithm") + }) + } + } + + deserializer.deserialize_any(COSEAlgorithmVisitor) + } +} + +impl TryFrom<i64> for COSEAlgorithm { + type Error = CryptoError; + fn try_from(i: i64) -> Result<Self, Self::Error> { + match i { + -259 => Ok(COSEAlgorithm::RS512), + -258 => Ok(COSEAlgorithm::RS384), + -257 => Ok(COSEAlgorithm::RS256), + -47 => Ok(COSEAlgorithm::ES256K), + -46 => Ok(COSEAlgorithm::HSS_LMS), + -45 => Ok(COSEAlgorithm::SHAKE256), + -44 => Ok(COSEAlgorithm::SHA512), + -43 => Ok(COSEAlgorithm::SHA384), + -42 => Ok(COSEAlgorithm::RSAES_OAEP_SHA_512), + -41 => Ok(COSEAlgorithm::RSAES_OAEP_SHA_256), + -40 => Ok(COSEAlgorithm::RSAES_OAEP_RFC_8017_default), + -39 => Ok(COSEAlgorithm::PS512), + -38 => Ok(COSEAlgorithm::PS384), + -37 => Ok(COSEAlgorithm::PS256), + -36 => Ok(COSEAlgorithm::ES512), + -35 => Ok(COSEAlgorithm::ES384), + -34 => Ok(COSEAlgorithm::ECDH_SS_A256KW), + -33 => Ok(COSEAlgorithm::ECDH_SS_A192KW), + -32 => Ok(COSEAlgorithm::ECDH_SS_A128KW), + -31 => Ok(COSEAlgorithm::ECDH_ES_A256KW), + -30 => Ok(COSEAlgorithm::ECDH_ES_A192KW), + -29 => Ok(COSEAlgorithm::ECDH_ES_A128KW), + -28 => Ok(COSEAlgorithm::ECDH_SS_HKDF512), + -27 => Ok(COSEAlgorithm::ECDH_SS_HKDF256), + -26 => Ok(COSEAlgorithm::ECDH_ES_HKDF512), + -25 => Ok(COSEAlgorithm::ECDH_ES_HKDF256), + -18 => Ok(COSEAlgorithm::SHAKE128), + -17 => Ok(COSEAlgorithm::SHA512_256), + -16 => Ok(COSEAlgorithm::SHA256), + -15 => Ok(COSEAlgorithm::SHA256_64), + -14 => Ok(COSEAlgorithm::SHA1), + -13 => Ok(COSEAlgorithm::Direct_HKDF_AES256), + -12 => Ok(COSEAlgorithm::Direct_HKDF_AES128), + -11 => Ok(COSEAlgorithm::Direct_HKDF_SHA512), + -10 => Ok(COSEAlgorithm::Direct_HKDF_SHA256), + -8 => Ok(COSEAlgorithm::EDDSA), + -7 => Ok(COSEAlgorithm::ES256), + -6 => Ok(COSEAlgorithm::Direct), + -5 => Ok(COSEAlgorithm::A256KW), + -4 => Ok(COSEAlgorithm::A192KW), + -3 => Ok(COSEAlgorithm::A128KW), + 1 => Ok(COSEAlgorithm::A128GCM), + 2 => Ok(COSEAlgorithm::A192GCM), + 3 => Ok(COSEAlgorithm::A256GCM), + 4 => Ok(COSEAlgorithm::HMAC256_64), + 5 => Ok(COSEAlgorithm::HMAC256_256), + 6 => Ok(COSEAlgorithm::HMAC384_384), + 7 => Ok(COSEAlgorithm::HMAC512_512), + 10 => Ok(COSEAlgorithm::AES_CCM_16_64_128), + 11 => Ok(COSEAlgorithm::AES_CCM_16_64_256), + 12 => Ok(COSEAlgorithm::AES_CCM_64_64_128), + 13 => Ok(COSEAlgorithm::AES_CCM_64_64_256), + 14 => Ok(COSEAlgorithm::AES_MAC_128_64), + 15 => Ok(COSEAlgorithm::AES_MAC_256_64), + 24 => Ok(COSEAlgorithm::ChaCha20_Poly1305), + 25 => Ok(COSEAlgorithm::AES_MAC_128_128), + 26 => Ok(COSEAlgorithm::AES_MAC_256_128), + 30 => Ok(COSEAlgorithm::AES_CCM_16_128_128), + 31 => Ok(COSEAlgorithm::AES_CCM_16_128_256), + 32 => Ok(COSEAlgorithm::AES_CCM_64_128_128), + 33 => Ok(COSEAlgorithm::AES_CCM_64_128_256), + 34 => Ok(COSEAlgorithm::IV_GENERATION), + -65535 => Ok(COSEAlgorithm::INSECURE_RS1), + _ => Err(CryptoError::UnknownAlgorithm), + } + } +} +/// A COSE Elliptic Curve Public Key. This is generally the provided credential +/// that an authenticator registers, and is used to authenticate the user. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct COSEEC2Key { + /// The curve that this key references. + pub curve: ECDSACurve, + /// The key's public X coordinate. + pub x: Vec<u8>, + /// The key's public Y coordinate. + pub y: Vec<u8>, +} + +/// A Octet Key Pair (OKP). +/// The other version uses only the x-coordinate as the y-coordinate is +/// either to be recomputed or not needed for the key agreement operation ('OKP'). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct COSEOKPKey { + /// The curve that this key references. + pub curve: ECDSACurve, + /// The key's public X coordinate. + pub x: Vec<u8>, +} + +/// A COSE RSA PublicKey. This is a provided credential from a registered +/// authenticator. +/// You will likely never need to interact with this value, as it is part of the Credential +/// API. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct COSERSAKey { + /// An RSA modulus + pub n: Vec<u8>, + /// An RSA exponent + pub e: Vec<u8>, +} + +/// A Octet Key Pair (OKP). +/// The other version uses only the x-coordinate as the y-coordinate is +/// either to be recomputed or not needed for the key agreement operation ('OKP'). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct COSESymmetricKey { + /// The key + pub key: Vec<u8>, +} + +// https://tools.ietf.org/html/rfc8152#section-13 +#[allow(non_camel_case_types)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[repr(i64)] +pub enum COSEKeyTypeId { + // Reserved is invalid + // Reserved = 0, + /// Octet Key Pair + OKP = 1, + /// Elliptic Curve Keys w/ x- and y-coordinate + EC2 = 2, + /// RSA + RSA = 3, + /// Symmetric + Symmetric = 4, +} + +impl TryFrom<u64> for COSEKeyTypeId { + type Error = CryptoError; + fn try_from(i: u64) -> Result<Self, Self::Error> { + match i { + 1 => Ok(COSEKeyTypeId::OKP), + 2 => Ok(COSEKeyTypeId::EC2), + 3 => Ok(COSEKeyTypeId::RSA), + 4 => Ok(COSEKeyTypeId::Symmetric), + _ => Err(CryptoError::UnknownKeyType), + } + } +} + +/// The type of Key contained within a COSE value. You should never need +/// to alter or change this type. +#[allow(non_camel_case_types)] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum COSEKeyType { + // +-----------+-------+-----------------------------------------------+ + // | Name | Value | Description | + // +-----------+-------+-----------------------------------------------+ + // | OKP | 1 | Octet Key Pair | + // | EC2 | 2 | Elliptic Curve Keys w/ x- and y-coordinate | + // | | | pair | + // | Symmetric | 4 | Symmetric Keys | + // | Reserved | 0 | This value is reserved | + // +-----------+-------+-----------------------------------------------+ + // Reserved, // should always be invalid. + /// Identifies this as an Elliptic Curve octet key pair + OKP(COSEOKPKey), // Not used here + /// Identifies this as an Elliptic Curve EC2 key + EC2(COSEEC2Key), + /// Identifies this as an RSA key + RSA(COSERSAKey), // Not used here + /// Identifies this as a Symmetric key + Symmetric(COSESymmetricKey), // Not used here +} + +/// A COSE Key as provided by the Authenticator. You should never need +/// to alter or change these values. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct COSEKey { + /// COSE signature algorithm, indicating the type of key and hash type + /// that should be used. + pub alg: COSEAlgorithm, + /// The public key + pub key: COSEKeyType, +} + +impl<'de> Deserialize<'de> for COSEKey { + fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error> + where + D: Deserializer<'de>, + { + struct COSEKeyVisitor; + + impl<'de> Visitor<'de> for COSEKeyVisitor { + type Value = COSEKey; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map") + } + + fn visit_map<M>(self, mut map: M) -> std::result::Result<Self::Value, M::Error> + where + M: MapAccess<'de>, + { + let mut curve: Option<ECDSACurve> = None; + let mut key_type: Option<COSEKeyTypeId> = None; + let mut alg: Option<COSEAlgorithm> = None; + let mut x: Option<Vec<u8>> = None; + let mut y: Option<Vec<u8>> = None; + + while let Some(key) = map.next_key()? { + trace!("cose key {:?}", key); + match key { + 1 => { + if key_type.is_some() { + return Err(SerdeError::duplicate_field("key_type")); + } + let value: u64 = map.next_value()?; + let val = COSEKeyTypeId::try_from(value).map_err(|_| { + SerdeError::custom(format!("unsupported key_type {}", value)) + })?; + key_type = Some(val); + // key_type = Some(map.next_value()?); + } + -1 => { + let key_type = key_type.ok_or(SerdeError::missing_field("key_type"))?; + if key_type == COSEKeyTypeId::RSA { + if y.is_some() { + return Err(SerdeError::duplicate_field("y")); + } + let value: ByteBuf = map.next_value()?; + y = Some(value.to_vec()); + } else { + if curve.is_some() { + return Err(SerdeError::duplicate_field("curve")); + } + let value: u64 = map.next_value()?; + let val = ECDSACurve::try_from(value).map_err(|_| { + SerdeError::custom(format!("unsupported curve {}", value)) + })?; + curve = Some(val); + // curve = Some(map.next_value()?); + } + } + -2 => { + if x.is_some() { + return Err(SerdeError::duplicate_field("x")); + } + let value: ByteBuf = map.next_value()?; + x = Some(value.to_vec()); + } + -3 => { + if y.is_some() { + return Err(SerdeError::duplicate_field("y")); + } + let value: ByteBuf = map.next_value()?; + y = Some(value.to_vec()); + } + 3 => { + if alg.is_some() { + return Err(SerdeError::duplicate_field("alg")); + } + let value: i64 = map.next_value()?; + let val = COSEAlgorithm::try_from(value).map_err(|_| { + SerdeError::custom(format!("unsupported algorithm {}", value)) + })?; + alg = Some(val); + // alg = map.next_value()?; + } + _ => { + // This unknown field should raise an error, but + // there is a couple of field I(baloo) do not understand + // yet. I(baloo) chose to ignore silently the + // error instead because of that + let value: Value = map.next_value()?; + trace!("cose unknown value {:?}:{:?}", key, value); + } + }; + } + + let key_type = key_type.ok_or(SerdeError::missing_field("key_type"))?; + let x = x.ok_or(SerdeError::missing_field("x"))?; + let alg = alg.ok_or(SerdeError::missing_field("alg"))?; + + let res = match key_type { + COSEKeyTypeId::OKP => { + let curve = curve.ok_or(SerdeError::missing_field("curve"))?; + COSEKeyType::OKP(COSEOKPKey { curve, x }) + } + COSEKeyTypeId::EC2 => { + let curve = curve.ok_or(SerdeError::missing_field("curve"))?; + let y = y.ok_or(SerdeError::missing_field("y"))?; + COSEKeyType::EC2(COSEEC2Key { curve, x, y }) + } + COSEKeyTypeId::RSA => { + let e = y.ok_or(SerdeError::missing_field("y"))?; + COSEKeyType::RSA(COSERSAKey { e, n: x }) + } + COSEKeyTypeId::Symmetric => COSEKeyType::Symmetric(COSESymmetricKey { key: x }), + }; + Ok(COSEKey { alg, key: res }) + } + } + + deserializer.deserialize_bytes(COSEKeyVisitor) + } +} + +impl Serialize for COSEKey { + fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> + where + S: Serializer, + { + let map_len = match &self.key { + COSEKeyType::OKP(_) => 3, + COSEKeyType::EC2(_) => 5, + COSEKeyType::RSA(_) => 4, + COSEKeyType::Symmetric(_) => 3, + }; + let mut map = serializer.serialize_map(Some(map_len))?; + match &self.key { + COSEKeyType::OKP(key) => { + map.serialize_entry(&1, &COSEKeyTypeId::OKP)?; + map.serialize_entry(&3, &self.alg)?; + map.serialize_entry(&-1, &key.curve)?; + map.serialize_entry(&-2, &key.x)?; + } + COSEKeyType::EC2(key) => { + map.serialize_entry(&1, &(COSEKeyTypeId::EC2 as u8))?; + map.serialize_entry(&3, &self.alg)?; + map.serialize_entry(&-1, &(key.curve as u8))?; + map.serialize_entry(&-2, &serde_bytes::Bytes::new(&key.x))?; + map.serialize_entry(&-3, &serde_bytes::Bytes::new(&key.y))?; + } + COSEKeyType::RSA(key) => { + map.serialize_entry(&1, &COSEKeyTypeId::RSA)?; + map.serialize_entry(&3, &self.alg)?; + map.serialize_entry(&-1, &key.n)?; + map.serialize_entry(&-2, &key.e)?; + } + COSEKeyType::Symmetric(key) => { + map.serialize_entry(&1, &COSEKeyTypeId::Symmetric)?; + map.serialize_entry(&3, &self.alg)?; + map.serialize_entry(&-1, &key.key)?; + } + } + + map.end() + } +} + +/// Errors that can be returned from COSE functions. +#[derive(Debug)] +pub enum CryptoError { + // DecodingFailure, + // LibraryFailure, + MalformedInput, + // MissingHeader, + // UnexpectedHeaderValue, + // UnexpectedTag, + // UnexpectedType, + // Unimplemented, + // VerificationFailed, + // SigningFailed, + // InvalidArgument, + UnknownKeyType, + UnknownSignatureScheme, + UnknownAlgorithm, + WrongSaltLength, + Backend(BackendError), +} + +impl From<BackendError> for CryptoError { + fn from(e: BackendError) -> Self { + CryptoError::Backend(e) + } +} + +impl From<CryptoError> for CommandError { + fn from(e: CryptoError) -> Self { + CommandError::Crypto(e) + } +} + +impl From<CryptoError> for AuthenticatorError { + fn from(e: CryptoError) -> Self { + AuthenticatorError::HIDError(HIDError::Command(CommandError::Crypto(e))) + } +} + +#[derive(Clone)] +pub struct ECDHSecret { + remote: COSEKey, + my: COSEKey, + shared_secret: Vec<u8>, +} + +impl ECDHSecret { + pub fn my_public_key(&self) -> &COSEKey { + &self.my + } + + pub fn shared_secret(&self) -> &[u8] { + &self.shared_secret + } +} + +impl fmt::Debug for ECDHSecret { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "ECDHSecret(remote: {:?}, my: {:?})", + self.remote, + self.my_public_key() + ) + } +} + +pub struct U2FRegisterAnswer<'a> { + pub certificate: &'a [u8], + pub signature: &'a [u8], +} + +// We will only return MalformedInput here +pub fn parse_u2f_der_certificate(data: &[u8]) -> Result<U2FRegisterAnswer, CryptoError> { + // So we don't panic below, when accessing individual bytes + if data.len() < 4 { + return Err(CryptoError::MalformedInput); + } + // Check if it is a SEQUENCE + if data[0] != 0x30 { + return Err(CryptoError::MalformedInput); + } + + // This algorithm is taken from mozilla-central/security/nss/lib/mozpkix/lib/pkixder.cpp + // The short form of length is a single byte with the high order bit set + // to zero. The long form of length is one byte with the high order bit + // set, followed by N bytes, where N is encoded in the lowest 7 bits of + // the first byte. + let end = if (data[1] & 0x80) == 0 { + 2 + data[1] as usize + } else if data[1] == 0x81 { + // The next byte specifies the length + + if data[2] < 128 { + // Not shortest possible encoding + // Forbidden by DER-format + return Err(CryptoError::MalformedInput); + } + 3 + data[2] as usize + } else if data[1] == 0x82 { + // The next 2 bytes specify the length + let l = u16::from_be_bytes([data[2], data[3]]); + if l < 256 { + // Not shortest possible encoding + // Forbidden by DER-format + return Err(CryptoError::MalformedInput); + } + 4 + l as usize + } else { + // We don't support lengths larger than 2^16 - 1. + return Err(CryptoError::MalformedInput); + }; + + if data.len() < end { + return Err(CryptoError::MalformedInput); + } + + Ok(U2FRegisterAnswer { + certificate: &data[0..end], + signature: &data[end..], + }) +} + +#[cfg(all(test, not(feature = "crypto_dummy")))] +mod test { + use super::{ + authenticate, decrypt, encrypt, imp::parse_key, imp::test_encapsulate, serialize_key, + COSEAlgorithm, COSEKey, ECDSACurve, + }; + use crate::crypto::{COSEEC2Key, COSEKeyType}; + use crate::ctap2::commands::client_pin::Pin; + use crate::util::decode_hex; + use serde_cbor::de::from_slice; + + #[test] + fn test_serialize_key() { + let x = [ + 0xfc, 0x9e, 0xd3, 0x6f, 0x7c, 0x1a, 0xa9, 0x15, 0xce, 0x3e, 0xa1, 0x77, 0xf0, 0x75, + 0x67, 0xf0, 0x7f, 0x16, 0xf9, 0x47, 0x9d, 0x95, 0xad, 0x8e, 0xd4, 0x97, 0x1d, 0x33, + 0x05, 0xe3, 0x1a, 0x80, + ]; + let y = [ + 0x50, 0xb7, 0x33, 0xaf, 0x8c, 0x0b, 0x0e, 0xe1, 0xda, 0x8d, 0xe0, 0xac, 0xf9, 0xd8, + 0xe1, 0x32, 0x82, 0xf0, 0x63, 0xb7, 0xb3, 0x0d, 0x73, 0xd4, 0xd3, 0x2c, 0x9a, 0xad, + 0x6d, 0xfa, 0x8b, 0x27, + ]; + let serialized_key = [ + 0x04, 0xfc, 0x9e, 0xd3, 0x6f, 0x7c, 0x1a, 0xa9, 0x15, 0xce, 0x3e, 0xa1, 0x77, 0xf0, + 0x75, 0x67, 0xf0, 0x7f, 0x16, 0xf9, 0x47, 0x9d, 0x95, 0xad, 0x8e, 0xd4, 0x97, 0x1d, + 0x33, 0x05, 0xe3, 0x1a, 0x80, 0x50, 0xb7, 0x33, 0xaf, 0x8c, 0x0b, 0x0e, 0xe1, 0xda, + 0x8d, 0xe0, 0xac, 0xf9, 0xd8, 0xe1, 0x32, 0x82, 0xf0, 0x63, 0xb7, 0xb3, 0x0d, 0x73, + 0xd4, 0xd3, 0x2c, 0x9a, 0xad, 0x6d, 0xfa, 0x8b, 0x27, + ]; + + let (res_x, res_y) = + serialize_key(ECDSACurve::SECP256R1, &serialized_key).expect("Failed to serialize key"); + assert_eq!(res_x, x); + assert_eq!(res_y, y); + + let res_key = parse_key(ECDSACurve::SECP256R1, &x, &y).expect("Failed to parse key"); + + assert_eq!(res_key, serialized_key) + } + + #[test] + fn test_parse_es256_serialize_key() { + // Test values taken from https://github.com/Yubico/python-fido2/blob/master/test/test_cose.py + let key_data = decode_hex("A5010203262001215820A5FD5CE1B1C458C530A54FA61B31BF6B04BE8B97AFDE54DD8CBB69275A8A1BE1225820FA3A3231DD9DEED9D1897BE5A6228C59501E4BCD12975D3DFF730F01278EA61C"); + let key: COSEKey = from_slice(&key_data).unwrap(); + assert_eq!(key.alg, COSEAlgorithm::ES256); + if let COSEKeyType::EC2(ec2key) = &key.key { + assert_eq!(ec2key.curve, ECDSACurve::SECP256R1); + assert_eq!( + ec2key.x, + decode_hex("A5FD5CE1B1C458C530A54FA61B31BF6B04BE8B97AFDE54DD8CBB69275A8A1BE1") + ); + assert_eq!( + ec2key.y, + decode_hex("FA3A3231DD9DEED9D1897BE5A6228C59501E4BCD12975D3DFF730F01278EA61C") + ); + } else { + panic!("Wrong key type!"); + } + + let serialized = serde_cbor::to_vec(&key).expect("Failed to serialize key"); + assert_eq!(key_data, serialized); + } + + #[test] + #[allow(non_snake_case)] + fn test_shared_secret() { + // Test values taken from https://github.com/Yubico/python-fido2/blob/master/test/test_ctap2.py + let EC_PRIV = + decode_hex("7452E599FEE739D8A653F6A507343D12D382249108A651402520B72F24FE7684"); + let EC_PUB_X = + decode_hex("44D78D7989B97E62EA993496C9EF6E8FD58B8B00715F9A89153DDD9C4657E47F"); + let EC_PUB_Y = + decode_hex("EC802EE7D22BD4E100F12E48537EB4E7E96ED3A47A0A3BD5F5EEAB65001664F9"); + let DEV_PUB_X = + decode_hex("0501D5BC78DA9252560A26CB08FCC60CBE0B6D3B8E1D1FCEE514FAC0AF675168"); + let DEV_PUB_Y = + decode_hex("D551B3ED46F665731F95B4532939C25D91DB7EB844BD96D4ABD4083785F8DF47"); + let SHARED = decode_hex("c42a039d548100dfba521e487debcbbb8b66bb7496f8b1862a7a395ed83e1a1c"); + let TOKEN_ENC = decode_hex("7A9F98E31B77BE90F9C64D12E9635040"); + let TOKEN = decode_hex("aff12c6dcfbf9df52f7a09211e8865cd"); + let PIN_HASH_ENC = decode_hex("afe8327ce416da8ee3d057589c2ce1a9"); + + // let peer_key = parse_key(ECDSACurve::SECP256R1, &DEV_PUB_X, &DEV_PUB_Y).unwrap(); + let my_pub_key_data = parse_key(ECDSACurve::SECP256R1, &EC_PUB_X, &EC_PUB_Y).unwrap(); + + let peer_key = COSEEC2Key { + curve: ECDSACurve::SECP256R1, + x: DEV_PUB_X, + y: DEV_PUB_Y, + }; + + // let my_pub_key = COSEKey { + // alg: COSEAlgorithm::ES256, + // key: COSEKeyType::EC2(COSEEC2Key { + // curve: ECDSACurve::SECP256R1, + // x: EC_PUB_X, + // y: EC_PUB_Y, + // }), + // }; + // We are using `test_encapsulate()` here, because we need a way to hand in the private key + // which would be generated on the fly otherwise (ephemeral keys), to predict the outputs + let shared_secret = + test_encapsulate(&peer_key, COSEAlgorithm::ES256, &my_pub_key_data, &EC_PRIV).unwrap(); + assert_eq!(shared_secret.shared_secret, SHARED); + + let token_enc = encrypt(&shared_secret.shared_secret(), &TOKEN).unwrap(); + assert_eq!(token_enc, TOKEN_ENC); + + let token = decrypt(&shared_secret.shared_secret(), &TOKEN_ENC).unwrap(); + assert_eq!(token, TOKEN); + + let pin = Pin::new("1234"); + let pin_hash_enc = + encrypt(&shared_secret.shared_secret(), pin.for_pin_token().as_ref()).unwrap(); + assert_eq!(pin_hash_enc, PIN_HASH_ENC); + } + + #[test] + fn test_authenticate() { + let key = "key"; + let message = "The quick brown fox jumps over the lazy dog"; + let expected = + decode_hex("f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8"); + + let result = + authenticate(key.as_bytes(), message.as_bytes()).expect("Failed to authenticate"); + assert_eq!(result, expected); + + let key = "The quick brown fox jumps over the lazy dogThe quick brown fox jumps over the lazy dog"; + let message = "message"; + let expected = + decode_hex("5597b93a2843078cbb0c920ae41dfe20f1685e10c67e423c11ab91adfc319d12"); + + let result = + authenticate(key.as_bytes(), message.as_bytes()).expect("Failed to authenticate"); + assert_eq!(result, expected); + } + + #[test] + fn test_pin_encryption_and_hashing() { + let pin = "1234"; + + let shared_secret = vec![ + 0x82, 0xE3, 0xD8, 0x41, 0xE2, 0x5C, 0x5C, 0x13, 0x46, 0x2C, 0x12, 0x3C, 0xC3, 0xD3, + 0x98, 0x78, 0x65, 0xBA, 0x3D, 0x20, 0x46, 0x74, 0xFB, 0xED, 0xD4, 0x7E, 0xF5, 0xAB, + 0xAB, 0x8D, 0x13, 0x72, + ]; + let expected_new_pin_enc = vec![ + 0x70, 0x66, 0x4B, 0xB5, 0x81, 0xE2, 0x57, 0x45, 0x1A, 0x3A, 0xB9, 0x1B, 0xF1, 0xAA, + 0xD8, 0xE4, 0x5F, 0x6C, 0xE9, 0xB5, 0xC3, 0xB0, 0xF3, 0x2B, 0x5E, 0xCD, 0x62, 0xD0, + 0xBA, 0x3B, 0x60, 0x5F, 0xD9, 0x18, 0x31, 0x66, 0xF6, 0xC5, 0xFA, 0xF3, 0xE4, 0xDA, + 0x24, 0x81, 0x50, 0x2C, 0xD0, 0xCE, 0xE0, 0x15, 0x8B, 0x35, 0x1F, 0xC3, 0x92, 0x08, + 0xA7, 0x7C, 0xB2, 0x74, 0x4B, 0xD4, 0x3C, 0xF9, + ]; + let expected_pin_auth = vec![ + 0x8E, 0x7F, 0x01, 0x69, 0x97, 0xF3, 0xB0, 0xA2, 0x7B, 0xA4, 0x34, 0x7A, 0x0E, 0x49, + 0xFD, 0xF5, + ]; + + // Padding to 64 bytes + let input: Vec<u8> = pin + .as_bytes() + .iter() + .chain(std::iter::repeat(&0x00)) + .take(64) + .cloned() + .collect(); + + let new_pin_enc = encrypt(&shared_secret, &input).expect("Failed to encrypt pin"); + assert_eq!(new_pin_enc, expected_new_pin_enc); + + let pin_auth = authenticate(&shared_secret, &new_pin_enc).expect("Failed to authenticate"); + assert_eq!(pin_auth[0..16], expected_pin_auth); + } +} diff --git a/third_party/rust/authenticator/src/crypto/nss.rs b/third_party/rust/authenticator/src/crypto/nss.rs new file mode 100644 index 0000000000..c469fd7d06 --- /dev/null +++ b/third_party/rust/authenticator/src/crypto/nss.rs @@ -0,0 +1,543 @@ +use super::{COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, ECDHSecret, ECDSACurve}; +use nss_gk_api::p11::{ + PK11Origin, PK11_CreateContextBySymKey, PK11_Decrypt, PK11_DigestFinal, PK11_DigestOp, + PK11_Encrypt, PK11_GenerateKeyPairWithOpFlags, PK11_HashBuf, PK11_ImportSymKey, + PK11_PubDeriveWithKDF, PrivateKey, PublicKey, SECKEY_DecodeDERSubjectPublicKeyInfo, + SECKEY_ExtractPublicKey, SECOidTag, Slot, SubjectPublicKeyInfo, AES_BLOCK_SIZE, + PK11_ATTR_SESSION, SHA256_LENGTH, +}; +use nss_gk_api::{Error as NSSError, IntoResult, SECItem, SECItemBorrowed, PR_FALSE}; +use pkcs11_bindings::{ + CKA_DERIVE, CKA_ENCRYPT, CKA_SIGN, CKD_NULL, CKF_DERIVE, CKM_AES_CBC, CKM_ECDH1_DERIVE, + CKM_EC_KEY_PAIR_GEN, CKM_SHA256_HMAC, CKM_SHA512_HMAC, +}; +use serde::Serialize; +use serde_bytes::ByteBuf; +use std::convert::{TryFrom, TryInto}; +use std::num::TryFromIntError; +use std::os::raw::c_uint; +use std::ptr; + +#[cfg(test)] +use nss_gk_api::p11::{PK11_ImportDERPrivateKeyInfoAndReturnKey, SECKEY_ConvertToPublicKey}; + +/// Errors that can be returned from COSE functions. +#[derive(Clone, Debug, Serialize)] +pub enum BackendError { + NSSError(String), + TryFromError, + UnsupportedAlgorithm(COSEAlgorithm), + UnsupportedCurve(ECDSACurve), + UnsupportedKeyType, +} + +impl From<NSSError> for BackendError { + fn from(e: NSSError) -> Self { + BackendError::NSSError(format!("{}", e)) + } +} + +impl From<TryFromIntError> for BackendError { + fn from(_: TryFromIntError) -> Self { + BackendError::TryFromError + } +} + +pub type Result<T> = std::result::Result<T, BackendError>; + +// Object identifiers in DER tag-length-value form + +const DER_OID_EC_PUBLIC_KEY_BYTES: &[u8] = &[ + 0x06, 0x07, + /* {iso(1) member-body(2) us(840) ansi-x962(10045) keyType(2) ecPublicKey(1)} */ + 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, +]; + +const DER_OID_P256_BYTES: &[u8] = &[ + 0x06, 0x08, + /* {iso(1) member-body(2) us(840) ansi-x962(10045) curves(3) prime(1) prime256v1(7)} */ + 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, +]; +const DER_OID_P384_BYTES: &[u8] = &[ + 0x06, 0x05, + /* {iso(1) identified-organization(3) certicom(132) curve(0) ansip384r1(34)} */ + 0x2b, 0x81, 0x04, 0x00, 0x22, +]; +const DER_OID_P521_BYTES: &[u8] = &[ + 0x06, 0x05, + /* {iso(1) identified-organization(3) certicom(132) curve(0) ansip521r1(35)} */ + 0x2b, 0x81, 0x04, 0x00, 0x23, +]; + +/* From CTAP2.1 spec: + +initialize() + + This is run by the platform when starting a series of transactions with a specific authenticator. +encapsulate(peerCoseKey) → (coseKey, sharedSecret) | error + + Generates an encapsulation for the authenticator’s public key and returns the message to transmit and the shared secret. +encrypt(key, demPlaintext) → ciphertext + + Encrypts a plaintext to produce a ciphertext, which may be longer than the plaintext. The plaintext is restricted to being a multiple of the AES block size (16 bytes) in length. +decrypt(key, ciphertext) → plaintext | error + + Decrypts a ciphertext and returns the plaintext. +authenticate(key, message) → signature + + Computes a MAC of the given message. +*/ + +// TODO(MS): Maybe remove ByteBuf and return Vec<u8>'s instead for a cleaner interface +pub(crate) fn serialize_key(_curve: ECDSACurve, key: &[u8]) -> Result<(ByteBuf, ByteBuf)> { + // TODO(MS): I actually have NO idea how to do this with NSS + let length = key[1..].len() / 2; + let chunks: Vec<_> = key[1..].chunks_exact(length).collect(); + Ok(( + ByteBuf::from(chunks[0].to_vec()), + ByteBuf::from(chunks[1].to_vec()), + )) +} + +pub(crate) fn parse_key(_curve: ECDSACurve, x: &[u8], y: &[u8]) -> Result<Vec<u8>> { + if x.len() != y.len() { + return Err(BackendError::NSSError( + "EC coordinates not equally long".to_string(), + )); + } + let mut buf = Vec::with_capacity(2 * x.len() + 1); + // The uncompressed point format is defined in Section 2.3.3 of "SEC 1: Elliptic Curve + // Cryptography" https://www.secg.org/sec1-v2.pdf. + buf.push(0x04); + buf.extend_from_slice(x); + buf.extend_from_slice(y); + Ok(buf) +} + +fn der_spki_from_cose(cose_key: &COSEKey) -> Result<Vec<u8>> { + let ec2key = match cose_key.key { + COSEKeyType::EC2(ref ec2key) => ec2key, + _ => return Err(BackendError::UnsupportedKeyType), + }; + + let (curve_oid, seq_len, alg_len, spk_len) = match ec2key.curve { + ECDSACurve::SECP256R1 => ( + DER_OID_P256_BYTES, + [0x59].as_slice(), + [0x13].as_slice(), + [0x42].as_slice(), + ), + ECDSACurve::SECP384R1 => ( + DER_OID_P384_BYTES, + [0x76].as_slice(), + [0x10].as_slice(), + [0x62].as_slice(), + ), + ECDSACurve::SECP521R1 => ( + DER_OID_P521_BYTES, + [0x81, 0x9b].as_slice(), + [0x10].as_slice(), + [0x8a, 0xdf].as_slice(), + ), + x => return Err(BackendError::UnsupportedCurve(x)), + }; + + let cose_key_sec1 = parse_key(ec2key.curve, &ec2key.x, &ec2key.y)?; + + // [RFC 5280] + let mut spki: Vec<u8> = vec![]; + // SubjectPublicKeyInfo + spki.push(0x30); + spki.extend_from_slice(seq_len); + // AlgorithmIdentifier + spki.push(0x30); + spki.extend_from_slice(alg_len); + // ObjectIdentifier + spki.extend_from_slice(DER_OID_EC_PUBLIC_KEY_BYTES); + // RFC 5480 ECParameters + spki.extend_from_slice(curve_oid); + // BIT STRING encoding uncompressed SEC1 public point + spki.push(0x03); + spki.extend_from_slice(spk_len); + spki.push(0x0); // no trailing zeros + spki.extend_from_slice(&cose_key_sec1); + + Ok(spki) +} + +/// This is run by the platform when starting a series of transactions with a specific authenticator. +//pub(crate) fn initialize() { } + +/// Generates an encapsulation for the authenticator's public key and returns the message +/// to transmit and the shared secret. +/// +/// `peer_cose_key` is the authenticator's (peer's) public key. +pub(crate) fn encapsulate(peer_cose_key: &COSEKey) -> Result<ECDHSecret> { + nss_gk_api::init(); + // Generate an ephmeral keypair to do ECDH with the authenticator. + // This is "platformKeyAgreementKey". + let ec2key = match peer_cose_key.key { + COSEKeyType::EC2(ref ec2key) => ec2key, + _ => return Err(BackendError::UnsupportedKeyType), + }; + + let mut oid = match ec2key.curve { + ECDSACurve::SECP256R1 => SECItemBorrowed::wrap(DER_OID_P256_BYTES), + ECDSACurve::SECP384R1 => SECItemBorrowed::wrap(DER_OID_P384_BYTES), + ECDSACurve::SECP521R1 => SECItemBorrowed::wrap(DER_OID_P521_BYTES), + x => return Err(BackendError::UnsupportedCurve(x)), + }; + let oid_ptr: *mut SECItem = oid.as_mut(); + + let slot = Slot::internal()?; + + let mut client_public_ptr = ptr::null_mut(); + + // We have to be careful with error handling between the `PK11_GenerateKeyPairWithOpFlags` and + // `PublicKey::from_ptr` calls here, so I've wrapped them in the same unsafe block as a + // warning. TODO(jms) Replace this once there is a safer alternative. + // https://github.com/mozilla/nss-gk-api/issues/1 + let (client_private, client_public) = unsafe { + let client_private = + // Type of `param` argument depends on mechanism. For EC keygen it is + // `SECKEYECParams *` which is a typedef for `SECItem *`. + PK11_GenerateKeyPairWithOpFlags( + *slot, + CKM_EC_KEY_PAIR_GEN, + oid_ptr.cast(), + &mut client_public_ptr, + PK11_ATTR_SESSION, + CKF_DERIVE, + CKF_DERIVE, + ptr::null_mut(), + ) + .into_result()?; + + let client_public = PublicKey::from_ptr(client_public_ptr)?; + + (client_private, client_public) + }; + + let peer_spki = der_spki_from_cose(peer_cose_key)?; + let peer_public = nss_public_key_from_der_spki(&peer_spki)?; + let shared_secret = encapsulate_helper(peer_public, client_private)?; + + let client_cose_key = cose_key_from_nss_public(peer_cose_key.alg, ec2key.curve, client_public)?; + + Ok(ECDHSecret { + remote: COSEKey { + alg: peer_cose_key.alg, + key: COSEKeyType::EC2(ec2key.clone()), + }, + my: client_cose_key, + shared_secret, + }) +} + +fn nss_public_key_from_der_spki(spki: &[u8]) -> Result<PublicKey> { + let mut spki_item = SECItemBorrowed::wrap(&spki); + let spki_item_ptr: *mut SECItem = spki_item.as_mut(); + let nss_spki = unsafe { + SubjectPublicKeyInfo::from_ptr(SECKEY_DecodeDERSubjectPublicKeyInfo(spki_item_ptr))? + }; + let public_key = unsafe { PublicKey::from_ptr(SECKEY_ExtractPublicKey(*nss_spki))? }; + Ok(public_key) +} + +fn cose_key_from_nss_public( + alg: COSEAlgorithm, + curve: ECDSACurve, + nss_public: PublicKey, +) -> Result<COSEKey> { + let public_data = nss_public.key_data()?; + let (public_x, public_y) = serialize_key(curve, &public_data)?; + Ok(COSEKey { + alg, + key: COSEKeyType::EC2(COSEEC2Key { + curve, + x: public_x.to_vec(), + y: public_y.to_vec(), + }), + }) +} + +/// `peer_public`: The authenticator's public key. +/// `client_private`: Our ephemeral private key. +fn encapsulate_helper(peer_public: PublicKey, client_private: PrivateKey) -> Result<Vec<u8>> { + let ecdh_x_coord = unsafe { + PK11_PubDeriveWithKDF( + *client_private, + *peer_public, + PR_FALSE, + std::ptr::null_mut(), + std::ptr::null_mut(), + CKM_ECDH1_DERIVE, + CKM_SHA512_HMAC, // unused + CKA_DERIVE, // unused + 0, + CKD_NULL, + std::ptr::null_mut(), + std::ptr::null_mut(), + ) + .into_result()? + }; + let ecdh_x_coord_bytes = ecdh_x_coord.as_bytes()?; + let mut shared_secret = [0u8; SHA256_LENGTH]; + unsafe { + PK11_HashBuf( + SECOidTag::SEC_OID_SHA256, + shared_secret.as_mut_ptr(), + ecdh_x_coord_bytes.as_ptr(), + ecdh_x_coord_bytes.len() as i32, + ) + }; + Ok(shared_secret.to_vec()) +} + +/// Encrypts a plaintext to produce a ciphertext, which may be longer than the plaintext. +/// The plaintext is restricted to being a multiple of the AES block size (16 bytes) in length. +pub(crate) fn encrypt(key: &[u8], data: &[u8]) -> Result<Vec<u8>> { + nss_gk_api::init(); + + if key.len() != 32 { + return Err(BackendError::NSSError( + "Invalid AES-256 key length".to_string(), + )); + } + + // The input must be a multiple of the AES block size, 16 + if data.len() % AES_BLOCK_SIZE != 0 { + return Err(BackendError::NSSError( + "Input to encrypt is too long".to_string(), + )); + } + let in_len = c_uint::try_from(data.len())?; + + let slot = Slot::internal()?; + + let sym_key = unsafe { + PK11_ImportSymKey( + *slot, + CKM_AES_CBC, + PK11Origin::PK11_OriginUnwrap, + CKA_ENCRYPT, + SECItemBorrowed::wrap(key).as_mut(), + ptr::null_mut(), + ) + .into_result()? + }; + + let iv = [0u8; AES_BLOCK_SIZE]; + let mut params = SECItemBorrowed::wrap(&iv); + let params_ptr: *mut SECItem = params.as_mut(); + let mut out_len: c_uint = 0; + let mut out = vec![0; in_len as usize]; + unsafe { + PK11_Encrypt( + *sym_key, + CKM_AES_CBC, + params_ptr, + out.as_mut_ptr(), + &mut out_len, + in_len, + data.as_ptr(), + in_len, + ) + .into_result()? + } + // CKM_AES_CBC should have output length equal to input length. + debug_assert_eq!(out_len, in_len); + + Ok(out) +} + +/// Decrypts a ciphertext and returns the plaintext. +pub(crate) fn decrypt(key: &[u8], data: &[u8]) -> Result<Vec<u8>> { + nss_gk_api::init(); + let slot = Slot::internal()?; + + if key.len() != 32 { + return Err(BackendError::NSSError( + "Invalid AES-256 key length".to_string(), + )); + } + + // The input must be a multiple of the AES block size, 16 + if data.len() % AES_BLOCK_SIZE != 0 { + return Err(BackendError::NSSError( + "Invalid input to decrypt".to_string(), + )); + } + let in_len = c_uint::try_from(data.len())?; + + let sym_key = unsafe { + PK11_ImportSymKey( + *slot, + CKM_AES_CBC, + PK11Origin::PK11_OriginUnwrap, + CKA_ENCRYPT, + SECItemBorrowed::wrap(key).as_mut(), + ptr::null_mut(), + ) + .into_result()? + }; + + let iv = [0u8; AES_BLOCK_SIZE]; + let mut params = SECItemBorrowed::wrap(&iv); + let params_ptr: *mut SECItem = params.as_mut(); + let mut out_len: c_uint = 0; + let mut out = vec![0; in_len as usize]; + unsafe { + PK11_Decrypt( + *sym_key, + CKM_AES_CBC, + params_ptr, + out.as_mut_ptr(), + &mut out_len, + in_len, + data.as_ptr(), + in_len, + ) + .into_result()? + } + // CKM_AES_CBC should have output length equal to input length. + debug_assert_eq!(out_len, in_len); + + Ok(out) +} + +/// Computes a MAC of the given message. +pub(crate) fn authenticate(token: &[u8], input: &[u8]) -> Result<Vec<u8>> { + nss_gk_api::init(); + let slot = Slot::internal()?; + let sym_key = unsafe { + PK11_ImportSymKey( + *slot, + CKM_SHA256_HMAC, + PK11Origin::PK11_OriginUnwrap, + CKA_SIGN, + SECItemBorrowed::wrap(token).as_mut(), + ptr::null_mut(), + ) + .into_result()? + }; + let param = SECItemBorrowed::make_empty(); + let context = unsafe { + PK11_CreateContextBySymKey(CKM_SHA256_HMAC, CKA_SIGN, *sym_key, param.as_ref()) + .into_result()? + }; + unsafe { PK11_DigestOp(*context, input.as_ptr(), input.len().try_into()?).into_result()? }; + let mut digest = vec![0u8; 32]; + let mut digest_len = 0u32; + unsafe { + PK11_DigestFinal( + *context, + digest.as_mut_ptr(), + &mut digest_len, + digest.len() as u32, + ) + .into_result()? + } + assert_eq!(digest_len, 32); + Ok(digest) +} + +#[cfg(test)] +pub(crate) fn test_encapsulate( + peer_coseec2_key: &COSEEC2Key, + alg: COSEAlgorithm, + my_pub_key: &[u8], + my_priv_key: &[u8], +) -> Result<ECDHSecret> { + nss_gk_api::init(); + + let peer_cose_key = COSEKey { + alg: alg, + key: COSEKeyType::EC2(peer_coseec2_key.clone()), + }; + let spki = der_spki_from_cose(&peer_cose_key)?; + let peer_public = nss_public_key_from_der_spki(&spki)?; + + /* NSS has no mechanism to import a raw elliptic curve coordinate as a private key. + * We need to encode it in a key storage format such as PKCS#8. To avoid a dependency + * on an ASN.1 encoder for this test, we'll do it manually. */ + let pkcs8_private_key_info_version = &[0x02, 0x01, 0x00]; + let rfc5915_ec_private_key_version = &[0x02, 0x01, 0x01]; + + let (curve_oid, seq_len, alg_len, attr_len, ecpriv_len, param_len, spk_len) = + match peer_coseec2_key.curve { + ECDSACurve::SECP256R1 => ( + DER_OID_P256_BYTES, + [0x81, 0x87].as_slice(), + [0x13].as_slice(), + [0x6d].as_slice(), + [0x6b].as_slice(), + [0x44].as_slice(), + [0x42].as_slice(), + ), + x => return Err(BackendError::UnsupportedCurve(x)), + }; + + let priv_len = my_priv_key.len() as u8; // < 127 + + let mut pkcs8_priv: Vec<u8> = vec![]; + // RFC 5208 PrivateKeyInfo + pkcs8_priv.push(0x30); + pkcs8_priv.extend_from_slice(seq_len); + // Integer (0) + pkcs8_priv.extend_from_slice(pkcs8_private_key_info_version); + // AlgorithmIdentifier + pkcs8_priv.push(0x30); + pkcs8_priv.extend_from_slice(alg_len); + // ObjectIdentifier + pkcs8_priv.extend_from_slice(DER_OID_EC_PUBLIC_KEY_BYTES); + // RFC 5480 ECParameters + pkcs8_priv.extend_from_slice(DER_OID_P256_BYTES); + // Attributes + pkcs8_priv.push(0x04); + pkcs8_priv.extend_from_slice(attr_len); + // RFC 5915 ECPrivateKey + pkcs8_priv.push(0x30); + pkcs8_priv.extend_from_slice(ecpriv_len); + pkcs8_priv.extend_from_slice(rfc5915_ec_private_key_version); + pkcs8_priv.push(0x04); + pkcs8_priv.push(priv_len); + pkcs8_priv.extend_from_slice(my_priv_key); + pkcs8_priv.push(0xa1); + pkcs8_priv.extend_from_slice(param_len); + pkcs8_priv.push(0x03); + pkcs8_priv.extend_from_slice(spk_len); + pkcs8_priv.push(0x0); + pkcs8_priv.extend_from_slice(&my_pub_key); + + // Now we can import the private key. + let slot = Slot::internal()?; + let mut pkcs8_priv_item = SECItemBorrowed::wrap(&pkcs8_priv); + let pkcs8_priv_item_ptr: *mut SECItem = pkcs8_priv_item.as_mut(); + let mut client_private_ptr = ptr::null_mut(); + unsafe { + PK11_ImportDERPrivateKeyInfoAndReturnKey( + *slot, + pkcs8_priv_item_ptr, + ptr::null_mut(), + ptr::null_mut(), + PR_FALSE, + PR_FALSE, + 255, /* todo: expose KU_ flags in nss-gk-api */ + &mut client_private_ptr, + ptr::null_mut(), + ) + }; + + let client_private = unsafe { PrivateKey::from_ptr(client_private_ptr) }?; + let client_public = unsafe { PublicKey::from_ptr(SECKEY_ConvertToPublicKey(*client_private))? }; + let client_cose_key = cose_key_from_nss_public(alg, peer_coseec2_key.curve, client_public)?; + + let shared_secret = encapsulate_helper(peer_public, client_private)?; + + Ok(ECDHSecret { + remote: peer_cose_key, + my: client_cose_key, + shared_secret, + }) +} diff --git a/third_party/rust/authenticator/src/crypto/openssl.rs b/third_party/rust/authenticator/src/crypto/openssl.rs new file mode 100644 index 0000000000..28a09bd3eb --- /dev/null +++ b/third_party/rust/authenticator/src/crypto/openssl.rs @@ -0,0 +1,283 @@ +use super::{ + /*Signature,*/ COSEAlgorithm, COSEEC2Key, /*PlainText*/ COSEKey, COSEKeyType, + /*CypherText,*/ ECDHSecret, ECDSACurve, +}; +use openssl::bn::{BigNum, BigNumContext}; +use openssl::derive::Deriver; +#[cfg(test)] +use openssl::ec::PointConversionForm; +use openssl::ec::{EcGroup, EcKey, EcPoint}; +use openssl::error::ErrorStack; +use openssl::hash::{hash, MessageDigest}; +use openssl::nid::Nid; +use openssl::pkey::{PKey, Private}; +use openssl::sign::{Signer, Verifier}; +use openssl::symm::{Cipher, Crypter, Mode}; +use openssl::x509::X509; +use serde::{Serialize, Serializer}; +use serde_bytes::ByteBuf; + +fn openssl_string<S>(_: &ErrorStack, s: S) -> std::result::Result<S::Ok, S::Error> +where + S: Serializer, +{ + s.serialize_str("OpenSSLError") +} + +/// Errors that can be returned from COSE functions. +#[derive(Clone, Debug, Serialize)] +pub enum BackendError { + #[serde(serialize_with = "openssl_string")] + OpenSSL(ErrorStack), + UnsupportedCurve(ECDSACurve), + UnsupportedKeyType, +} + +impl From<ErrorStack> for BackendError { + fn from(e: ErrorStack) -> Self { + BackendError::OpenSSL(e) + } +} + +impl From<&ErrorStack> for BackendError { + fn from(e: &ErrorStack) -> Self { + BackendError::OpenSSL(e.clone()) + } +} + +pub type Result<T> = std::result::Result<T, BackendError>; + +/* From CTAP2.1 spec: + +initialize() + + This is run by the platform when starting a series of transactions with a specific authenticator. +encapsulate(peerCoseKey) → (coseKey, sharedSecret) | error + + Generates an encapsulation for the authenticator’s public key and returns the message to transmit and the shared secret. +encrypt(key, demPlaintext) → ciphertext + + Encrypts a plaintext to produce a ciphertext, which may be longer than the plaintext. The plaintext is restricted to being a multiple of the AES block size (16 bytes) in length. +decrypt(key, ciphertext) → plaintext | error + + Decrypts a ciphertext and returns the plaintext. +authenticate(key, message) → signature + + Computes a MAC of the given message. +*/ + +fn to_openssl_name(curve: ECDSACurve) -> Result<Nid> { + match curve { + ECDSACurve::SECP256R1 => Ok(Nid::X9_62_PRIME256V1), + ECDSACurve::SECP384R1 => Ok(Nid::SECP384R1), + ECDSACurve::SECP521R1 => Ok(Nid::SECP521R1), + x => Err(BackendError::UnsupportedCurve(x)), + } +} + +fn affine_coordinates(curve: ECDSACurve, bytes: &[u8]) -> Result<(ByteBuf, ByteBuf)> { + let name = to_openssl_name(curve)?; + let group = EcGroup::from_curve_name(name)?; + + let mut ctx = BigNumContext::new()?; + let point = EcPoint::from_bytes(&group, bytes, &mut ctx)?; + + let mut x = BigNum::new()?; + let mut y = BigNum::new()?; + + point.affine_coordinates_gfp(&group, &mut x, &mut y, &mut ctx)?; + //point.affine_coordinates_gf2m(&group, &mut x, &mut y, &mut ctx)?; + + Ok((ByteBuf::from(x.to_vec()), ByteBuf::from(y.to_vec()))) +} + +// TODO(MS): Maybe remove ByteBuf and return Vec<u8>'s instead for a cleaner interface +pub(crate) fn serialize_key(curve: ECDSACurve, key: &[u8]) -> Result<(ByteBuf, ByteBuf)> { + affine_coordinates(curve, key) +} + +#[cfg(test)] +pub(crate) fn parse_key(curve: ECDSACurve, x: &[u8], y: &[u8]) -> Result<Vec<u8>> { + let name = to_openssl_name(curve)?; + let group = EcGroup::from_curve_name(name)?; + + let mut ctx = BigNumContext::new()?; + let x = BigNum::from_slice(x)?; + let y = BigNum::from_slice(y)?; + + let key = EcKey::from_public_key_affine_coordinates(&group, &x, &y)?; + // TODO(baloo): what is uncompressed?! + let pub_key = key.public_key(); + + Ok(pub_key.to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)?) +} + +/// This is run by the platform when starting a series of transactions with a specific authenticator. +//pub(crate) fn initialize() { +// +//} + +/// Generates an encapsulation for the authenticator’s public key and returns the message +/// to transmit and the shared secret. +pub(crate) fn encapsulate(key: &COSEKey) -> Result<ECDHSecret> { + if let COSEKeyType::EC2(ec2key) = &key.key { + let curve_name = to_openssl_name(ec2key.curve)?; + let group = EcGroup::from_curve_name(curve_name)?; + let my_key = EcKey::generate(&group)?; + + encapsulate_helper(&ec2key, key.alg, group, my_key) + } else { + Err(BackendError::UnsupportedKeyType) + } +} + +pub(crate) fn encapsulate_helper( + key: &COSEEC2Key, + alg: COSEAlgorithm, + group: EcGroup, + my_key: EcKey<Private>, +) -> Result<ECDHSecret> { + let mut ctx = BigNumContext::new()?; + let mut x = BigNum::new()?; + let mut y = BigNum::new()?; + + my_key + .public_key() + .affine_coordinates_gfp(&group, &mut x, &mut y, &mut ctx)?; + + let my_public_key = COSEKey { + alg, + key: COSEKeyType::EC2(COSEEC2Key { + curve: key.curve.clone(), + x: x.to_vec(), + y: y.to_vec(), + }), + }; + + // let point = EcPoint::from_bytes(&group, &key.key, &mut ctx)?; + let peer_public_key = PKey::from_ec_key(EcKey::from_public_key_affine_coordinates( + &group, + BigNum::from_slice(&key.x).as_ref()?, + BigNum::from_slice(&key.y).as_ref()?, + )?)?; + + let my_ec_key = PKey::from_ec_key(my_key)?; + let mut deriver = Deriver::new(my_ec_key.as_ref())?; + deriver.set_peer(&peer_public_key)?; + let shared_sec = deriver.derive_to_vec()?; + + // Hashing the key material + let digest = hash(MessageDigest::sha256(), &shared_sec)?; + + Ok(ECDHSecret { + remote: COSEKey { + alg, + key: COSEKeyType::EC2(key.clone()), + }, + my: my_public_key, + shared_secret: digest.as_ref().to_vec(), + }) +} + +#[cfg(test)] +pub(crate) fn test_encapsulate( + key: &COSEEC2Key, + alg: COSEAlgorithm, + my_pub_key: &[u8], + my_priv_key: &[u8], +) -> Result<ECDHSecret> { + let curve_name = to_openssl_name(key.curve)?; + let group = EcGroup::from_curve_name(curve_name)?; + + let mut ctx = BigNumContext::new()?; + let my_pub_point = EcPoint::from_bytes(&group, &my_pub_key, &mut ctx)?; + let my_priv_bignum = BigNum::from_slice(my_priv_key)?; + let my_key = EcKey::from_private_components(&group, &my_priv_bignum, &my_pub_point)?; + + encapsulate_helper(key, alg, group, my_key) +} + +/// Encrypts a plaintext to produce a ciphertext, which may be longer than the plaintext. +/// The plaintext is restricted to being a multiple of the AES block size (16 bytes) in length. +pub(crate) fn encrypt( + key: &[u8], + plain_text: &[u8], /*PlainText*/ +) -> Result<Vec<u8> /*CypherText*/> { + let cipher = Cipher::aes_256_cbc(); + + // TODO(baloo): This might trigger a panic if size is not big enough + let mut cypher_text = vec![0; plain_text.len() * 2]; + cypher_text.resize(plain_text.len() * 2, 0); + // Spec says explicitly IV=0 + let iv = [0u8; 16]; + let mut encrypter = Crypter::new(cipher, Mode::Encrypt, key, Some(&iv))?; + encrypter.pad(false); + let mut out_size = 0; + out_size += encrypter.update(plain_text, cypher_text.as_mut_slice())?; + out_size += encrypter.finalize(cypher_text.as_mut_slice())?; + cypher_text.truncate(out_size); + Ok(cypher_text) +} + +/// Decrypts a ciphertext and returns the plaintext. +pub(crate) fn decrypt( + key: &[u8], + cypher_text: &[u8], /*CypherText*/ +) -> Result<Vec<u8> /*PlainText*/> { + let cipher = Cipher::aes_256_cbc(); + + // TODO(baloo): This might trigger a panic if size is not big enough + let mut plain_text = vec![0; cypher_text.len() * 2]; + plain_text.resize(cypher_text.len() * 2, 0); + // Spec says explicitly IV=0 + let iv = [0u8; 16]; + let mut encrypter = Crypter::new(cipher, Mode::Decrypt, key, Some(&iv))?; + encrypter.pad(false); + let mut out_size = 0; + out_size += encrypter.update(cypher_text, plain_text.as_mut_slice())?; + out_size += encrypter.finalize(plain_text.as_mut_slice())?; + plain_text.truncate(out_size); + + Ok(plain_text) +} + +/// Computes a MAC of the given message. +pub(crate) fn authenticate(token: &[u8], input: &[u8]) -> Result<Vec<u8>> { + // Create a PKey + let key = PKey::hmac(token)?; + + // Compute the HMAC + let mut signer = Signer::new(MessageDigest::sha256(), &key)?; + signer.update(input)?; + let hmac = signer.sign_to_vec()?; + Ok(hmac) +} + +// Currently unsued, because rc_crypto does not expose PKCS12 of NSS, so we can't parse the cert there +// To use it in statemachine.rs for example, do: +// if let Ok(cdhash) = client_data.hash() { +// let verification_data: Vec<u8> = attestation +// .auth_data +// .to_vec() +// .iter() +// .chain(cdhash.as_ref().iter()) +// .copied() +// .collect(); +// let res = attestation.att_statement.verify(&verification_data); +// ... +// } +#[allow(dead_code)] +pub(crate) fn verify( + sig_alg: ECDSACurve, + pub_key: &[u8], + signature: &[u8], + data: &[u8], +) -> Result<bool> { + let _alg = to_openssl_name(sig_alg)?; // TODO(MS): Actually use this to determine the right MessageDigest below + let pkey = X509::from_der(&pub_key)?; + let pubkey = pkey.public_key()?; + let mut verifier = Verifier::new(MessageDigest::sha256(), &pubkey)?; + verifier.update(data)?; + let res = verifier.verify(signature)?; + Ok(res) +} diff --git a/third_party/rust/authenticator/src/crypto/ring.rs b/third_party/rust/authenticator/src/crypto/ring.rs new file mode 100644 index 0000000000..ff8178ae57 --- /dev/null +++ b/third_party/rust/authenticator/src/crypto/ring.rs @@ -0,0 +1,168 @@ +use super::{ + /*Signature,*/ COSEAlgorithm, COSEEC2Key, /*PlainText*/ COSEKey, COSEKeyType, + /*CypherText,*/ ECDHSecret, ECDSACurve, +}; +use ring::agreement::{ + agree_ephemeral, Algorithm, EphemeralPrivateKey, UnparsedPublicKey, ECDH_P256, ECDH_P384, +}; +use ring::digest; +use ring::hmac; +use ring::rand::SystemRandom; +use ring::signature::KeyPair; +use serde::Serialize; +use serde_bytes::ByteBuf; +/* +initialize() + + This is run by the platform when starting a series of transactions with a specific authenticator. +encapsulate(peerCoseKey) → (coseKey, sharedSecret) | error + + Generates an encapsulation for the authenticator’s public key and returns the message to transmit and the shared secret. +encrypt(key, demPlaintext) → ciphertext + + Encrypts a plaintext to produce a ciphertext, which may be longer than the plaintext. The plaintext is restricted to being a multiple of the AES block size (16 bytes) in length. +decrypt(key, ciphertext) → plaintext | error + + Decrypts a ciphertext and returns the plaintext. +authenticate(key, message) → signature + + Computes a MAC of the given message. +*/ + +pub type Result<T> = std::result::Result<T, BackendError>; + +#[derive(Clone, Debug, PartialEq, Serialize)] +pub enum BackendError { + AgreementError, + UnspecifiedRingError, + KeyRejected, + UnsupportedKeyType, + UnsupportedCurve(ECDSACurve), +} + +fn to_ring_curve(curve: ECDSACurve) -> Result<&'static Algorithm> { + match curve { + ECDSACurve::SECP256R1 => Ok(&ECDH_P256), + ECDSACurve::SECP384R1 => Ok(&ECDH_P384), + x => Err(BackendError::UnsupportedCurve(x)), + } +} + +impl From<ring::error::Unspecified> for BackendError { + fn from(e: ring::error::Unspecified) -> Self { + BackendError::UnspecifiedRingError + } +} + +impl From<ring::error::KeyRejected> for BackendError { + fn from(e: ring::error::KeyRejected) -> Self { + BackendError::KeyRejected + } +} + +pub(crate) fn parse_key(curve: ECDSACurve, x: &[u8], y: &[u8]) -> Result<Vec<u8>> { + compile_error!( + "Ring-backend is not yet implemented. Compile with `--features crypto_openssl` for now." + ) +} + +// TODO(MS): Maybe remove ByteBuf and return Vec<u8>'s instead for a cleaner interface +pub(crate) fn serialize_key(curve: ECDSACurve, key: &[u8]) -> Result<(ByteBuf, ByteBuf)> { + compile_error!( + "Ring-backend is not yet implemented. Compile with `--features crypto_openssl` for now." + ) +} + +/// This is run by the platform when starting a series of transactions with a specific authenticator. +pub(crate) fn initialize() { + compile_error!( + "Ring-backend is not yet implemented. Compile with `--features crypto_openssl` for now." + ) +} + +/// Generates an encapsulation for the authenticator’s public key and returns the message +/// to transmit and the shared secret. +pub(crate) fn encapsulate(key: &COSEKey) -> Result<ECDHSecret> { + if let COSEKeyType::EC2(ec2key) = &key.key { + // let curve_name = to_openssl_name(ec2key.curve)?; + // let group = EcGroup::from_curve_name(curve_name)?; + // let my_key = EcKey::generate(&group)?; + + // encapsulate_helper(&ec2key, key.alg, group, my_key) + let rng = SystemRandom::new(); + let peer_public_key_alg = to_ring_curve(ec2key.curve)?; + let private_key = EphemeralPrivateKey::generate(peer_public_key_alg, &rng)?; + let my_public_key = private_key.compute_public_key()?; + let (x, y) = serialize_key(ec2key.curve, my_public_key.as_ref())?; + let my_public_key = COSEKey { + alg: key.alg, + key: COSEKeyType::EC2(COSEEC2Key { + curve: ec2key.curve, + x: x.to_vec(), + y: y.to_vec(), + }), + }; + + let key_bytes = parse_key(ec2key.curve, &ec2key.x, &ec2key.y)?; + let peer_public_key = UnparsedPublicKey::new(peer_public_key_alg, &key_bytes); + + let shared_secret = agree_ephemeral(private_key, &peer_public_key, (), |key_material| { + let digest = digest::digest(&digest::SHA256, key_material); + Ok(Vec::from(digest.as_ref())) + }) + .map_err(|_| BackendError::AgreementError)?; + + Ok(ECDHSecret { + remote: COSEKey { + alg: key.alg, + key: COSEKeyType::EC2(ec2key.clone()), + }, + my: my_public_key, + shared_secret, + }) + } else { + Err(BackendError::UnsupportedKeyType) + } +} + +#[cfg(test)] +pub(crate) fn test_encapsulate( + key: &COSEEC2Key, + alg: COSEAlgorithm, + my_pub_key: &[u8], + my_priv_key: &[u8], +) -> Result<ECDHSecret> { + compile_error!( + "Ring-backend is not yet implemented. Compile with `--features crypto_openssl` for now." + ) +} + +/// Encrypts a plaintext to produce a ciphertext, which may be longer than the plaintext. +/// The plaintext is restricted to being a multiple of the AES block size (16 bytes) in length. +pub(crate) fn encrypt( + key: &[u8], + plain_text: &[u8], /*PlainText*/ +) -> Result<Vec<u8> /*CypherText*/> { + // Ring doesn't support AES-CBC yet + compile_error!( + "Ring-backend is not yet implemented. Compile with `--features crypto_openssl` for now." + ) +} + +/// Decrypts a ciphertext and returns the plaintext. +pub(crate) fn decrypt( + key: &[u8], + cypher_text: &[u8], /*CypherText*/ +) -> Result<Vec<u8> /*PlainText*/> { + // Ring doesn't support AES-CBC yet + compile_error!( + "Ring-backend is not yet implemented. Compile with `--features crypto_openssl` for now." + ) +} + +/// Computes a MAC of the given message. +pub(crate) fn authenticate(token: &[u8], input: &[u8]) -> Result<Vec<u8>> { + let s_key = hmac::Key::new(hmac::HMAC_SHA256, token); + let tag = hmac::sign(&s_key, input); + Ok(tag.as_ref().to_vec()) +} diff --git a/third_party/rust/authenticator/src/ctap2-capi.h b/third_party/rust/authenticator/src/ctap2-capi.h new file mode 100644 index 0000000000..d9f5b903b9 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2-capi.h @@ -0,0 +1,254 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __CTAP2_CAPI +#define __CTAP2_CAPI +#include <stdlib.h> +#include "nsString.h" + +extern "C" { +const uint8_t CTAP2_SIGN_RESULT_PUBKEY_CRED_ID = 1; +const uint8_t CTAP2_SIGN_RESULT_AUTH_DATA = 2; +const uint8_t CTAP2_SIGN_RESULT_SIGNATURE = 3; +const uint8_t CTAP2_SIGN_RESULT_USER_ID = 4; +const uint8_t CTAP2_SIGN_RESULT_USER_NAME = 5; + +typedef struct { + const uint8_t *id_ptr; + size_t id_len; + const char *name; +} AuthenticatorArgsUser; + +typedef struct { + const uint8_t *ptr; + size_t len; +} AuthenticatorArgsChallenge; + +typedef struct { + const int32_t *ptr; + size_t len; +} AuthenticatorArgsPubCred; + +typedef struct { + bool resident_key; + bool user_verification; + bool user_presence; + bool force_none_attestation; +} AuthenticatorArgsOptions; + +// NOTE: Preconditions +// * All rust_u2f_mgr* pointers must refer to pointers which are returned +// by rust_u2f_mgr_new, and must be freed with rust_u2f_mgr_free. +// * All rust_u2f_khs* pointers must refer to pointers which are returned +// by rust_u2f_pkcd_new, and must be freed with rust_u2f_pkcd_free. +// * All rust_u2f_res* pointers must refer to pointers passed to the +// register() and sign() callbacks. They can be null on failure. + +// The `rust_u2f_key_handles` opaque type is equivalent to the rust type +// `Ctap2PubKeyCredDescriptors` +struct rust_ctap2_pub_key_cred_descriptors; + +/// Ctap2PubKeyCredDescriptors functions. +rust_ctap2_pub_key_cred_descriptors* rust_ctap2_pkcd_new(); +void rust_ctap2_pkcd_add(rust_ctap2_pub_key_cred_descriptors* pkcd, const uint8_t* id_ptr, + size_t id_len, uint8_t transports); +/* unsafe */ void rust_ctap2_pkcd_free(rust_ctap2_pub_key_cred_descriptors* khs); + +// The `rust_ctap2_mgr` opaque type is equivalent to the rust type `Ctap2Manager` +// struct rust_ctap_manager; + +// The `rust_ctap2_result` opaque type is equivalent to the rust type `RegisterResult` +struct rust_ctap2_register_result; + +// The `rust_ctap2_result` opaque type is equivalent to the rust type `RegisterResult` +struct rust_ctap2_sign_result; + +// Ctap2 exposes the results directly without repackaging them. Use getter-functions. +typedef void (*rust_ctap2_register_callback)(uint64_t, rust_ctap2_register_result*); +typedef void (*rust_ctap2_sign_callback)(uint64_t, rust_ctap2_sign_result*); + +// Status updates get sent, if a device needs a PIN, if a device needs to be selected, etc. +struct rust_ctap2_status_update_res; +// May be called with NULL, in case of an error +typedef void (*rust_ctap2_status_update_callback)(rust_ctap2_status_update_res*); + +rust_ctap_manager* rust_ctap2_mgr_new(); +/* unsafe */ void rust_ctap2_mgr_free(rust_ctap_manager* mgr); + +/* unsafe */ void rust_ctap2_register_res_free(rust_ctap2_register_result* res); +/* unsafe */ void rust_ctap2_sign_res_free(rust_ctap2_sign_result* res); + +uint64_t rust_ctap2_mgr_register( + rust_ctap_manager* mgr, uint64_t timeout, rust_ctap2_register_callback, rust_ctap2_status_update_callback, + AuthenticatorArgsChallenge challenge, + const char* relying_party_id, const char *origin_ptr, + AuthenticatorArgsUser user, AuthenticatorArgsPubCred pub_cred_params, + const rust_ctap2_pub_key_cred_descriptors* exclude_list, AuthenticatorArgsOptions options, + const char *pin +); + +uint64_t rust_ctap2_mgr_sign( + rust_ctap_manager* mgr, uint64_t timeout, rust_ctap2_sign_callback, rust_ctap2_status_update_callback, + AuthenticatorArgsChallenge challenge, + const char* relying_party_id, const char *origin_ptr, + const rust_ctap2_pub_key_cred_descriptors* allow_list, AuthenticatorArgsOptions options, + const char *pin +); + +void rust_ctap2_mgr_cancel(rust_ctap_manager* mgr); + +// Returns 0 for success, or the U2F_ERROR error code >= 1. +uint8_t rust_ctap2_register_result_error(const rust_ctap2_register_result* res); +uint8_t rust_ctap2_sign_result_error(const rust_ctap2_sign_result* res); + +/// # Safety +/// +/// This function is used to get the length, prior to calling +/// rust_ctap2_register_result_client_data_copy() +bool rust_ctap2_register_result_client_data_len( + const rust_ctap2_register_result *res, + size_t *len +); + +/// # Safety +/// +/// This method does not ensure anything about dst before copying, so +/// ensure it is long enough (using rust_ctap2_register_result_client_data_len) +bool rust_ctap2_register_result_client_data_copy( + const rust_ctap2_register_result *res, + const char *dst +); + +/// # Safety +/// +/// This function is used to get the length, prior to calling +/// rust_ctap2_register_result_item_copy() +bool rust_ctap2_register_result_attestation_len( + const rust_ctap2_register_result *res, + size_t *len +); + +/// # Safety +/// +/// This method does not ensure anything about dst before copying, so +/// ensure it is long enough (using rust_ctap2_register_result_item_len) +bool rust_ctap2_register_result_attestation_copy( + const rust_ctap2_register_result* res, + uint8_t *dst +); +/// # Safety +/// +/// This function is used to get the length, prior to calling +/// rust_ctap2_register_result_client_data_copy() +bool rust_ctap2_sign_result_client_data_len( + const rust_ctap2_sign_result *res, + size_t *len +); + +/// # Safety +/// +/// This method does not ensure anything about dst before copying, so +/// ensure it is long enough (using rust_ctap2_sign_result_client_data_len) +bool rust_ctap2_sign_result_client_data_copy( + const rust_ctap2_sign_result *res, + const char *dst +); + +/// # Safety +/// +/// This function is used to get the length, prior to calling +/// rust_ctap2_register_result_client_data_copy() +bool rust_ctap2_sign_result_assertions_len( + const rust_ctap2_sign_result *res, + size_t *len +); + +bool rust_ctap2_sign_result_item_contains( + const rust_ctap2_sign_result *res, + size_t assertion_idx, + uint8_t item_idx +); + +/// # Safety +/// +/// This function is used to get the length, prior to calling +/// rust_ctap2_sign_result_item_copy() +bool rust_ctap2_sign_result_item_len( + const rust_ctap2_sign_result *res, + size_t assertion_idx, + uint8_t item_idx, + size_t *len +); + +/// # Safety +/// +/// This method does not ensure anything about dst before copying, so +/// ensure it is long enough (using rust_ctap2_sign_result_item_len) +bool rust_ctap2_sign_result_item_copy( + const rust_ctap2_sign_result* res, + size_t assertion_idx, + uint8_t item_idx, + uint8_t *dst +); + +bool rust_ctap2_sign_result_contains_username( + const rust_ctap2_sign_result *res, + size_t assertion_idx +); + +/// # Safety +/// +/// This function is used to get the length, prior to calling +/// rust_ctap2_sign_result_username_copy() +bool rust_ctap2_sign_result_username_len( + const rust_ctap2_sign_result *res, + size_t assertion_idx, + size_t *len +); + +/// # Safety +/// +/// This method does not ensure anything about dst before copying, so +/// ensure it is long enough (using rust_ctap2_sign_result_username_len) +bool rust_ctap2_sign_result_username_copy( + const rust_ctap2_sign_result* res, + size_t assertion_idx, + const char *dst +); + +/// # Safety +/// +/// This function is used to get the length, prior to calling +/// rust_ctap2_status_update_copy_json() +bool rust_ctap2_status_update_len( + const rust_ctap2_status_update_res *res, + size_t *len +); + +/// # Safety +/// +/// This method does not ensure anything about dst before copying, so +/// ensure it is long enough (using rust_ctap2_status_update_len) +bool rust_ctap2_status_update_copy_json( + const rust_ctap2_status_update_res *res, + const char *dst +); + +bool rust_ctap2_status_update_send_pin( + const rust_ctap2_status_update_res *res, + const char *pin +); + + +/// # Safety +/// This frees the memory of a status_update_res +bool rust_ctap2_destroy_status_update_res( + rust_ctap2_status_update_res *res +); + + +} +#endif // __CTAP2_CAPI diff --git a/third_party/rust/authenticator/src/ctap2/attestation.rs b/third_party/rust/authenticator/src/ctap2/attestation.rs new file mode 100644 index 0000000000..896db0dee3 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/attestation.rs @@ -0,0 +1,799 @@ +use super::utils::from_slice_stream; +use crate::crypto::COSEAlgorithm; +use crate::ctap2::commands::CommandError; +use crate::ctap2::server::RpIdHash; +use crate::{crypto::COSEKey, errors::AuthenticatorError}; +use nom::{ + bytes::complete::take, + combinator::{cond, map}, + error::Error as NomError, + number::complete::{be_u16, be_u32, be_u8}, + Err as NomErr, IResult, +}; +use serde::ser::{Error as SerError, SerializeMap, Serializer}; +use serde::{ + de::{Error as SerdeError, MapAccess, Visitor}, + Deserialize, Deserializer, Serialize, +}; +use serde_bytes::ByteBuf; +use serde_cbor; +use std::fmt; + +#[derive(Debug, PartialEq, Eq)] +pub enum HmacSecretResponse { + /// This is returned by MakeCredential calls to display if CredRandom was + /// successfully generated + Confirmed(bool), + /// This is returned by GetAssertion: + /// AES256-CBC(shared_secret, HMAC-SHA265(CredRandom, salt1) || HMAC-SHA265(CredRandom, salt2)) + Secret(Vec<u8>), +} + +impl Serialize for HmacSecretResponse { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + match self { + HmacSecretResponse::Confirmed(x) => serializer.serialize_bool(*x), + HmacSecretResponse::Secret(x) => serializer.serialize_bytes(&x), + } + } +} +impl<'de> Deserialize<'de> for HmacSecretResponse { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + struct HmacSecretResponseVisitor; + + impl<'de> Visitor<'de> for HmacSecretResponseVisitor { + type Value = HmacSecretResponse; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a byte array or a boolean") + } + + fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E> + where + E: SerdeError, + { + Ok(HmacSecretResponse::Secret(v.to_vec())) + } + + fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E> + where + E: SerdeError, + { + Ok(HmacSecretResponse::Confirmed(v)) + } + } + deserializer.deserialize_any(HmacSecretResponseVisitor) + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Default)] +pub struct Extension { + #[serde(rename = "pinMinLength", skip_serializing_if = "Option::is_none")] + pub pin_min_length: Option<u64>, + #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")] + pub hmac_secret: Option<HmacSecretResponse>, +} + +fn parse_extensions<'a>(input: &'a [u8]) -> IResult<&'a [u8], Extension, NomError<&'a [u8]>> { + serde_to_nom(input) +} + +#[derive(Serialize, PartialEq, Default, Eq, Clone)] +pub struct AAGuid(pub [u8; 16]); + +impl AAGuid { + pub fn from(src: &[u8]) -> Result<AAGuid, ()> { + let mut payload = [0u8; 16]; + if src.len() != payload.len() { + Err(()) + } else { + payload.copy_from_slice(src); + Ok(AAGuid(payload)) + } + } +} + +impl fmt::Debug for AAGuid { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "AAGuid({:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x})", + self.0[0], + self.0[1], + self.0[2], + self.0[3], + self.0[4], + self.0[5], + self.0[6], + self.0[7], + self.0[8], + self.0[9], + self.0[10], + self.0[11], + self.0[12], + self.0[13], + self.0[14], + self.0[15] + ) + } +} + +impl<'de> Deserialize<'de> for AAGuid { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + struct AAGuidVisitor; + + impl<'de> Visitor<'de> for AAGuidVisitor { + type Value = AAGuid; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a byte array") + } + + fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E> + where + E: SerdeError, + { + let mut buf = [0u8; 16]; + if v.len() != buf.len() { + return Err(E::invalid_length(v.len(), &"16")); + } + + buf.copy_from_slice(v); + + Ok(AAGuid(buf)) + } + } + + deserializer.deserialize_bytes(AAGuidVisitor) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct AttestedCredentialData { + pub aaguid: AAGuid, + pub credential_id: Vec<u8>, + pub credential_public_key: COSEKey, +} + +fn serde_to_nom<'a, Output>(input: &'a [u8]) -> IResult<&'a [u8], Output> +where + Output: Deserialize<'a>, +{ + from_slice_stream(input) + .map_err(|_e| nom::Err::Error(nom::error::make_error(input, nom::error::ErrorKind::NoneOf))) + // can't use custom errorkind because of error type mismatch in parse_attested_cred_data + //.map_err(|e| NomErr::Error(Context::Code(input, ErrorKind::Custom(e)))) + // .map_err(|_| NomErr::Error(Context::Code(input, ErrorKind::Custom(42)))) +} + +fn parse_attested_cred_data<'a>( + input: &'a [u8], +) -> IResult<&'a [u8], AttestedCredentialData, NomError<&'a [u8]>> { + let (rest, aaguid_res) = map(take(16u8), AAGuid::from)(input)?; + // // We can unwrap here, since we _know_ the input will be 16 bytes error out before calling from() + let aaguid = aaguid_res.unwrap(); + let (rest, cred_len) = be_u16(rest)?; + let (rest, credential_id) = map(take(cred_len), Vec::from)(rest)?; + let (rest, credential_public_key) = serde_to_nom(rest)?; + Ok(( + rest, + (AttestedCredentialData { + aaguid, + credential_id, + credential_public_key: credential_public_key, + }), + )) +} + +bitflags! { + pub struct AuthenticatorDataFlags: u8 { + const USER_PRESENT = 0x01; + const USER_VERIFIED = 0x04; + const ATTESTED = 0x40; + const EXTENSION_DATA = 0x80; + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct AuthenticatorData { + pub rp_id_hash: RpIdHash, + pub flags: AuthenticatorDataFlags, + pub counter: u32, + pub credential_data: Option<AttestedCredentialData>, + pub extensions: Extension, +} + +fn parse_ad<'a>(input: &'a [u8]) -> IResult<&'a [u8], AuthenticatorData, NomError<&'a [u8]>> { + let (rest, rp_id_hash_res) = map(take(32u8), RpIdHash::from)(input)?; + // We can unwrap here, since we _know_ the input to from() will be 32 bytes or error out before calling from() + let rp_id_hash = rp_id_hash_res.unwrap(); + // be conservative, there is a couple of reserved values in + // AuthenticatorDataFlags, just truncate the one we don't know + let (rest, flags) = map(be_u8, AuthenticatorDataFlags::from_bits_truncate)(rest)?; + let (rest, counter) = be_u32(rest)?; + let (rest, credential_data) = cond( + flags.contains(AuthenticatorDataFlags::ATTESTED), + parse_attested_cred_data, + )(rest)?; + let (rest, extensions) = cond( + flags.contains(AuthenticatorDataFlags::EXTENSION_DATA), + parse_extensions, + )(rest)?; + // TODO(baloo): we should check for end of buffer and raise a parse + // parse error if data is still in the buffer + //eof!() >> + Ok(( + rest, + AuthenticatorData { + rp_id_hash, + flags, + counter, + credential_data, + extensions: extensions.unwrap_or_default(), + }, + )) +} + +impl<'de> Deserialize<'de> for AuthenticatorData { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + struct AuthenticatorDataVisitor; + + impl<'de> Visitor<'de> for AuthenticatorDataVisitor { + type Value = AuthenticatorData; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a byte array") + } + + fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E> + where + E: SerdeError, + { + parse_ad(v) + .map(|(_input, value)| value) + .map_err(|e| match e { + NomErr::Incomplete(nom::Needed::Size(len)) => { + E::invalid_length(v.len(), &format!("{}", v.len() + len.get()).as_ref()) + } + NomErr::Incomplete(nom::Needed::Unknown) => { + E::invalid_length(v.len(), &"unknown") // We don't know the expected value + } + // TODO(baloo): is that enough? should we be more + // specific on the error type? + e => E::custom(e.to_string()), + }) + } + } + + deserializer.deserialize_bytes(AuthenticatorDataVisitor) + } +} + +impl AuthenticatorData { + pub fn to_vec(&self) -> Result<Vec<u8>, AuthenticatorError> { + let mut data = Vec::new(); + data.extend(&self.rp_id_hash.0); + data.extend(&[self.flags.bits()]); + data.extend(&self.counter.to_be_bytes()); + + // TODO(baloo): need to yield credential_data and extensions, but that dependends on flags, + // should we consider another type system? + if let Some(cred) = &self.credential_data { + data.extend(&cred.aaguid.0); + data.extend(&(cred.credential_id.len() as u16).to_be_bytes()); + data.extend(&cred.credential_id); + data.extend( + &serde_cbor::to_vec(&cred.credential_public_key) + .map_err(CommandError::Serializing)?, + ); + } + Ok(data) + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +/// x509 encoded attestation certificate +pub struct AttestationCertificate(#[serde(with = "serde_bytes")] pub(crate) Vec<u8>); + +impl AsRef<[u8]> for AttestationCertificate { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +#[derive(Serialize, Deserialize, PartialEq, Eq)] +pub struct Signature(#[serde(with = "serde_bytes")] pub(crate) ByteBuf); + +impl fmt::Debug for Signature { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let value = base64::encode_config(&self.0, base64::URL_SAFE_NO_PAD); + write!(f, "Signature({})", value) + } +} + +impl AsRef<[u8]> for Signature { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum AttestationStatement { + None, + Packed(AttestationStatementPacked), + // TODO(baloo): there is a couple other options than None and Packed: + // https://w3c.github.io/webauthn/#generating-an-attestation-object + // https://w3c.github.io/webauthn/#defined-attestation-formats + //TPM, + //AndroidKey, + //AndroidSafetyNet, + FidoU2F(AttestationStatementFidoU2F), +} + +// Not all crypto-backends currently provide "crypto::verify()", so we do not implement it yet. +// Also not sure, if we really need it. Would be a sanity-check only, to verify the signature is valid, +// before sendig it out. +// impl AttestationStatement { +// pub fn verify(&self, data: &[u8]) -> Result<bool, AuthenticatorError> { +// match self { +// AttestationStatement::None => Ok(true), +// AttestationStatement::Unparsed(_) => Err(AuthenticatorError::Custom( +// "Unparsed attestation object can't be used to verify signature.".to_string(), +// )), +// AttestationStatement::FidoU2F(att) => { +// let res = crypto::verify( +// crypto::SignatureAlgorithm::ES256, +// &att.attestation_cert[0].as_ref(), +// att.sig.as_ref(), +// data, +// )?; +// Ok(res) +// } +// AttestationStatement::Packed(att) => { +// if att.alg != Alg::ES256 { +// return Err(AuthenticatorError::Custom( +// "Verification only supported for ES256".to_string(), +// )); +// } +// let res = crypto::verify( +// crypto::SignatureAlgorithm::ES256, +// att.attestation_cert[0].as_ref(), +// att.sig.as_ref(), +// data, +// )?; +// Ok(res) +// } +// } +// } +// } + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +// See https://www.w3.org/TR/webauthn/#fido-u2f-attestation +pub struct AttestationStatementFidoU2F { + pub sig: Signature, + + #[serde(rename = "x5c")] + /// Certificate chain in x509 format + pub attestation_cert: Vec<AttestationCertificate>, +} + +#[allow(dead_code)] // TODO(MS): Remove me, once we can parse AttestationStatements and use this function +impl AttestationStatementFidoU2F { + pub fn new(cert: &[u8], signature: &[u8]) -> Self { + AttestationStatementFidoU2F { + sig: Signature(ByteBuf::from(signature)), + attestation_cert: vec![AttestationCertificate(Vec::from(cert))], + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +// TODO(baloo): there is a couple other options than x5c: +// https://www.w3.org/TR/webauthn/#packed-attestation +pub struct AttestationStatementPacked { + pub alg: COSEAlgorithm, + pub sig: Signature, + + #[serde(rename = "x5c")] + /// Certificate chain in x509 format + pub attestation_cert: Vec<AttestationCertificate>, +} + +#[derive(Debug, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +enum AttestationFormat { + #[serde(rename = "fido-u2f")] + FidoU2F, + Packed, + None, + // TOOD(baloo): only packed is implemented for now, see spec: + // https://www.w3.org/TR/webauthn/#defined-attestation-formats + //TPM, + //AndroidKey, + //AndroidSafetyNet, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct AttestationObject { + pub auth_data: AuthenticatorData, + pub att_statement: AttestationStatement, +} + +impl<'de> Deserialize<'de> for AttestationObject { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + struct AttestationObjectVisitor; + + impl<'de> Visitor<'de> for AttestationObjectVisitor { + type Value = AttestationObject; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a cbor map") + } + + fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error> + where + M: MapAccess<'de>, + { + let mut format: Option<AttestationFormat> = None; + let mut auth_data = None; + let mut att_statement = None; + + while let Some(key) = map.next_key()? { + match key { + // Spec for CTAP 2.0 is wrong and fmt should be numbered 1, and auth_data 2: + // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorMakeCredential + // Corrected in CTAP 2.1 and Webauthn spec + 1 => { + if format.is_some() { + return Err(SerdeError::duplicate_field("fmt")); + } + format = Some(map.next_value()?); + } + 2 => { + if auth_data.is_some() { + return Err(SerdeError::duplicate_field("auth_data")); + } + auth_data = Some(map.next_value()?); + } + 3 => { + let format = format + .as_ref() + .ok_or_else(|| SerdeError::missing_field("fmt"))?; + if att_statement.is_some() { + return Err(SerdeError::duplicate_field("att_statement")); + } + match format { + // This should not actually happen, but ... + AttestationFormat::None => { + att_statement = Some(AttestationStatement::None); + } + AttestationFormat::Packed => { + att_statement = + Some(AttestationStatement::Packed(map.next_value()?)); + } + AttestationFormat::FidoU2F => { + att_statement = + Some(AttestationStatement::FidoU2F(map.next_value()?)); + } + } + } + k => return Err(M::Error::custom(format!("unexpected key: {:?}", k))), + } + } + + let auth_data = + auth_data.ok_or_else(|| M::Error::custom("found no auth_data".to_string()))?; + let att_statement = att_statement.unwrap_or(AttestationStatement::None); + + Ok(AttestationObject { + auth_data, + att_statement, + }) + } + } + + deserializer.deserialize_bytes(AttestationObjectVisitor) + } +} + +impl Serialize for AttestationObject { + /// Serialize can be used to repackage the CBOR answer we get from the token using CTAP-format + /// to webauthn-format (string-keys like "authData" instead of numbers). Yes, the specs are weird. + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let map_len = 3; + let mut map = serializer.serialize_map(Some(map_len))?; + + let auth_data = self + .auth_data + .to_vec() + .map(|v| serde_cbor::Value::Bytes(v)) + .map_err(|_| SerError::custom("Failed to serialize auth_data"))?; + + map.serialize_entry(&"authData", &auth_data)?; + match self.att_statement { + AttestationStatement::None => { + // Even with Att None, an empty map is returned in the cbor! + map.serialize_entry(&"fmt", &"none")?; + let v = serde_cbor::Value::Map(std::collections::BTreeMap::new()); + map.serialize_entry(&"attStmt", &v)?; + } + AttestationStatement::Packed(ref v) => { + map.serialize_entry(&"fmt", &"packed")?; + map.serialize_entry(&"attStmt", v)?; + } + AttestationStatement::FidoU2F(ref v) => { + map.serialize_entry(&"fmt", &"fido-u2f")?; + map.serialize_entry(&"attStmt", v)?; + } + } + map.end() + } +} + +#[cfg(test)] +mod test { + use super::super::utils::from_slice_stream; + use super::*; + use serde_cbor::from_slice; + + const SAMPLE_ATTESTATION: [u8; 1006] = [ + 0xa3, 0x1, 0x66, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x64, 0x2, 0x58, 0xc4, 0x49, 0x96, 0xd, + 0xe5, 0x88, 0xe, 0x8c, 0x68, 0x74, 0x34, 0x17, 0xf, 0x64, 0x76, 0x60, 0x5b, 0x8f, 0xe4, + 0xae, 0xb9, 0xa2, 0x86, 0x32, 0xc7, 0x99, 0x5c, 0xf3, 0xba, 0x83, 0x1d, 0x97, 0x63, 0x41, + 0x0, 0x0, 0x0, 0x7, 0xcb, 0x69, 0x48, 0x1e, 0x8f, 0xf7, 0x40, 0x39, 0x93, 0xec, 0xa, 0x27, + 0x29, 0xa1, 0x54, 0xa8, 0x0, 0x40, 0xc3, 0xcf, 0x1, 0x3b, 0xc6, 0x26, 0x93, 0x28, 0xfb, + 0x7f, 0xa9, 0x76, 0xef, 0xa8, 0x4b, 0x66, 0x71, 0xad, 0xa9, 0x64, 0xea, 0xcb, 0x58, 0x76, + 0x54, 0x51, 0xa, 0xc8, 0x86, 0x4f, 0xbb, 0x53, 0x2d, 0xfb, 0x2, 0xfc, 0xdc, 0xa9, 0x84, + 0xc2, 0x5c, 0x67, 0x8a, 0x3a, 0xab, 0x57, 0xf3, 0x71, 0x77, 0xd3, 0xd4, 0x41, 0x64, 0x1, + 0x50, 0xca, 0x6c, 0x42, 0x73, 0x1c, 0x42, 0xcb, 0x81, 0xba, 0xa5, 0x1, 0x2, 0x3, 0x26, + 0x20, 0x1, 0x21, 0x58, 0x20, 0x9, 0x2e, 0x34, 0xfe, 0xa7, 0xd7, 0x32, 0xc8, 0xae, 0x4c, + 0xf6, 0x96, 0xbe, 0x7a, 0x12, 0xdc, 0x29, 0xd5, 0xf1, 0xd3, 0xf1, 0x55, 0x4d, 0xdc, 0x87, + 0xc4, 0xc, 0x9b, 0xd0, 0x17, 0xba, 0xf, 0x22, 0x58, 0x20, 0xc9, 0xf0, 0x97, 0x33, 0x55, + 0x36, 0x58, 0xd9, 0xdb, 0x76, 0xf5, 0xef, 0x95, 0xcf, 0x8a, 0xc7, 0xfc, 0xc1, 0xb6, 0x81, + 0x25, 0x5f, 0x94, 0x6b, 0x62, 0x13, 0x7d, 0xd0, 0xc4, 0x86, 0x53, 0xdb, 0x3, 0xa3, 0x63, + 0x61, 0x6c, 0x67, 0x26, 0x63, 0x73, 0x69, 0x67, 0x58, 0x48, 0x30, 0x46, 0x2, 0x21, 0x0, + 0xac, 0x2a, 0x78, 0xa8, 0xaf, 0x18, 0x80, 0x39, 0x73, 0x8d, 0x3, 0x5e, 0x4, 0x4d, 0x94, + 0x4f, 0x3f, 0x57, 0xce, 0x88, 0x41, 0xfa, 0x81, 0x50, 0x40, 0xb6, 0xd1, 0x95, 0xb5, 0xeb, + 0xe4, 0x6f, 0x2, 0x21, 0x0, 0x8f, 0xf4, 0x15, 0xc9, 0xb3, 0x6d, 0x1c, 0xd, 0x4c, 0xa3, + 0xcf, 0x99, 0x8a, 0x46, 0xd4, 0x4c, 0x8b, 0x5c, 0x26, 0x3f, 0xdf, 0x22, 0x6c, 0x9b, 0x23, + 0x83, 0x8b, 0x69, 0x47, 0x67, 0x48, 0x45, 0x63, 0x78, 0x35, 0x63, 0x81, 0x59, 0x2, 0xc1, + 0x30, 0x82, 0x2, 0xbd, 0x30, 0x82, 0x1, 0xa5, 0xa0, 0x3, 0x2, 0x1, 0x2, 0x2, 0x4, 0x18, + 0xac, 0x46, 0xc0, 0x30, 0xd, 0x6, 0x9, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0xd, 0x1, 0x1, 0xb, + 0x5, 0x0, 0x30, 0x2e, 0x31, 0x2c, 0x30, 0x2a, 0x6, 0x3, 0x55, 0x4, 0x3, 0x13, 0x23, 0x59, + 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x55, 0x32, 0x46, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, + 0x43, 0x41, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x20, 0x34, 0x35, 0x37, 0x32, 0x30, + 0x30, 0x36, 0x33, 0x31, 0x30, 0x20, 0x17, 0xd, 0x31, 0x34, 0x30, 0x38, 0x30, 0x31, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x18, 0xf, 0x32, 0x30, 0x35, 0x30, 0x30, 0x39, 0x30, + 0x34, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x30, 0x6e, 0x31, 0xb, 0x30, 0x9, 0x6, 0x3, + 0x55, 0x4, 0x6, 0x13, 0x2, 0x53, 0x45, 0x31, 0x12, 0x30, 0x10, 0x6, 0x3, 0x55, 0x4, 0xa, + 0xc, 0x9, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x41, 0x42, 0x31, 0x22, 0x30, 0x20, + 0x6, 0x3, 0x55, 0x4, 0xb, 0xc, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, + 0x61, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x31, 0x27, 0x30, 0x25, 0x6, 0x3, 0x55, 0x4, 0x3, 0xc, 0x1e, 0x59, 0x75, 0x62, 0x69, + 0x63, 0x6f, 0x20, 0x55, 0x32, 0x46, 0x20, 0x45, 0x45, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, + 0x6c, 0x20, 0x34, 0x31, 0x33, 0x39, 0x34, 0x33, 0x34, 0x38, 0x38, 0x30, 0x59, 0x30, 0x13, + 0x6, 0x7, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x2, 0x1, 0x6, 0x8, 0x2a, 0x86, 0x48, 0xce, 0x3d, + 0x3, 0x1, 0x7, 0x3, 0x42, 0x0, 0x4, 0x79, 0xea, 0x3b, 0x2c, 0x7c, 0x49, 0x70, 0x10, 0x62, + 0x23, 0xc, 0xd2, 0x3f, 0xeb, 0x60, 0xe5, 0x29, 0x31, 0x71, 0xd4, 0x83, 0xf1, 0x0, 0xbe, + 0x85, 0x9d, 0x6b, 0xf, 0x83, 0x97, 0x3, 0x1, 0xb5, 0x46, 0xcd, 0xd4, 0x6e, 0xcf, 0xca, + 0xe3, 0xe3, 0xf3, 0xf, 0x81, 0xe9, 0xed, 0x62, 0xbd, 0x26, 0x8d, 0x4c, 0x1e, 0xbd, 0x37, + 0xb3, 0xbc, 0xbe, 0x92, 0xa8, 0xc2, 0xae, 0xeb, 0x4e, 0x3a, 0xa3, 0x6c, 0x30, 0x6a, 0x30, + 0x22, 0x6, 0x9, 0x2b, 0x6, 0x1, 0x4, 0x1, 0x82, 0xc4, 0xa, 0x2, 0x4, 0x15, 0x31, 0x2e, + 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x31, 0x34, 0x38, 0x32, + 0x2e, 0x31, 0x2e, 0x37, 0x30, 0x13, 0x6, 0xb, 0x2b, 0x6, 0x1, 0x4, 0x1, 0x82, 0xe5, 0x1c, + 0x2, 0x1, 0x1, 0x4, 0x4, 0x3, 0x2, 0x5, 0x20, 0x30, 0x21, 0x6, 0xb, 0x2b, 0x6, 0x1, 0x4, + 0x1, 0x82, 0xe5, 0x1c, 0x1, 0x1, 0x4, 0x4, 0x12, 0x4, 0x10, 0xcb, 0x69, 0x48, 0x1e, 0x8f, + 0xf7, 0x40, 0x39, 0x93, 0xec, 0xa, 0x27, 0x29, 0xa1, 0x54, 0xa8, 0x30, 0xc, 0x6, 0x3, 0x55, + 0x1d, 0x13, 0x1, 0x1, 0xff, 0x4, 0x2, 0x30, 0x0, 0x30, 0xd, 0x6, 0x9, 0x2a, 0x86, 0x48, + 0x86, 0xf7, 0xd, 0x1, 0x1, 0xb, 0x5, 0x0, 0x3, 0x82, 0x1, 0x1, 0x0, 0x97, 0x9d, 0x3, 0x97, + 0xd8, 0x60, 0xf8, 0x2e, 0xe1, 0x5d, 0x31, 0x1c, 0x79, 0x6e, 0xba, 0xfb, 0x22, 0xfa, 0xa7, + 0xe0, 0x84, 0xd9, 0xba, 0xb4, 0xc6, 0x1b, 0xbb, 0x57, 0xf3, 0xe6, 0xb4, 0xc1, 0x8a, 0x48, + 0x37, 0xb8, 0x5c, 0x3c, 0x4e, 0xdb, 0xe4, 0x83, 0x43, 0xf4, 0xd6, 0xa5, 0xd9, 0xb1, 0xce, + 0xda, 0x8a, 0xe1, 0xfe, 0xd4, 0x91, 0x29, 0x21, 0x73, 0x5, 0x8e, 0x5e, 0xe1, 0xcb, 0xdd, + 0x6b, 0xda, 0xc0, 0x75, 0x57, 0xc6, 0xa0, 0xe8, 0xd3, 0x68, 0x25, 0xba, 0x15, 0x9e, 0x7f, + 0xb5, 0xad, 0x8c, 0xda, 0xf8, 0x4, 0x86, 0x8c, 0xf9, 0xe, 0x8f, 0x1f, 0x8a, 0xea, 0x17, + 0xc0, 0x16, 0xb5, 0x5c, 0x2a, 0x7a, 0xd4, 0x97, 0xc8, 0x94, 0xfb, 0x71, 0xd7, 0x53, 0xd7, + 0x9b, 0x9a, 0x48, 0x4b, 0x6c, 0x37, 0x6d, 0x72, 0x3b, 0x99, 0x8d, 0x2e, 0x1d, 0x43, 0x6, + 0xbf, 0x10, 0x33, 0xb5, 0xae, 0xf8, 0xcc, 0xa5, 0xcb, 0xb2, 0x56, 0x8b, 0x69, 0x24, 0x22, + 0x6d, 0x22, 0xa3, 0x58, 0xab, 0x7d, 0x87, 0xe4, 0xac, 0x5f, 0x2e, 0x9, 0x1a, 0xa7, 0x15, + 0x79, 0xf3, 0xa5, 0x69, 0x9, 0x49, 0x7d, 0x72, 0xf5, 0x4e, 0x6, 0xba, 0xc1, 0xc3, 0xb4, + 0x41, 0x3b, 0xba, 0x5e, 0xaf, 0x94, 0xc3, 0xb6, 0x4f, 0x34, 0xf9, 0xeb, 0xa4, 0x1a, 0xcb, + 0x6a, 0xe2, 0x83, 0x77, 0x6d, 0x36, 0x46, 0x53, 0x78, 0x48, 0xfe, 0xe8, 0x84, 0xbd, 0xdd, + 0xf5, 0xb1, 0xba, 0x57, 0x98, 0x54, 0xcf, 0xfd, 0xce, 0xba, 0xc3, 0x44, 0x5, 0x95, 0x27, + 0xe5, 0x6d, 0xd5, 0x98, 0xf8, 0xf5, 0x66, 0x71, 0x5a, 0xbe, 0x43, 0x1, 0xdd, 0x19, 0x11, + 0x30, 0xe6, 0xb9, 0xf0, 0xc6, 0x40, 0x39, 0x12, 0x53, 0xe2, 0x29, 0x80, 0x3f, 0x3a, 0xef, + 0x27, 0x4b, 0xed, 0xbf, 0xde, 0x3f, 0xcb, 0xbd, 0x42, 0xea, 0xd6, 0x79, + ]; + + const SAMPLE_CERT_CHAIN: [u8; 709] = [ + 0x81, 0x59, 0x2, 0xc1, 0x30, 0x82, 0x2, 0xbd, 0x30, 0x82, 0x1, 0xa5, 0xa0, 0x3, 0x2, 0x1, + 0x2, 0x2, 0x4, 0x18, 0xac, 0x46, 0xc0, 0x30, 0xd, 0x6, 0x9, 0x2a, 0x86, 0x48, 0x86, 0xf7, + 0xd, 0x1, 0x1, 0xb, 0x5, 0x0, 0x30, 0x2e, 0x31, 0x2c, 0x30, 0x2a, 0x6, 0x3, 0x55, 0x4, 0x3, + 0x13, 0x23, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x55, 0x32, 0x46, 0x20, 0x52, 0x6f, + 0x6f, 0x74, 0x20, 0x43, 0x41, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x20, 0x34, 0x35, + 0x37, 0x32, 0x30, 0x30, 0x36, 0x33, 0x31, 0x30, 0x20, 0x17, 0xd, 0x31, 0x34, 0x30, 0x38, + 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x18, 0xf, 0x32, 0x30, 0x35, 0x30, + 0x30, 0x39, 0x30, 0x34, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x30, 0x6e, 0x31, 0xb, + 0x30, 0x9, 0x6, 0x3, 0x55, 0x4, 0x6, 0x13, 0x2, 0x53, 0x45, 0x31, 0x12, 0x30, 0x10, 0x6, + 0x3, 0x55, 0x4, 0xa, 0xc, 0x9, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x41, 0x42, 0x31, + 0x22, 0x30, 0x20, 0x6, 0x3, 0x55, 0x4, 0xb, 0xc, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, + 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x31, 0x27, 0x30, 0x25, 0x6, 0x3, 0x55, 0x4, 0x3, 0xc, 0x1e, 0x59, + 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x55, 0x32, 0x46, 0x20, 0x45, 0x45, 0x20, 0x53, 0x65, + 0x72, 0x69, 0x61, 0x6c, 0x20, 0x34, 0x31, 0x33, 0x39, 0x34, 0x33, 0x34, 0x38, 0x38, 0x30, + 0x59, 0x30, 0x13, 0x6, 0x7, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x2, 0x1, 0x6, 0x8, 0x2a, 0x86, + 0x48, 0xce, 0x3d, 0x3, 0x1, 0x7, 0x3, 0x42, 0x0, 0x4, 0x79, 0xea, 0x3b, 0x2c, 0x7c, 0x49, + 0x70, 0x10, 0x62, 0x23, 0xc, 0xd2, 0x3f, 0xeb, 0x60, 0xe5, 0x29, 0x31, 0x71, 0xd4, 0x83, + 0xf1, 0x0, 0xbe, 0x85, 0x9d, 0x6b, 0xf, 0x83, 0x97, 0x3, 0x1, 0xb5, 0x46, 0xcd, 0xd4, 0x6e, + 0xcf, 0xca, 0xe3, 0xe3, 0xf3, 0xf, 0x81, 0xe9, 0xed, 0x62, 0xbd, 0x26, 0x8d, 0x4c, 0x1e, + 0xbd, 0x37, 0xb3, 0xbc, 0xbe, 0x92, 0xa8, 0xc2, 0xae, 0xeb, 0x4e, 0x3a, 0xa3, 0x6c, 0x30, + 0x6a, 0x30, 0x22, 0x6, 0x9, 0x2b, 0x6, 0x1, 0x4, 0x1, 0x82, 0xc4, 0xa, 0x2, 0x4, 0x15, + 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x31, 0x34, + 0x38, 0x32, 0x2e, 0x31, 0x2e, 0x37, 0x30, 0x13, 0x6, 0xb, 0x2b, 0x6, 0x1, 0x4, 0x1, 0x82, + 0xe5, 0x1c, 0x2, 0x1, 0x1, 0x4, 0x4, 0x3, 0x2, 0x5, 0x20, 0x30, 0x21, 0x6, 0xb, 0x2b, 0x6, + 0x1, 0x4, 0x1, 0x82, 0xe5, 0x1c, 0x1, 0x1, 0x4, 0x4, 0x12, 0x4, 0x10, 0xcb, 0x69, 0x48, + 0x1e, 0x8f, 0xf7, 0x40, 0x39, 0x93, 0xec, 0xa, 0x27, 0x29, 0xa1, 0x54, 0xa8, 0x30, 0xc, + 0x6, 0x3, 0x55, 0x1d, 0x13, 0x1, 0x1, 0xff, 0x4, 0x2, 0x30, 0x0, 0x30, 0xd, 0x6, 0x9, 0x2a, + 0x86, 0x48, 0x86, 0xf7, 0xd, 0x1, 0x1, 0xb, 0x5, 0x0, 0x3, 0x82, 0x1, 0x1, 0x0, 0x97, 0x9d, + 0x3, 0x97, 0xd8, 0x60, 0xf8, 0x2e, 0xe1, 0x5d, 0x31, 0x1c, 0x79, 0x6e, 0xba, 0xfb, 0x22, + 0xfa, 0xa7, 0xe0, 0x84, 0xd9, 0xba, 0xb4, 0xc6, 0x1b, 0xbb, 0x57, 0xf3, 0xe6, 0xb4, 0xc1, + 0x8a, 0x48, 0x37, 0xb8, 0x5c, 0x3c, 0x4e, 0xdb, 0xe4, 0x83, 0x43, 0xf4, 0xd6, 0xa5, 0xd9, + 0xb1, 0xce, 0xda, 0x8a, 0xe1, 0xfe, 0xd4, 0x91, 0x29, 0x21, 0x73, 0x5, 0x8e, 0x5e, 0xe1, + 0xcb, 0xdd, 0x6b, 0xda, 0xc0, 0x75, 0x57, 0xc6, 0xa0, 0xe8, 0xd3, 0x68, 0x25, 0xba, 0x15, + 0x9e, 0x7f, 0xb5, 0xad, 0x8c, 0xda, 0xf8, 0x4, 0x86, 0x8c, 0xf9, 0xe, 0x8f, 0x1f, 0x8a, + 0xea, 0x17, 0xc0, 0x16, 0xb5, 0x5c, 0x2a, 0x7a, 0xd4, 0x97, 0xc8, 0x94, 0xfb, 0x71, 0xd7, + 0x53, 0xd7, 0x9b, 0x9a, 0x48, 0x4b, 0x6c, 0x37, 0x6d, 0x72, 0x3b, 0x99, 0x8d, 0x2e, 0x1d, + 0x43, 0x6, 0xbf, 0x10, 0x33, 0xb5, 0xae, 0xf8, 0xcc, 0xa5, 0xcb, 0xb2, 0x56, 0x8b, 0x69, + 0x24, 0x22, 0x6d, 0x22, 0xa3, 0x58, 0xab, 0x7d, 0x87, 0xe4, 0xac, 0x5f, 0x2e, 0x9, 0x1a, + 0xa7, 0x15, 0x79, 0xf3, 0xa5, 0x69, 0x9, 0x49, 0x7d, 0x72, 0xf5, 0x4e, 0x6, 0xba, 0xc1, + 0xc3, 0xb4, 0x41, 0x3b, 0xba, 0x5e, 0xaf, 0x94, 0xc3, 0xb6, 0x4f, 0x34, 0xf9, 0xeb, 0xa4, + 0x1a, 0xcb, 0x6a, 0xe2, 0x83, 0x77, 0x6d, 0x36, 0x46, 0x53, 0x78, 0x48, 0xfe, 0xe8, 0x84, + 0xbd, 0xdd, 0xf5, 0xb1, 0xba, 0x57, 0x98, 0x54, 0xcf, 0xfd, 0xce, 0xba, 0xc3, 0x44, 0x5, + 0x95, 0x27, 0xe5, 0x6d, 0xd5, 0x98, 0xf8, 0xf5, 0x66, 0x71, 0x5a, 0xbe, 0x43, 0x1, 0xdd, + 0x19, 0x11, 0x30, 0xe6, 0xb9, 0xf0, 0xc6, 0x40, 0x39, 0x12, 0x53, 0xe2, 0x29, 0x80, 0x3f, + 0x3a, 0xef, 0x27, 0x4b, 0xed, 0xbf, 0xde, 0x3f, 0xcb, 0xbd, 0x42, 0xea, 0xd6, 0x79, + ]; + + const SAMPLE_AUTH_DATA_MAKE_CREDENTIAL: [u8; 164] = [ + 0x58, 0xA2, // bytes(162) + // authData + 0xc2, 0x89, 0xc5, 0xca, 0x9b, 0x04, 0x60, 0xf9, 0x34, 0x6a, 0xb4, 0xe4, 0x2d, 0x84, + 0x27, // rp_id_hash + 0x43, 0x40, 0x4d, 0x31, 0xf4, 0x84, 0x68, 0x25, 0xa6, 0xd0, 0x65, 0xbe, 0x59, 0x7a, + 0x87, // rp_id_hash + 0x05, 0x1d, // rp_id_hash + 0xC1, // authData Flags + 0x00, 0x00, 0x00, 0x0b, // authData counter + 0xf8, 0xa0, 0x11, 0xf3, 0x8c, 0x0a, 0x4d, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1f, 0x9e, 0xdc, + 0x7d, // AAGUID + 0x00, 0x10, // credential id length + 0x89, 0x59, 0xce, 0xad, 0x5b, 0x5c, 0x48, 0x16, 0x4e, 0x8a, 0xbc, 0xd6, 0xd9, 0x43, 0x5c, + 0x6f, // credential id + // credential public key + 0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20, 0xa5, 0xfd, 0x5c, 0xe1, 0xb1, + 0xc4, 0x58, 0xc5, 0x30, 0xa5, 0x4f, 0xa6, 0x1b, 0x31, 0xbf, 0x6b, 0x04, 0xbe, 0x8b, 0x97, + 0xaf, 0xde, 0x54, 0xdd, 0x8c, 0xbb, 0x69, 0x27, 0x5a, 0x8a, 0x1b, 0xe1, 0x22, 0x58, 0x20, + 0xfa, 0x3a, 0x32, 0x31, 0xdd, 0x9d, 0xee, 0xd9, 0xd1, 0x89, 0x7b, 0xe5, 0xa6, 0x22, 0x8c, + 0x59, 0x50, 0x1e, 0x4b, 0xcd, 0x12, 0x97, 0x5d, 0x3d, 0xff, 0x73, 0x0f, 0x01, 0x27, 0x8e, + 0xa6, 0x1c, // pub key end + // Extensions + 0xA1, // map(1) + 0x6B, // text(11) + 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // "hmac-secret" + 0xF5, // true + ]; + + const SAMPLE_AUTH_DATA_GET_ASSERTION: [u8; 229] = [ + 0x58, 0xE3, // bytes(227) + // authData + 0xc2, 0x89, 0xc5, 0xca, 0x9b, 0x04, 0x60, 0xf9, 0x34, 0x6a, 0xb4, 0xe4, 0x2d, 0x84, + 0x27, // rp_id_hash + 0x43, 0x40, 0x4d, 0x31, 0xf4, 0x84, 0x68, 0x25, 0xa6, 0xd0, 0x65, 0xbe, 0x59, 0x7a, + 0x87, // rp_id_hash + 0x05, 0x1d, // rp_id_hash + 0xC1, // authData Flags + 0x00, 0x00, 0x00, 0x0b, // authData counter + 0xf8, 0xa0, 0x11, 0xf3, 0x8c, 0x0a, 0x4d, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1f, 0x9e, 0xdc, + 0x7d, // AAGUID + 0x00, 0x10, // credential id length + 0x89, 0x59, 0xce, 0xad, 0x5b, 0x5c, 0x48, 0x16, 0x4e, 0x8a, 0xbc, 0xd6, 0xd9, 0x43, 0x5c, + 0x6f, // credential id + // credential public key + 0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20, 0xa5, 0xfd, 0x5c, 0xe1, 0xb1, + 0xc4, 0x58, 0xc5, 0x30, 0xa5, 0x4f, 0xa6, 0x1b, 0x31, 0xbf, 0x6b, 0x04, 0xbe, 0x8b, 0x97, + 0xaf, 0xde, 0x54, 0xdd, 0x8c, 0xbb, 0x69, 0x27, 0x5a, 0x8a, 0x1b, 0xe1, 0x22, 0x58, 0x20, + 0xfa, 0x3a, 0x32, 0x31, 0xdd, 0x9d, 0xee, 0xd9, 0xd1, 0x89, 0x7b, 0xe5, 0xa6, 0x22, 0x8c, + 0x59, 0x50, 0x1e, 0x4b, 0xcd, 0x12, 0x97, 0x5d, 0x3d, 0xff, 0x73, 0x0f, 0x01, 0x27, 0x8e, + 0xa6, 0x1c, // pub key end + // Extensions + 0xA1, // map(1) + 0x6B, // text(11) + 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // "hmac-secret" + 0x58, 0x40, // bytes(64) + 0x1F, 0x91, 0x52, 0x6C, 0xAE, 0x45, 0x6E, 0x4C, 0xBB, 0x71, 0xC4, 0xDD, 0xE7, 0xBB, 0x87, + 0x71, 0x57, 0xE6, 0xE5, 0x4D, 0xFE, 0xD3, 0x01, 0x5D, 0x7D, 0x4D, 0xBB, 0x22, 0x69, 0xAF, + 0xCD, 0xE6, 0xA9, 0x1B, 0x8D, 0x26, 0x7E, 0xBB, 0xF8, 0x48, 0xEB, 0x95, 0xA6, 0x8E, 0x79, + 0xC7, 0xAC, 0x70, 0x5E, 0x35, 0x1D, 0x54, 0x3D, 0xB0, 0x16, 0x58, 0x87, 0xD6, 0x29, 0x0F, + 0xD4, 0x7A, 0x40, 0xC4, + ]; + + #[test] + fn parse_cert_chain() { + let cert: AttestationCertificate = from_slice(&SAMPLE_CERT_CHAIN[1..]).unwrap(); + assert_eq!(&cert.0, &SAMPLE_CERT_CHAIN[4..]); + + let _cert: Vec<AttestationCertificate> = from_slice(&SAMPLE_CERT_CHAIN).unwrap(); + } + + #[test] + fn parse_attestation_object() { + let value: AttestationObject = from_slice(&SAMPLE_ATTESTATION).unwrap(); + println!("{:?}", value); + + //assert_eq!(true, false); + } + + #[test] + fn parse_reader() { + let v: Vec<u8> = vec![ + 0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72, 0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72, + ]; + let (rest, value): (&[u8], String) = from_slice_stream(&v).unwrap(); + assert_eq!(value, "foobar"); + assert_eq!(rest, &[0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72]); + let (rest, value): (&[u8], String) = from_slice_stream(rest).unwrap(); + assert_eq!(value, "foobar"); + assert!(rest.is_empty()); + } + + #[test] + fn parse_extensions() { + let auth_make: AuthenticatorData = from_slice(&SAMPLE_AUTH_DATA_MAKE_CREDENTIAL).unwrap(); + assert_eq!( + auth_make.extensions.hmac_secret, + Some(HmacSecretResponse::Confirmed(true)) + ); + let auth_get: AuthenticatorData = from_slice(&SAMPLE_AUTH_DATA_GET_ASSERTION).unwrap(); + assert_eq!( + auth_get.extensions.hmac_secret, + Some(HmacSecretResponse::Secret(vec![ + 0x1F, 0x91, 0x52, 0x6C, 0xAE, 0x45, 0x6E, 0x4C, 0xBB, 0x71, 0xC4, 0xDD, 0xE7, 0xBB, + 0x87, 0x71, 0x57, 0xE6, 0xE5, 0x4D, 0xFE, 0xD3, 0x01, 0x5D, 0x7D, 0x4D, 0xBB, 0x22, + 0x69, 0xAF, 0xCD, 0xE6, 0xA9, 0x1B, 0x8D, 0x26, 0x7E, 0xBB, 0xF8, 0x48, 0xEB, 0x95, + 0xA6, 0x8E, 0x79, 0xC7, 0xAC, 0x70, 0x5E, 0x35, 0x1D, 0x54, 0x3D, 0xB0, 0x16, 0x58, + 0x87, 0xD6, 0x29, 0x0F, 0xD4, 0x7A, 0x40, 0xC4, + ])) + ); + } + + /// See: https://github.com/mozilla/authenticator-rs/issues/187 + #[test] + fn test_aaguid_output() { + let input = [ + 0xcb, 0x69, 0x48, 0x1e, 0x8f, 0xf0, 0x00, 0x39, 0x93, 0xec, 0x0a, 0x27, 0x29, 0xa1, + 0x54, 0xa8, + ]; + let expected = "AAGuid(cb69481e-8ff0-0039-93ec-0a2729a154a8)"; + let result = AAGuid::from(&input).expect("Failed to parse AAGuid"); + let res_str = format!("{:?}", result); + assert_eq!(expected, &res_str); + } +} diff --git a/third_party/rust/authenticator/src/ctap2/client_data.rs b/third_party/rust/authenticator/src/ctap2/client_data.rs new file mode 100644 index 0000000000..6e214b17ff --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/client_data.rs @@ -0,0 +1,399 @@ +use super::commands::CommandError; +use crate::transport::errors::HIDError; +use serde::de::{self, Deserializer, Error as SerdeError, MapAccess, Visitor}; +use serde::ser::SerializeMap; +use serde::{Deserialize, Serialize, Serializer}; +use serde_json as json; +use sha2::{Digest, Sha256}; +use std::fmt; + +/// https://w3c.github.io/webauthn/#dom-collectedclientdata-tokenbinding +// tokenBinding, of type TokenBinding +// +// This OPTIONAL member contains information about the state of the Token +// Binding protocol [TokenBinding] used when communicating with the Relying +// Party. Its absence indicates that the client doesn’t support token +// binding. +// +// status, of type TokenBindingStatus +// +// This member is one of the following: +// +// supported +// +// Indicates the client supports token binding, but it was not +// negotiated when communicating with the Relying Party. +// +// present +// +// Indicates token binding was used when communicating with the +// Relying Party. In this case, the id member MUST be present. +// +// id, of type DOMString +// +// This member MUST be present if status is present, and MUST be a +// base64url encoding of the Token Binding ID that was used when +// communicating with the Relying Party. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TokenBinding { + Present(String), + Supported, +} + +impl Serialize for TokenBinding { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(2))?; + match *self { + TokenBinding::Supported => { + map.serialize_entry(&"status", &"supported")?; + } + TokenBinding::Present(ref v) => { + map.serialize_entry(&"status", "present")?; + // Verify here, that `v` is valid base64 encoded? + // base64::decode_config(&v, base64::URL_SAFE_NO_PAD); + // For now: Let the token do that. + map.serialize_entry(&"id", &v)?; + } + } + map.end() + } +} + +impl<'de> Deserialize<'de> for TokenBinding { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + struct TokenBindingVisitor; + + impl<'de> Visitor<'de> for TokenBindingVisitor { + type Value = TokenBinding; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a byte string") + } + + fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error> + where + M: MapAccess<'de>, + { + let mut id = None; + let mut status = None; + + while let Some(key) = map.next_key()? { + match key { + "status" => { + status = Some(map.next_value()?); + } + "id" => { + id = Some(map.next_value()?); + } + k => { + return Err(M::Error::custom(format!("unexpected key: {:?}", k))); + } + } + } + + if let Some(stat) = status { + match stat { + "present" => { + if let Some(id) = id { + Ok(TokenBinding::Present(id)) + } else { + Err(SerdeError::missing_field("id")) + } + } + "supported" => Ok(TokenBinding::Supported), + k => { + return Err(M::Error::custom(format!( + "unexpected status key: {:?}", + k + ))); + } + } + } else { + Err(SerdeError::missing_field("status")) + } + } + } + + deserializer.deserialize_map(TokenBindingVisitor) + } +} + +/// https://w3c.github.io/webauthn/#dom-collectedclientdata-type +// type, of type DOMString +// +// This member contains the string "webauthn.create" when creating new +// credentials, and "webauthn.get" when getting an assertion from an +// existing credential. The purpose of this member is to prevent certain +// types of signature confusion attacks (where an attacker substitutes one +// legitimate signature for another). +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum WebauthnType { + Create, + Get, +} + +impl Serialize for WebauthnType { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + match *self { + WebauthnType::Create => serializer.serialize_str(&"webauthn.create"), + WebauthnType::Get => serializer.serialize_str(&"webauthn.get"), + } + } +} + +impl<'de> Deserialize<'de> for WebauthnType { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + struct WebauthnTypeVisitor; + + impl<'de> Visitor<'de> for WebauthnTypeVisitor { + type Value = WebauthnType; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string") + } + + fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> + where + E: de::Error, + { + match v { + "webauthn.create" => Ok(WebauthnType::Create), + "webauthn.get" => Ok(WebauthnType::Get), + _ => Err(E::custom("unexpected webauthn_type")), + } + } + } + + deserializer.deserialize_str(WebauthnTypeVisitor) + } +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] +pub struct Challenge(pub String); + +impl Challenge { + pub fn new(input: Vec<u8>) -> Self { + let value = base64::encode_config(&input, base64::URL_SAFE_NO_PAD); + Challenge(value) + } +} + +impl From<Vec<u8>> for Challenge { + fn from(v: Vec<u8>) -> Challenge { + Challenge::new(v) + } +} + +impl AsRef<[u8]> for Challenge { + fn as_ref(&self) -> &[u8] { + self.0.as_bytes() + } +} + +pub type Origin = String; + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct CollectedClientData { + #[serde(rename = "type")] + pub webauthn_type: WebauthnType, + pub challenge: Challenge, + pub origin: Origin, + // It is optional, according to https://www.w3.org/TR/webauthn/#collectedclientdata-hash-of-the-serialized-client-data + // But we are serializing it, so we *have to* set crossOrigin (if not given, we have to set it to false) + // Thus, on our side, it is not optional. For deserializing, we provide a default (bool's default == False) + #[serde(rename = "crossOrigin", default)] + pub cross_origin: bool, + #[serde(rename = "tokenBinding", skip_serializing_if = "Option::is_none")] + pub token_binding: Option<TokenBinding>, +} + +#[derive(Debug, Eq, PartialEq)] +pub struct ClientDataHash([u8; 32]); + +impl PartialEq<[u8]> for ClientDataHash { + fn eq(&self, other: &[u8]) -> bool { + self.0.eq(other) + } +} + +impl AsRef<[u8]> for ClientDataHash { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl Serialize for ClientDataHash { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serializer.serialize_bytes(&self.0) + } +} + +#[cfg(test)] +impl<'de> Deserialize<'de> for ClientDataHash { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + struct ClientDataHashVisitor; + + impl<'de> Visitor<'de> for ClientDataHashVisitor { + type Value = ClientDataHash; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a byte string") + } + + fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E> + where + E: de::Error, + { + let mut out = [0u8; 32]; + if out.len() != v.len() { + return Err(E::invalid_length(v.len(), &"32")); + } + out.copy_from_slice(v); + Ok(ClientDataHash(out)) + } + } + + deserializer.deserialize_bytes(ClientDataHashVisitor) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CollectedClientDataWrapper { + pub client_data: CollectedClientData, + pub serialized_data: Vec<u8>, +} + +impl CollectedClientDataWrapper { + pub fn new(client_data: CollectedClientData) -> Result<Self, HIDError> { + let serialized_data = json::to_vec(&client_data).map_err(CommandError::Json)?; + Ok(CollectedClientDataWrapper { + client_data, + serialized_data, + }) + } + + pub fn hash(&self) -> ClientDataHash { + // WebIDL's dictionary definition specifies that the order of the struct + // is exactly as the WebIDL specification declares it, with an algorithm + // for partial dictionaries, so that's how interop works for these + // things. + // See: https://heycam.github.io/webidl/#dfn-dictionary + let mut hasher = Sha256::new(); + hasher.update(&self.serialized_data); + + let mut output = [0u8; 32]; + output.copy_from_slice(hasher.finalize().as_slice()); + + ClientDataHash(output) + } +} + +#[cfg(test)] +mod test { + use crate::CollectedClientDataWrapper; + + use super::{Challenge, ClientDataHash, CollectedClientData, TokenBinding, WebauthnType}; + use serde_json as json; + + #[test] + fn test_token_binding_status() { + let tok = TokenBinding::Present("AAECAw".to_string()); + + let json_value = json::to_string(&tok).unwrap(); + assert_eq!(json_value, "{\"status\":\"present\",\"id\":\"AAECAw\"}"); + + let tok = TokenBinding::Supported; + + let json_value = json::to_string(&tok).unwrap(); + assert_eq!(json_value, "{\"status\":\"supported\"}"); + } + + #[test] + fn test_webauthn_type() { + let t = WebauthnType::Create; + + let json_value = json::to_string(&t).unwrap(); + assert_eq!(json_value, "\"webauthn.create\""); + + let t = WebauthnType::Get; + let json_value = json::to_string(&t).unwrap(); + assert_eq!(json_value, "\"webauthn.get\""); + } + + #[test] + fn test_collected_client_data_parsing() { + let original_str = "{\"type\":\"webauthn.create\",\"challenge\":\"AAECAw\",\"origin\":\"example.com\",\"crossOrigin\":false,\"tokenBinding\":{\"status\":\"present\",\"id\":\"AAECAw\"}}"; + let parsed: CollectedClientData = serde_json::from_str(&original_str).unwrap(); + let expected = CollectedClientData { + webauthn_type: WebauthnType::Create, + challenge: Challenge::new(vec![0x00, 0x01, 0x02, 0x03]), + origin: String::from("example.com"), + cross_origin: false, + token_binding: Some(TokenBinding::Present("AAECAw".to_string())), + }; + assert_eq!(parsed, expected); + + let back_again = serde_json::to_string(&expected).unwrap(); + assert_eq!(back_again, original_str); + } + + #[test] + fn test_collected_client_data_defaults() { + let cross_origin_str = "{\"type\":\"webauthn.create\",\"challenge\":\"AAECAw\",\"origin\":\"example.com\",\"crossOrigin\":false,\"tokenBinding\":{\"status\":\"present\",\"id\":\"AAECAw\"}}"; + let no_cross_origin_str = "{\"type\":\"webauthn.create\",\"challenge\":\"AAECAw\",\"origin\":\"example.com\",\"tokenBinding\":{\"status\":\"present\",\"id\":\"AAECAw\"}}"; + let parsed: CollectedClientData = serde_json::from_str(&no_cross_origin_str).unwrap(); + let expected = CollectedClientData { + webauthn_type: WebauthnType::Create, + challenge: Challenge::new(vec![0x00, 0x01, 0x02, 0x03]), + origin: String::from("example.com"), + cross_origin: false, + token_binding: Some(TokenBinding::Present("AAECAw".to_string())), + }; + assert_eq!(parsed, expected); + + let back_again = serde_json::to_string(&expected).unwrap(); + assert_eq!(back_again, cross_origin_str); + } + + #[test] + fn test_collected_client_data() { + let client_data = CollectedClientData { + webauthn_type: WebauthnType::Create, + challenge: Challenge::new(vec![0x00, 0x01, 0x02, 0x03]), + origin: String::from("example.com"), + cross_origin: false, + token_binding: Some(TokenBinding::Present("AAECAw".to_string())), + }; + let c = + CollectedClientDataWrapper::new(client_data).expect("Failed to serialize client_data"); + assert_eq!( + c.hash(), + // echo -n '{"type":"webauthn.create","challenge":"AAECAw","origin":"example.com","crossOrigin":false,"tokenBinding":{"status":"present","id":"AAECAw"}}' | sha256sum -t + ClientDataHash { + 0: [ + 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, + 0x32, 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, + 0x10, 0x87, 0x54, 0xc3, 0x2d, 0x80 + ] + } + ); + } +} diff --git a/third_party/rust/authenticator/src/ctap2/commands/client_pin.rs b/third_party/rust/authenticator/src/ctap2/commands/client_pin.rs new file mode 100644 index 0000000000..c70ce73cb0 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/client_pin.rs @@ -0,0 +1,786 @@ +use super::{get_info::AuthenticatorInfo, Command, CommandError, RequestCtap2, StatusCode}; +use crate::crypto::{ + authenticate, decrypt, encapsulate, encrypt, BackendError, COSEKey, CryptoError, ECDHSecret, +}; +use crate::transport::errors::HIDError; +use crate::u2ftypes::U2FDevice; +use serde::{ + de::{Error as SerdeError, IgnoredAny, MapAccess, Visitor}, + ser::SerializeMap, + Deserialize, Deserializer, Serialize, Serializer, +}; +use serde_bytes::ByteBuf; +use serde_cbor::de::from_slice; +use serde_cbor::ser::to_vec; +use serde_cbor::Value; +use sha2::{Digest, Sha256}; +use std::error::Error as StdErrorT; +use std::fmt; + +#[derive(Debug, Copy, Clone)] +#[repr(u8)] +pub enum PINSubcommand { + GetRetries = 0x01, + GetKeyAgreement = 0x02, + SetPIN = 0x03, + ChangePIN = 0x04, + GetPINToken = 0x05, // superseded by GetPinUvAuth* + GetPinUvAuthTokenUsingUvWithPermissions = 0x06, + GetUVRetries = 0x07, + GetPinUvAuthTokenUsingPinWithPermissions = 0x09, // Yes, 0x08 is missing +} + +#[derive(Debug, Copy, Clone)] +#[repr(u8)] +pub enum PinUvAuthTokenPermission { + MakeCredential = 0x01, // rp_id required + GetAssertion = 0x02, // rp_id required + CredentialManagement = 0x04, // rp_id optional + BioEnrollment = 0x08, // rp_id ignored + LargeBlobWrite = 0x10, // rp_id ignored + AuthenticatorConfiguration = 0x20, // rp_id ignored +} + +#[derive(Debug)] +pub struct ClientPIN { + pin_protocol: Option<u8>, + subcommand: PINSubcommand, + key_agreement: Option<COSEKey>, + pin_auth: Option<PinAuth>, + new_pin_enc: Option<ByteBuf>, + pin_hash_enc: Option<ByteBuf>, + permissions: Option<u8>, + rp_id: Option<String>, +} + +impl Default for ClientPIN { + fn default() -> Self { + ClientPIN { + pin_protocol: None, + subcommand: PINSubcommand::GetRetries, + key_agreement: None, + pin_auth: None, + new_pin_enc: None, + pin_hash_enc: None, + permissions: None, + rp_id: None, + } + } +} + +impl Serialize for ClientPIN { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + // Need to define how many elements are going to be in the map + // beforehand + let mut map_len = 1; + if self.pin_protocol.is_some() { + map_len += 1; + } + if self.key_agreement.is_some() { + map_len += 1; + } + if self.pin_auth.is_some() { + map_len += 1; + } + if self.new_pin_enc.is_some() { + map_len += 1; + } + if self.pin_hash_enc.is_some() { + map_len += 1; + } + if self.permissions.is_some() { + map_len += 1; + } + if self.rp_id.is_some() { + map_len += 1; + } + + let mut map = serializer.serialize_map(Some(map_len))?; + if let Some(ref pin_protocol) = self.pin_protocol { + map.serialize_entry(&1, pin_protocol)?; + } + let command: u8 = self.subcommand as u8; + map.serialize_entry(&2, &command)?; + if let Some(ref key_agreement) = self.key_agreement { + map.serialize_entry(&3, key_agreement)?; + } + if let Some(ref pin_auth) = self.pin_auth { + map.serialize_entry(&4, &ByteBuf::from(pin_auth.as_ref()))?; + } + if let Some(ref new_pin_enc) = self.new_pin_enc { + map.serialize_entry(&5, new_pin_enc)?; + } + if let Some(ref pin_hash_enc) = self.pin_hash_enc { + map.serialize_entry(&6, pin_hash_enc)?; + } + if let Some(ref permissions) = self.permissions { + map.serialize_entry(&9, permissions)?; + } + if let Some(ref rp_id) = self.rp_id { + map.serialize_entry(&0x0A, rp_id)?; + } + + map.end() + } +} + +pub trait ClientPINSubCommand { + type Output; + fn as_client_pin(&self) -> Result<ClientPIN, CommandError>; + fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError>; +} + +struct ClientPinResponse { + key_agreement: Option<COSEKey>, + pin_token: Option<EncryptedPinToken>, + /// Number of PIN attempts remaining before lockout. + pin_retries: Option<u8>, + power_cycle_state: Option<bool>, + uv_retries: Option<u8>, +} + +impl<'de> Deserialize<'de> for ClientPinResponse { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + struct ClientPinResponseVisitor; + + impl<'de> Visitor<'de> for ClientPinResponseVisitor { + type Value = ClientPinResponse; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map") + } + + fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error> + where + M: MapAccess<'de>, + { + let mut key_agreement = None; + let mut pin_token = None; + let mut pin_retries = None; + let mut power_cycle_state = None; + let mut uv_retries = None; + while let Some(key) = map.next_key()? { + match key { + 0x01 => { + if key_agreement.is_some() { + return Err(SerdeError::duplicate_field("key_agreement")); + } + key_agreement = map.next_value()?; + } + 0x02 => { + if pin_token.is_some() { + return Err(SerdeError::duplicate_field("pin_token")); + } + pin_token = map.next_value()?; + } + 0x03 => { + if pin_retries.is_some() { + return Err(SerdeError::duplicate_field("pin_retries")); + } + pin_retries = Some(map.next_value()?); + } + 0x04 => { + if power_cycle_state.is_some() { + return Err(SerdeError::duplicate_field("power_cycle_state")); + } + power_cycle_state = Some(map.next_value()?); + } + 0x05 => { + if uv_retries.is_some() { + return Err(SerdeError::duplicate_field("uv_retries")); + } + uv_retries = Some(map.next_value()?); + } + k => { + warn!("ClientPinResponse: unexpected key: {:?}", k); + let _ = map.next_value::<IgnoredAny>()?; + continue; + } + } + } + Ok(ClientPinResponse { + key_agreement, + pin_token, + pin_retries, + power_cycle_state, + uv_retries, + }) + } + } + deserializer.deserialize_bytes(ClientPinResponseVisitor) + } +} + +#[derive(Debug)] +pub struct GetKeyAgreement { + pin_protocol: u8, +} + +impl GetKeyAgreement { + pub fn new(info: &AuthenticatorInfo) -> Result<Self, CommandError> { + if info.pin_protocols.contains(&1) { + Ok(GetKeyAgreement { pin_protocol: 1 }) + } else { + Err(CommandError::UnsupportedPinProtocol) + } + } +} + +impl ClientPINSubCommand for GetKeyAgreement { + type Output = KeyAgreement; + + fn as_client_pin(&self) -> Result<ClientPIN, CommandError> { + Ok(ClientPIN { + pin_protocol: Some(self.pin_protocol), + subcommand: PINSubcommand::GetKeyAgreement, + ..ClientPIN::default() + }) + } + + fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> { + let value: Value = from_slice(input).map_err(CommandError::Deserializing)?; + debug!("GetKeyAgreement::parse_response_payload {:?}", value); + + let get_pin_response: ClientPinResponse = + from_slice(input).map_err(CommandError::Deserializing)?; + if let Some(key_agreement) = get_pin_response.key_agreement { + Ok(KeyAgreement(key_agreement)) + } else { + Err(CommandError::MissingRequiredField("key_agreement")) + } + } +} + +#[derive(Debug)] +/// Superseded by GetPinUvAuthTokenUsingUvWithPermissions or GetPinUvAuthTokenUsingPinWithPermissions, +/// thus for backwards compatibility only +pub struct GetPinToken<'sc, 'pin> { + pin_protocol: u8, + shared_secret: &'sc ECDHSecret, + pin: &'pin Pin, +} + +impl<'sc, 'pin> GetPinToken<'sc, 'pin> { + pub fn new( + info: &AuthenticatorInfo, + shared_secret: &'sc ECDHSecret, + pin: &'pin Pin, + ) -> Result<Self, CommandError> { + if info.pin_protocols.contains(&1) { + Ok(GetPinToken { + pin_protocol: 1, + shared_secret, + pin, + }) + } else { + Err(CommandError::UnsupportedPinProtocol) + } + } +} + +impl<'sc, 'pin> ClientPINSubCommand for GetPinToken<'sc, 'pin> { + type Output = PinToken; + + fn as_client_pin(&self) -> Result<ClientPIN, CommandError> { + let input = self.pin.for_pin_token(); + trace!("pin_hash = {:#04X?}", &input.as_ref()); + let pin_hash_enc = encrypt(self.shared_secret.shared_secret(), input.as_ref()) + .map_err(|e| CryptoError::Backend(e))?; + trace!("pin_hash_enc = {:#04X?}", &pin_hash_enc); + + Ok(ClientPIN { + pin_protocol: Some(self.pin_protocol), + subcommand: PINSubcommand::GetPINToken, + key_agreement: Some(self.shared_secret.my_public_key().clone()), + pin_hash_enc: Some(ByteBuf::from(pin_hash_enc)), + ..ClientPIN::default() + }) + } + + fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> { + let value: Value = from_slice(input).map_err(CommandError::Deserializing)?; + debug!("GetKeyAgreement::parse_response_payload {:?}", value); + + let get_pin_response: ClientPinResponse = + from_slice(input).map_err(CommandError::Deserializing)?; + match get_pin_response.pin_token { + Some(encrypted_pin_token) => { + let pin_token = decrypt( + self.shared_secret.shared_secret(), + encrypted_pin_token.as_ref(), + ) + .map_err(|e| CryptoError::Backend(e))?; + let pin_token = PinToken(pin_token); + Ok(pin_token) + } + None => Err(CommandError::MissingRequiredField("key_agreement")), + } + } +} + +#[derive(Debug)] +pub struct GetPinUvAuthTokenUsingPinWithPermissions<'sc, 'pin> { + pin_protocol: u8, + shared_secret: &'sc ECDHSecret, + pin: &'pin Pin, + permissions: PinUvAuthTokenPermission, + rp_id: Option<String>, +} + +impl<'sc, 'pin> GetPinUvAuthTokenUsingPinWithPermissions<'sc, 'pin> { + pub fn new( + info: &AuthenticatorInfo, + shared_secret: &'sc ECDHSecret, + pin: &'pin Pin, + permissions: PinUvAuthTokenPermission, + rp_id: Option<String>, + ) -> Result<Self, CommandError> { + // TODO(MS): Actually handle protocol 2! + if info.pin_protocols.contains(&1) { + Ok(GetPinUvAuthTokenUsingPinWithPermissions { + pin_protocol: 1, + shared_secret, + pin, + permissions, + rp_id, + }) + } else { + Err(CommandError::UnsupportedPinProtocol) + } + } +} + +impl<'sc, 'pin> ClientPINSubCommand for GetPinUvAuthTokenUsingPinWithPermissions<'sc, 'pin> { + type Output = PinToken; + + fn as_client_pin(&self) -> Result<ClientPIN, CommandError> { + let input = self.pin.for_pin_token(); + let pin_hash_enc = encrypt(self.shared_secret.shared_secret(), input.as_ref()) + .map_err(|e| CryptoError::Backend(e))?; + + Ok(ClientPIN { + pin_protocol: Some(self.pin_protocol), + subcommand: PINSubcommand::GetPinUvAuthTokenUsingPinWithPermissions, + key_agreement: Some(self.shared_secret.my_public_key().clone()), + pin_hash_enc: Some(ByteBuf::from(pin_hash_enc)), + permissions: Some(self.permissions as u8), + rp_id: self.rp_id.clone(), // TODO: This could probably be done less wasteful with &str all the way + ..ClientPIN::default() + }) + } + + fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> { + let value: Value = from_slice(input).map_err(CommandError::Deserializing)?; + debug!("GetKeyAgreement::parse_response_payload {:?}", value); + + let get_pin_response: ClientPinResponse = + from_slice(input).map_err(CommandError::Deserializing)?; + match get_pin_response.pin_token { + Some(encrypted_pin_token) => { + let pin_token = decrypt( + self.shared_secret.shared_secret(), + encrypted_pin_token.as_ref(), + ) + .map_err(|e| CryptoError::Backend(e))?; + let pin_token = PinToken(pin_token); + Ok(pin_token) + } + None => Err(CommandError::MissingRequiredField("key_agreement")), + } + } +} + +#[derive(Debug)] +pub struct GetRetries {} + +impl GetRetries { + pub fn new() -> Self { + GetRetries {} + } +} + +impl ClientPINSubCommand for GetRetries { + type Output = u8; + + fn as_client_pin(&self) -> Result<ClientPIN, CommandError> { + Ok(ClientPIN { + subcommand: PINSubcommand::GetRetries, + ..ClientPIN::default() + }) + } + + fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> { + let value: Value = from_slice(input).map_err(CommandError::Deserializing)?; + debug!("GetKeyAgreement::parse_response_payload {:?}", value); + + let get_pin_response: ClientPinResponse = + from_slice(input).map_err(CommandError::Deserializing)?; + match get_pin_response.pin_retries { + Some(pin_retries) => Ok(pin_retries), + None => Err(CommandError::MissingRequiredField("pin_retries")), + } + } +} + +#[derive(Debug)] +pub struct SetNewPin<'sc, 'pin> { + pin_protocol: u8, + shared_secret: &'sc ECDHSecret, + new_pin: &'pin Pin, +} + +impl<'sc, 'pin> SetNewPin<'sc, 'pin> { + pub fn new( + info: &AuthenticatorInfo, + shared_secret: &'sc ECDHSecret, + new_pin: &'pin Pin, + ) -> Result<Self, CommandError> { + if info.pin_protocols.contains(&1) { + Ok(SetNewPin { + pin_protocol: 1, + shared_secret, + new_pin, + }) + } else { + Err(CommandError::UnsupportedPinProtocol) + } + } +} + +impl<'sc, 'pin> ClientPINSubCommand for SetNewPin<'sc, 'pin> { + type Output = (); + + fn as_client_pin(&self) -> Result<ClientPIN, CommandError> { + if self.new_pin.as_bytes().len() > 63 { + return Err(CommandError::StatusCode( + StatusCode::PinPolicyViolation, + None, + )); + } + // Padding the PIN with trailing zeros, according to spec + let input: Vec<u8> = self + .new_pin + .as_bytes() + .iter() + .chain(std::iter::repeat(&0x00)) + .take(64) + .cloned() + .collect(); + + let shared_secret = self.shared_secret.shared_secret(); + // AES256-CBC(sharedSecret, IV=0, newPin) + let new_pin_enc = + encrypt(shared_secret, input.as_ref()).map_err(|e| CryptoError::Backend(e))?; + + // LEFT(HMAC-SHA-265(sharedSecret, newPinEnc), 16) + let pin_auth = PinToken(shared_secret.to_vec()) + .auth(&new_pin_enc) + .map_err(CommandError::Crypto)?; + + Ok(ClientPIN { + pin_protocol: Some(self.pin_protocol), + subcommand: PINSubcommand::SetPIN, + key_agreement: Some(self.shared_secret.my_public_key().clone()), + new_pin_enc: Some(ByteBuf::from(new_pin_enc)), + pin_auth: Some(pin_auth), + ..ClientPIN::default() + }) + } + + fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> { + // Should be an empty response or a valid cbor-value (which we ignore) + if input.is_empty() { + Ok(()) + } else { + let _: Value = from_slice(input).map_err(CommandError::Deserializing)?; + Ok(()) + } + } +} + +#[derive(Debug)] +pub struct ChangeExistingPin<'sc, 'pin> { + pin_protocol: u8, + shared_secret: &'sc ECDHSecret, + current_pin: &'pin Pin, + new_pin: &'pin Pin, +} + +impl<'sc, 'pin> ChangeExistingPin<'sc, 'pin> { + pub fn new( + info: &AuthenticatorInfo, + shared_secret: &'sc ECDHSecret, + current_pin: &'pin Pin, + new_pin: &'pin Pin, + ) -> Result<Self, CommandError> { + if info.pin_protocols.contains(&1) { + Ok(ChangeExistingPin { + pin_protocol: 1, + shared_secret, + current_pin, + new_pin, + }) + } else { + Err(CommandError::UnsupportedPinProtocol) + } + } +} + +impl<'sc, 'pin> ClientPINSubCommand for ChangeExistingPin<'sc, 'pin> { + type Output = (); + + fn as_client_pin(&self) -> Result<ClientPIN, CommandError> { + if self.new_pin.as_bytes().len() > 63 { + return Err(CommandError::StatusCode( + StatusCode::PinPolicyViolation, + None, + )); + } + // Padding the PIN with trailing zeros, according to spec + let input: Vec<u8> = self + .new_pin + .as_bytes() + .iter() + .chain(std::iter::repeat(&0x00)) + .take(64) + .cloned() + .collect(); + + let shared_secret = self.shared_secret.shared_secret(); + // AES256-CBC(sharedSecret, IV=0, newPin) + let new_pin_enc = + encrypt(shared_secret, input.as_ref()).map_err(|e| CryptoError::Backend(e))?; + + // AES256-CBC(sharedSecret, IV=0, LEFT(SHA-256(oldPin), 16)) + let input = self.current_pin.for_pin_token(); + let pin_hash_enc = encrypt(self.shared_secret.shared_secret(), input.as_ref()) + .map_err(|e| CryptoError::Backend(e))?; + + // LEFT(HMAC-SHA-265(sharedSecret, newPinEnc), 16) + let pin_auth = PinToken(shared_secret.to_vec()) + .auth(&[new_pin_enc.as_slice(), pin_hash_enc.as_slice()].concat()) + .map_err(CommandError::Crypto)?; + + Ok(ClientPIN { + pin_protocol: Some(self.pin_protocol), + subcommand: PINSubcommand::ChangePIN, + key_agreement: Some(self.shared_secret.my_public_key().clone()), + new_pin_enc: Some(ByteBuf::from(new_pin_enc)), + pin_hash_enc: Some(ByteBuf::from(pin_hash_enc)), + pin_auth: Some(pin_auth), + permissions: None, + rp_id: None, + }) + } + + fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> { + // Should be an empty response or a valid cbor-value (which we ignore) + if input.is_empty() { + Ok(()) + } else { + let _: Value = from_slice(input).map_err(CommandError::Deserializing)?; + Ok(()) + } + } +} + +impl<T> RequestCtap2 for T +where + T: ClientPINSubCommand, + T: fmt::Debug, +{ + type Output = <T as ClientPINSubCommand>::Output; + + fn command() -> Command { + Command::ClientPin + } + + fn wire_format<Dev>(&self, _dev: &mut Dev) -> Result<Vec<u8>, HIDError> + where + Dev: U2FDevice, + { + let client_pin = self.as_client_pin()?; + let output = to_vec(&client_pin).map_err(CommandError::Serializing)?; + trace!("client subcommmand: {:04X?}", &output); + + Ok(output) + } + + fn handle_response_ctap2<Dev>( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result<Self::Output, HIDError> + where + Dev: U2FDevice, + { + trace!("Client pin subcomand response:{:04X?}", &input); + if input.is_empty() { + return Err(CommandError::InputTooSmall.into()); + } + + let status: StatusCode = input[0].into(); + debug!("response status code: {:?}", status); + if status.is_ok() { + let res = <T as ClientPINSubCommand>::parse_response_payload(self, &input[1..]) + .map_err(HIDError::Command); + res + } else { + let add_data = if input.len() > 1 { + let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + Some(data) + } else { + None + }; + Err(CommandError::StatusCode(status, add_data).into()) + } + } +} + +#[derive(Debug)] +pub struct KeyAgreement(COSEKey); + +impl KeyAgreement { + pub fn shared_secret(&self) -> Result<ECDHSecret, CommandError> { + encapsulate(&self.0).map_err(|e| CommandError::Crypto(CryptoError::Backend(e))) + } +} + +#[derive(Debug, Deserialize)] +pub struct EncryptedPinToken(ByteBuf); + +impl AsRef<[u8]> for EncryptedPinToken { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +#[derive(Debug)] +pub struct PinToken(Vec<u8>); + +impl PinToken { + pub fn auth(&self, payload: &[u8]) -> Result<PinAuth, CryptoError> { + let hmac = authenticate(self.as_ref(), payload)?; + + let mut out = [0u8; 16]; + out.copy_from_slice(&hmac[0..16]); + + Ok(PinAuth(out.to_vec())) + } +} + +impl AsRef<[u8]> for PinToken { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +#[derive(Debug, Clone)] +#[cfg_attr(test, derive(Deserialize))] +pub struct PinAuth(Vec<u8>); + +impl PinAuth { + pub(crate) fn empty_pin_auth() -> Self { + PinAuth(vec![]) + } +} + +impl AsRef<[u8]> for PinAuth { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl Serialize for PinAuth { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serde_bytes::serialize(&self.0[..], serializer) + } +} + +#[derive(Clone)] +pub struct Pin(String); + +impl fmt::Debug for Pin { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Pin(redacted)") + } +} + +impl Pin { + pub fn new(value: &str) -> Pin { + Pin(String::from(value)) + } + + pub fn for_pin_token(&self) -> PinAuth { + let mut hasher = Sha256::new(); + hasher.update(&self.0.as_bytes()); + + let mut output = [0u8; 16]; + let len = output.len(); + output.copy_from_slice(&hasher.finalize().as_slice()[..len]); + + PinAuth(output.to_vec()) + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[derive(Clone, Debug, Serialize)] +pub enum PinError { + PinRequired, + PinIsTooShort, + PinIsTooLong(usize), + InvalidKeyLen, + InvalidPin(Option<u8>), + PinAuthBlocked, + PinBlocked, + PinNotSet, + Backend(BackendError), +} + +impl fmt::Display for PinError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + PinError::PinRequired => write!(f, "PinError: Pin required."), + PinError::PinIsTooShort => write!(f, "PinError: pin is too short"), + PinError::PinIsTooLong(len) => write!(f, "PinError: pin is too long ({})", len), + PinError::InvalidKeyLen => write!(f, "PinError: invalid key len"), + PinError::InvalidPin(ref e) => { + let mut res = write!(f, "PinError: Invalid Pin."); + if let Some(pin_retries) = e { + res = write!(f, " Retries left: {:?}", pin_retries) + } + res + } + PinError::PinAuthBlocked => write!( + f, + "PinError: Pin authentication blocked. Device needs power cycle." + ), + PinError::PinBlocked => write!( + f, + "PinError: No retries left. Pin blocked. Device needs reset." + ), + PinError::PinNotSet => write!(f, "PinError: Pin needed but not set on device."), + PinError::Backend(ref e) => write!(f, "PinError: Crypto backend error: {:?}", e), + } + } +} + +impl StdErrorT for PinError {} + +impl From<BackendError> for PinError { + fn from(e: BackendError) -> Self { + PinError::Backend(e) + } +} diff --git a/third_party/rust/authenticator/src/ctap2/commands/get_assertion.rs b/third_party/rust/authenticator/src/ctap2/commands/get_assertion.rs new file mode 100644 index 0000000000..16704f0ba8 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/get_assertion.rs @@ -0,0 +1,1282 @@ +use super::{ + Command, CommandError, PinAuthCommand, Request, RequestCtap1, RequestCtap2, Retryable, + StatusCode, +}; +use crate::consts::{ + PARAMETER_SIZE, U2F_AUTHENTICATE, U2F_CHECK_IS_REGISTERED, U2F_REQUEST_USER_PRESENCE, +}; +use crate::crypto::{authenticate, encrypt, COSEKey, CryptoError, ECDHSecret}; +use crate::ctap2::attestation::{AuthenticatorData, AuthenticatorDataFlags}; +use crate::ctap2::client_data::{ClientDataHash, CollectedClientData, CollectedClientDataWrapper}; +use crate::ctap2::commands::client_pin::{Pin, PinAuth}; +use crate::ctap2::commands::get_next_assertion::GetNextAssertion; +use crate::ctap2::commands::make_credentials::UserVerification; +use crate::ctap2::server::{PublicKeyCredentialDescriptor, RelyingPartyWrapper, User}; +use crate::errors::AuthenticatorError; +use crate::transport::errors::{ApduErrorStatus, HIDError}; +use crate::transport::FidoDevice; +use crate::u2ftypes::{CTAP1RequestAPDU, U2FDevice}; +use nom::{ + error::VerboseError, + number::complete::{be_u32, be_u8}, + sequence::tuple, +}; +use serde::{ + de::{Error as DesError, MapAccess, Visitor}, + ser::{Error as SerError, SerializeMap}, + Deserialize, Deserializer, Serialize, Serializer, +}; +use serde_bytes::ByteBuf; +use serde_cbor::{de::from_slice, ser, Value}; +use std::convert::TryInto; +use std::fmt; +use std::io; + +#[derive(Debug, PartialEq)] +pub enum GetAssertionResult { + CTAP1(Vec<u8>), + CTAP2(AssertionObject, CollectedClientDataWrapper), +} + +#[derive(Clone, Copy, Debug, Serialize)] +#[cfg_attr(test, derive(Deserialize))] +pub struct GetAssertionOptions { + #[serde(rename = "uv", skip_serializing_if = "Option::is_none")] + pub user_verification: Option<bool>, + #[serde(rename = "up", skip_serializing_if = "Option::is_none")] + pub user_presence: Option<bool>, +} + +impl Default for GetAssertionOptions { + fn default() -> Self { + Self { + user_presence: Some(true), + user_verification: None, + } + } +} + +impl GetAssertionOptions { + pub(crate) fn has_some(&self) -> bool { + self.user_presence.is_some() || self.user_verification.is_some() + } +} + +impl UserVerification for GetAssertionOptions { + fn ask_user_verification(&self) -> bool { + if let Some(e) = self.user_verification { + e + } else { + false + } + } +} + +#[derive(Debug, Clone)] +pub struct CalculatedHmacSecretExtension { + pub public_key: COSEKey, + pub salt_enc: Vec<u8>, + pub salt_auth: [u8; 16], +} + +#[derive(Debug, Clone, Default)] +pub struct HmacSecretExtension { + pub salt1: Vec<u8>, + pub salt2: Option<Vec<u8>>, + calculated_hmac: Option<CalculatedHmacSecretExtension>, +} + +impl HmacSecretExtension { + pub fn new(salt1: Vec<u8>, salt2: Option<Vec<u8>>) -> Self { + HmacSecretExtension { + salt1, + salt2, + calculated_hmac: None, + } + } + + pub fn calculate(&mut self, secret: &ECDHSecret) -> Result<(), AuthenticatorError> { + if self.salt1.len() < 32 { + return Err(CryptoError::WrongSaltLength.into()); + } + let salt_enc = match &self.salt2 { + Some(salt2) => { + if salt2.len() < 32 { + return Err(CryptoError::WrongSaltLength.into()); + } + let salts = [&self.salt1[..32], &salt2[..32]].concat(); // salt1 || salt2 + encrypt(secret.shared_secret(), &salts) + } + None => encrypt(secret.shared_secret(), &self.salt1[..32]), + } + .map_err(|e| CryptoError::Backend(e))?; + let salt_auth_full = + authenticate(secret.shared_secret(), &salt_enc).map_err(|e| CryptoError::Backend(e))?; + let salt_auth = salt_auth_full + .windows(16) + .next() + .ok_or(AuthenticatorError::InternalError(String::from( + "salt_auth too short", + )))? + .try_into() + .map_err(|_| { + AuthenticatorError::InternalError(String::from( + "salt_auth conversion failed. Should never happen.", + )) + })?; + let public_key = secret.my_public_key().clone(); + self.calculated_hmac = Some(CalculatedHmacSecretExtension { + public_key, + salt_enc, + salt_auth, + }); + + Ok(()) + } +} + +impl Serialize for HmacSecretExtension { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + if let Some(calc) = &self.calculated_hmac { + let mut map = serializer.serialize_map(Some(3))?; + map.serialize_entry(&1, &calc.public_key)?; + map.serialize_entry(&2, serde_bytes::Bytes::new(&calc.salt_enc))?; + map.serialize_entry(&3, serde_bytes::Bytes::new(&calc.salt_auth))?; + map.end() + } else { + Err(SerError::custom( + "hmac secret has not been calculated before being serialized", + )) + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct GetAssertionExtensions { + pub hmac_secret: Option<HmacSecretExtension>, +} + +impl Serialize for GetAssertionExtensions { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry(&"hmac-secret", &self.hmac_secret)?; + map.end() + } +} + +impl GetAssertionExtensions { + fn has_extensions(&self) -> bool { + self.hmac_secret.is_some() + } +} + +#[derive(Debug, Clone)] +pub struct GetAssertion { + pub(crate) client_data_wrapper: CollectedClientDataWrapper, + pub(crate) rp: RelyingPartyWrapper, + pub(crate) allow_list: Vec<PublicKeyCredentialDescriptor>, + + // https://www.w3.org/TR/webauthn/#client-extension-input + // The client extension input, which is a value that can be encoded in JSON, + // is passed from the WebAuthn Relying Party to the client in the get() or + // create() call, while the CBOR authenticator extension input is passed + // from the client to the authenticator for authenticator extensions during + // the processing of these calls. + pub(crate) extensions: GetAssertionExtensions, + pub(crate) options: GetAssertionOptions, + pub(crate) pin: Option<Pin>, + pub(crate) pin_auth: Option<PinAuth>, + //TODO(MS): pinProtocol +} + +impl GetAssertion { + pub fn new( + client_data_wrapper: CollectedClientData, + rp: RelyingPartyWrapper, + allow_list: Vec<PublicKeyCredentialDescriptor>, + options: GetAssertionOptions, + extensions: GetAssertionExtensions, + pin: Option<Pin>, + ) -> Result<Self, HIDError> { + let client_data_wrapper = CollectedClientDataWrapper::new(client_data_wrapper)?; + Ok(Self { + client_data_wrapper, + rp, + allow_list, + extensions, + options, + pin, + pin_auth: None, + }) + } +} + +impl PinAuthCommand for GetAssertion { + fn pin(&self) -> &Option<Pin> { + &self.pin + } + + fn set_pin(&mut self, pin: Option<Pin>) { + self.pin = pin; + } + + fn pin_auth(&self) -> &Option<PinAuth> { + &self.pin_auth + } + + fn set_pin_auth(&mut self, pin_auth: Option<PinAuth>, _pint_auth_protocol: Option<u64>) { + self.pin_auth = pin_auth; + } + + fn client_data_hash(&self) -> ClientDataHash { + self.client_data_wrapper.hash() + } + + fn unset_uv_option(&mut self) { + self.options.user_verification = None; + } +} + +impl Serialize for GetAssertion { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + // Need to define how many elements are going to be in the map + // beforehand + let mut map_len = 2; + if !self.allow_list.is_empty() { + map_len += 1; + } + if self.extensions.has_extensions() { + map_len += 1; + } + if self.options.has_some() { + map_len += 1; + } + if self.pin_auth.is_some() { + map_len += 2; + } + + let mut map = serializer.serialize_map(Some(map_len))?; + match self.rp { + RelyingPartyWrapper::Data(ref d) => { + map.serialize_entry(&1, &d.id)?; + } + _ => { + return Err(S::Error::custom( + "Can't serialize a RelyingParty::Hash for CTAP2", + )); + } + } + + let client_data_hash = self.client_data_hash(); + map.serialize_entry(&2, &client_data_hash)?; + if !self.allow_list.is_empty() { + map.serialize_entry(&3, &self.allow_list)?; + } + if self.extensions.has_extensions() { + map.serialize_entry(&4, &self.extensions)?; + } + if self.options.has_some() { + map.serialize_entry(&5, &self.options)?; + } + if let Some(pin_auth) = &self.pin_auth { + map.serialize_entry(&6, &pin_auth)?; + map.serialize_entry(&7, &1)?; + } + map.end() + } +} + +impl Request<GetAssertionResult> for GetAssertion { + fn is_ctap2_request(&self) -> bool { + match self.rp { + RelyingPartyWrapper::Data(_) => true, + RelyingPartyWrapper::Hash(_) => false, + } + } +} + +/// This command is used to check which key_handle is valid for this +/// token. This is sent before a GetAssertion command, to determine which +/// is valid for a specific token and which key_handle GetAssertion +/// should send to the token. Or before a MakeCredential command, to determine +/// if this token is already registered or not. +#[derive(Debug)] +pub(crate) struct CheckKeyHandle<'assertion> { + pub(crate) key_handle: &'assertion [u8], + pub(crate) client_data_wrapper: &'assertion CollectedClientDataWrapper, + pub(crate) rp: &'assertion RelyingPartyWrapper, +} + +impl<'assertion> RequestCtap1 for CheckKeyHandle<'assertion> { + type Output = (); + + fn ctap1_format<Dev>(&self, _dev: &mut Dev) -> Result<Vec<u8>, HIDError> + where + Dev: U2FDevice + io::Read + io::Write + fmt::Debug, + { + let flags = U2F_CHECK_IS_REGISTERED; + // TODO(MS): Need to check "up" here. If up==false, set to 0x08? Or not? Spec is + // ambiguous + let mut auth_data = Vec::with_capacity(2 * PARAMETER_SIZE + 1 + self.key_handle.len()); + + auth_data.extend_from_slice(self.client_data_wrapper.hash().as_ref()); + auth_data.extend_from_slice(self.rp.hash().as_ref()); + auth_data.extend_from_slice(&[self.key_handle.len() as u8]); + auth_data.extend_from_slice(self.key_handle); + let cmd = U2F_AUTHENTICATE; + let apdu = CTAP1RequestAPDU::serialize(cmd, flags, &auth_data)?; + Ok(apdu) + } + + fn handle_response_ctap1( + &self, + status: Result<(), ApduErrorStatus>, + _input: &[u8], + ) -> Result<Self::Output, Retryable<HIDError>> { + // From the U2F-spec: https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#registration-request-message---u2f_register + // if the control byte is set to 0x07 by the FIDO Client, the U2F token is supposed to + // simply check whether the provided key handle was originally created by this token, + // and whether it was created for the provided application parameter. If so, the U2F + // token MUST respond with an authentication response message:error:test-of-user-presence-required + // (note that despite the name this signals a success condition). + // If the key handle was not created by this U2F token, or if it was created for a different + // application parameter, the token MUST respond with an authentication response message:error:bad-key-handle. + match status { + Ok(_) | Err(ApduErrorStatus::ConditionsNotSatisfied) => Ok(()), + Err(e) => Err(Retryable::Error(HIDError::ApduStatus(e))), + } + } +} + +impl RequestCtap1 for GetAssertion { + type Output = GetAssertionResult; + + fn ctap1_format<Dev>(&self, dev: &mut Dev) -> Result<Vec<u8>, HIDError> + where + Dev: io::Read + io::Write + fmt::Debug + FidoDevice, + { + let key_handle = self + .allow_list + .iter() + // key-handles in CTAP1 are limited to 255 bytes, but are not limited in CTAP2. + // Filter out key-handles that are too long (can happen if this is a CTAP2-request, + // but the token only speaks CTAP1). If none is found, return an error. + .filter(|allowed_handle| allowed_handle.id.len() < 256) + .find_map(|allowed_handle| { + let check_command = CheckKeyHandle { + key_handle: allowed_handle.id.as_ref(), + client_data_wrapper: &self.client_data_wrapper, + rp: &self.rp, + }; + let res = dev.send_ctap1(&check_command); + match res { + Ok(_) => Some(allowed_handle.id.clone()), + _ => None, + } + }) + .ok_or(HIDError::DeviceNotSupported)?; + + debug!("sending key_handle = {:?}", key_handle); + + let flags = if self.options.user_presence.unwrap_or(false) { + U2F_REQUEST_USER_PRESENCE + } else { + 0 + }; + let mut auth_data = + Vec::with_capacity(2 * PARAMETER_SIZE + 1 /* key_handle_len */ + key_handle.len()); + + if self.is_ctap2_request() { + auth_data.extend_from_slice(self.client_data_hash().as_ref()); + } else { + let decoded = base64::decode_config( + &self.client_data_wrapper.client_data.challenge.0, + base64::URL_SAFE_NO_PAD, + ) + .map_err(|_| HIDError::DeviceError)?; // We encoded it, so this should never fail + auth_data.extend_from_slice(&decoded); + } + auth_data.extend_from_slice(self.rp.hash().as_ref()); + auth_data.extend_from_slice(&[key_handle.len() as u8]); + auth_data.extend_from_slice(key_handle.as_ref()); + + let cmd = U2F_AUTHENTICATE; + let apdu = CTAP1RequestAPDU::serialize(cmd, flags, &auth_data)?; + Ok(apdu) + } + + fn handle_response_ctap1( + &self, + status: Result<(), ApduErrorStatus>, + input: &[u8], + ) -> Result<Self::Output, Retryable<HIDError>> { + if Err(ApduErrorStatus::ConditionsNotSatisfied) == status { + return Err(Retryable::Retry); + } + if let Err(err) = status { + return Err(Retryable::Error(HIDError::ApduStatus(err))); + } + + if self.is_ctap2_request() { + let parse_authentication = |input| { + // Parsing an u8, then a u32, and the rest is the signature + let (rest, (user_presence, counter)) = tuple((be_u8, be_u32))(input)?; + let signature = Vec::from(rest); + Ok((user_presence, counter, signature)) + }; + let (user_presence, counter, signature) = parse_authentication(input) + .map_err(|e: nom::Err<VerboseError<_>>| { + error!("error while parsing authentication: {:?}", e); + CommandError::Deserializing(DesError::custom("unable to parse authentication")) + }) + .map_err(HIDError::Command) + .map_err(Retryable::Error)?; + + let mut flags = AuthenticatorDataFlags::empty(); + if user_presence == 1 { + flags |= AuthenticatorDataFlags::USER_PRESENT; + } + let auth_data = AuthenticatorData { + rp_id_hash: self.rp.hash(), + flags, + counter, + credential_data: None, + extensions: Default::default(), + }; + let assertion = Assertion { + credentials: None, + signature, + user: None, + auth_data, + }; + + Ok(GetAssertionResult::CTAP2( + AssertionObject(vec![assertion]), + self.client_data_wrapper.clone(), + )) + } else { + Ok(GetAssertionResult::CTAP1(input.to_vec())) + } + } +} + +impl RequestCtap2 for GetAssertion { + type Output = GetAssertionResult; + + fn command() -> Command { + Command::GetAssertion + } + + fn wire_format<Dev>(&self, _dev: &mut Dev) -> Result<Vec<u8>, HIDError> + where + Dev: FidoDevice + io::Read + io::Write + fmt::Debug, + { + Ok(ser::to_vec(&self).map_err(CommandError::Serializing)?) + } + + fn handle_response_ctap2<Dev>( + &self, + dev: &mut Dev, + input: &[u8], + ) -> Result<Self::Output, HIDError> + where + Dev: FidoDevice + io::Read + io::Write + fmt::Debug, + { + if input.is_empty() { + return Err(CommandError::InputTooSmall.into()); + } + + let status: StatusCode = input[0].into(); + debug!( + "response status code: {:?}, rest: {:?}", + status, + &input[1..] + ); + if input.len() > 1 { + if status.is_ok() { + let assertion: GetAssertionResponse = + from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + let number_of_credentials = assertion.number_of_credentials.unwrap_or(1); + let mut assertions = Vec::with_capacity(number_of_credentials); + assertions.push(assertion.into()); + + let msg = GetNextAssertion; + // We already have one, so skipping 0 + for _ in 1..number_of_credentials { + let new_cred = dev.send_cbor(&msg)?; + assertions.push(new_cred.into()); + } + + Ok(GetAssertionResult::CTAP2( + AssertionObject(assertions), + self.client_data_wrapper.clone(), + )) + } else { + let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + Err(CommandError::StatusCode(status, Some(data)).into()) + } + } else if status.is_ok() { + Err(CommandError::InputTooSmall.into()) + } else { + Err(CommandError::StatusCode(status, None).into()) + } + } +} + +#[derive(Debug, PartialEq)] +pub struct Assertion { + pub credentials: Option<PublicKeyCredentialDescriptor>, /* Was optional in CTAP2.0, is + * mandatory in CTAP2.1 */ + pub auth_data: AuthenticatorData, + pub signature: Vec<u8>, + pub user: Option<User>, +} + +impl From<GetAssertionResponse> for Assertion { + fn from(r: GetAssertionResponse) -> Self { + Assertion { + credentials: r.credentials, + auth_data: r.auth_data, + signature: r.signature, + user: r.user, + } + } +} + +// TODO(baloo): Move this to src/ctap2/mod.rs? +#[derive(Debug, PartialEq)] +pub struct AssertionObject(pub Vec<Assertion>); + +impl AssertionObject { + pub fn u2f_sign_data(&self) -> Vec<u8> { + if let Some(first) = self.0.first() { + let mut res = Vec::new(); + res.push(first.auth_data.flags.bits()); + res.extend(&first.auth_data.counter.to_be_bytes()); + res.extend(&first.signature); + res + // first.signature.clone() + } else { + Vec::new() + } + } +} + +pub(crate) struct GetAssertionResponse { + credentials: Option<PublicKeyCredentialDescriptor>, + auth_data: AuthenticatorData, + signature: Vec<u8>, + user: Option<User>, + number_of_credentials: Option<usize>, +} + +impl<'de> Deserialize<'de> for GetAssertionResponse { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + struct GetAssertionResponseVisitor; + + impl<'de> Visitor<'de> for GetAssertionResponseVisitor { + type Value = GetAssertionResponse; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a byte array") + } + + fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error> + where + M: MapAccess<'de>, + { + let mut credentials = None; + let mut auth_data = None; + let mut signature = None; + let mut user = None; + let mut number_of_credentials = None; + + while let Some(key) = map.next_key()? { + match key { + 1 => { + if credentials.is_some() { + return Err(M::Error::duplicate_field("credentials")); + } + credentials = Some(map.next_value()?); + } + 2 => { + if auth_data.is_some() { + return Err(M::Error::duplicate_field("auth_data")); + } + auth_data = Some(map.next_value()?); + } + 3 => { + if signature.is_some() { + return Err(M::Error::duplicate_field("signature")); + } + let signature_bytes: ByteBuf = map.next_value()?; + let signature_bytes: Vec<u8> = signature_bytes.into_vec(); + signature = Some(signature_bytes); + } + 4 => { + if user.is_some() { + return Err(M::Error::duplicate_field("user")); + } + user = map.next_value()?; + } + 5 => { + if number_of_credentials.is_some() { + return Err(M::Error::duplicate_field("number_of_credentials")); + } + number_of_credentials = Some(map.next_value()?); + } + k => return Err(M::Error::custom(format!("unexpected key: {:?}", k))), + } + } + + let auth_data = auth_data.ok_or_else(|| M::Error::missing_field("auth_data"))?; + let signature = signature.ok_or_else(|| M::Error::missing_field("signature"))?; + + Ok(GetAssertionResponse { + credentials, + auth_data, + signature, + user, + number_of_credentials, + }) + } + } + + deserializer.deserialize_bytes(GetAssertionResponseVisitor) + } +} + +#[cfg(test)] +pub mod test { + use super::{Assertion, GetAssertion, GetAssertionOptions, GetAssertionResult, HIDError}; + use crate::consts::{ + HIDCmd, SW_CONDITIONS_NOT_SATISFIED, SW_NO_ERROR, U2F_CHECK_IS_REGISTERED, + U2F_REQUEST_USER_PRESENCE, + }; + use crate::ctap2::attestation::{AuthenticatorData, AuthenticatorDataFlags}; + use crate::ctap2::client_data::{ + Challenge, CollectedClientData, CollectedClientDataWrapper, TokenBinding, WebauthnType, + }; + use crate::ctap2::commands::get_assertion::AssertionObject; + use crate::ctap2::commands::RequestCtap1; + use crate::ctap2::server::{ + PublicKeyCredentialDescriptor, RelyingParty, RelyingPartyWrapper, RpIdHash, Transport, User, + }; + use crate::transport::device_selector::Device; + use crate::transport::hid::HIDDevice; + use crate::transport::FidoDevice; + use crate::u2ftypes::U2FDevice; + use rand::{thread_rng, RngCore}; + + #[test] + fn test_get_assertion_ctap2() { + let client_data = CollectedClientData { + webauthn_type: WebauthnType::Create, + challenge: Challenge::from(vec![0x00, 0x01, 0x02, 0x03]), + origin: String::from("example.com"), + cross_origin: false, + token_binding: Some(TokenBinding::Present(String::from("AAECAw"))), + }; + let assertion = GetAssertion::new( + client_data.clone(), + RelyingPartyWrapper::Data(RelyingParty { + id: String::from("example.com"), + name: Some(String::from("Acme")), + icon: None, + }), + vec![PublicKeyCredentialDescriptor { + id: vec![ + 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35, + 0xEF, 0xAA, 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D, + 0xA4, 0x85, 0x34, 0xC8, 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5, + 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD, 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5, + 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08, 0xFE, 0x42, 0x00, 0x38, + ], + transports: vec![Transport::USB], + }], + GetAssertionOptions { + user_presence: Some(true), + user_verification: None, + }, + Default::default(), + None, + ) + .expect("Failed to create GetAssertion"); + let mut device = Device::new("commands/get_assertion").unwrap(); + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + device.set_cid(cid); + + let mut msg = cid.to_vec(); + msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x90]); + msg.extend(vec![0x2]); // u2f command + msg.extend(vec![ + 0xa4, // map(4) + 0x1, // rpid + 0x6b, // text(11) + 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, // example.com + 0x2, // clientDataHash + 0x58, 0x20, //bytes(32) + 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, 0x32, + 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, 0x10, 0x87, + 0x54, 0xc3, 0x2d, 0x80, // hash + 0x3, //allowList + 0x81, // array(1) + 0xa2, // map(2) + 0x64, // text(4), + 0x74, 0x79, 0x70, // typ + ]); + device.add_write(&msg, 0); + + msg = cid.to_vec(); + msg.extend(&[0x0]); //SEQ + msg.extend(vec![ + 0x65, // e (continuation of type) + 0x6a, // text(10) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // public-key + 0x62, // text(2) + 0x69, 0x64, // id + 0x58, 0x40, // bytes(64) + ]); + msg.extend(&assertion.allow_list[0].id[..42]); + device.add_write(&msg, 0); + + msg = cid.to_vec(); + msg.extend(&[0x1]); //SEQ + msg.extend(&assertion.allow_list[0].id[42..]); + msg.extend(vec![ + 0x5, // options + 0xa1, // map(1) + 0x62, // text(2) + 0x75, 0x70, // up + 0xf5, // true + ]); + device.add_write(&msg, 0); + + // fido response + let mut msg = cid.to_vec(); + msg.extend(&[HIDCmd::Cbor.into(), 0x1, 0x5c]); // cmd + bcnt + msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[..57]); + device.add_read(&msg, 0); + + let mut msg = cid.to_vec(); + msg.extend(&[0x0]); // SEQ + msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[57..116]); + device.add_read(&msg, 0); + + let mut msg = cid.to_vec(); + msg.extend(&[0x1]); // SEQ + msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[116..175]); + device.add_read(&msg, 0); + + let mut msg = cid.to_vec(); + msg.extend(&[0x2]); // SEQ + msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[175..234]); + device.add_read(&msg, 0); + + let mut msg = cid.to_vec(); + msg.extend(&[0x3]); // SEQ + msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[234..293]); + device.add_read(&msg, 0); + let mut msg = cid.to_vec(); + msg.extend(&[0x4]); // SEQ + msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[293..]); + device.add_read(&msg, 0); + + // Check if response is correct + let expected_auth_data = AuthenticatorData { + rp_id_hash: RpIdHash([ + 0x62, 0x5d, 0xda, 0xdf, 0x74, 0x3f, 0x57, 0x27, 0xe6, 0x6b, 0xba, 0x8c, 0x2e, 0x38, + 0x79, 0x22, 0xd1, 0xaf, 0x43, 0xc5, 0x03, 0xd9, 0x11, 0x4a, 0x8f, 0xba, 0x10, 0x4d, + 0x84, 0xd0, 0x2b, 0xfa, + ]), + flags: AuthenticatorDataFlags::USER_PRESENT, + counter: 0x11, + credential_data: None, + extensions: Default::default(), + }; + + let expected_assertion = Assertion { + credentials: Some(PublicKeyCredentialDescriptor { + id: vec![ + 242, 32, 6, 222, 79, 144, 90, 246, 138, 67, 148, 47, 2, 79, 42, 94, 206, 96, + 61, 156, 109, 75, 61, 248, 190, 8, 237, 1, 252, 68, 38, 70, 208, 52, 133, 138, + 199, 91, 237, 63, 213, 128, 191, 152, 8, 217, 79, 203, 238, 130, 185, 178, 239, + 102, 119, 175, 10, 220, 195, 88, 82, 234, 107, 158, + ], + transports: vec![], + }), + signature: vec![ + 0x30, 0x45, 0x02, 0x20, 0x4a, 0x5a, 0x9d, 0xd3, 0x92, 0x98, 0x14, 0x9d, 0x90, 0x47, + 0x69, 0xb5, 0x1a, 0x45, 0x14, 0x33, 0x00, 0x6f, 0x18, 0x2a, 0x34, 0xfb, 0xdf, 0x66, + 0xde, 0x5f, 0xc7, 0x17, 0xd7, 0x5f, 0xb3, 0x50, 0x02, 0x21, 0x00, 0xa4, 0x6b, 0x8e, + 0xa3, 0xc3, 0xb9, 0x33, 0x82, 0x1c, 0x6e, 0x7f, 0x5e, 0xf9, 0xda, 0xae, 0x94, 0xab, + 0x47, 0xf1, 0x8d, 0xb4, 0x74, 0xc7, 0x47, 0x90, 0xea, 0xab, 0xb1, 0x44, 0x11, 0xe7, + 0xa0, + ], + user: Some(User { + id: vec![ + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, + ], + icon: Some("https://pics.example.com/00/p/aBjjjpqPb.png".to_string()), + name: Some("johnpsmith@example.com".to_string()), + display_name: Some("John P. Smith".to_string()), + }), + auth_data: expected_auth_data, + }; + + let expected = GetAssertionResult::CTAP2( + AssertionObject(vec![expected_assertion]), + CollectedClientDataWrapper { + client_data, + serialized_data: CLIENT_DATA_VEC.to_vec(), + }, + ); + let response = device.send_cbor(&assertion).unwrap(); + assert_eq!(response, expected); + } + + fn fill_device_ctap1(device: &mut Device, cid: [u8; 4], flags: u8, answer_status: [u8; 2]) { + // ctap2 request + let mut msg = cid.to_vec(); + msg.extend(&[HIDCmd::Msg.into(), 0x00, 0x8A]); // cmd + bcnt + msg.extend(&[0x00, 0x2]); // U2F_AUTHENTICATE + msg.extend(&[flags]); + msg.extend(&[0x00, 0x00, 0x00]); + msg.extend(&[0x81]); // Data len - 7 + msg.extend(&CLIENT_DATA_HASH); + msg.extend(&RELYING_PARTY_HASH[..18]); + device.add_write(&msg, 0); + + // Continuation package + let mut msg = cid.to_vec(); + msg.extend(vec![0x00]); // SEQ + msg.extend(&RELYING_PARTY_HASH[18..]); + msg.extend(&[KEY_HANDLE.len() as u8]); + msg.extend(&KEY_HANDLE[..44]); + device.add_write(&msg, 0); + + let mut msg = cid.to_vec(); + msg.extend(vec![0x01]); // SEQ + msg.extend(&KEY_HANDLE[44..]); + device.add_write(&msg, 0); + + // fido response + let mut msg = cid.to_vec(); + msg.extend(&[HIDCmd::Msg.into(), 0x0, 0x4D]); // cmd + bcnt + msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP1[0..57]); + device.add_read(&msg, 0); + + let mut msg = cid.to_vec(); + msg.extend(&[0x0]); // SEQ + msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP1[57..]); + msg.extend(&answer_status); + device.add_read(&msg, 0); + } + + #[test] + fn test_get_assertion_ctap1() { + let client_data = CollectedClientData { + webauthn_type: WebauthnType::Create, + challenge: Challenge::from(vec![0x00, 0x01, 0x02, 0x03]), + origin: String::from("example.com"), + cross_origin: false, + token_binding: Some(TokenBinding::Present(String::from("AAECAw"))), + }; + let assertion = GetAssertion::new( + client_data.clone(), + RelyingPartyWrapper::Data(RelyingParty { + id: String::from("example.com"), + name: Some(String::from("Acme")), + icon: None, + }), + vec![PublicKeyCredentialDescriptor { + id: vec![ + 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35, + 0xEF, 0xAA, 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D, + 0xA4, 0x85, 0x34, 0xC8, 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5, + 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD, 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5, + 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08, 0xFE, 0x42, 0x00, 0x38, + ], + transports: vec![Transport::USB], + }], + GetAssertionOptions { + user_presence: Some(true), + user_verification: None, + }, + Default::default(), + None, + ) + .expect("Failed to create GetAssertion"); + let mut device = Device::new("commands/get_assertion").unwrap(); // not really used (all functions ignore it) + // channel id + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + + device.set_cid(cid); + + // ctap1 request + fill_device_ctap1( + &mut device, + cid, + U2F_CHECK_IS_REGISTERED, + SW_CONDITIONS_NOT_SATISFIED, + ); + let ctap1_request = assertion.ctap1_format(&mut device).unwrap(); + // Check if the request is going to be correct + assert_eq!(ctap1_request, GET_ASSERTION_SAMPLE_REQUEST_CTAP1); + + // Now do it again, but parse the actual response + fill_device_ctap1( + &mut device, + cid, + U2F_CHECK_IS_REGISTERED, + SW_CONDITIONS_NOT_SATISFIED, + ); + fill_device_ctap1(&mut device, cid, U2F_REQUEST_USER_PRESENCE, SW_NO_ERROR); + + let response = device.send_ctap1(&assertion).unwrap(); + + // Check if response is correct + let expected_auth_data = AuthenticatorData { + rp_id_hash: RpIdHash(RELYING_PARTY_HASH), + flags: AuthenticatorDataFlags::USER_PRESENT, + counter: 0x3B, + credential_data: None, + extensions: Default::default(), + }; + + let expected_assertion = Assertion { + credentials: None, + signature: vec![ + 0x30, 0x44, 0x02, 0x20, 0x7B, 0xDE, 0x0A, 0x52, 0xAC, 0x1F, 0x4C, 0x8B, 0x27, 0xE0, + 0x03, 0xA3, 0x70, 0xCD, 0x66, 0xA4, 0xC7, 0x11, 0x8D, 0xD2, 0x2D, 0x54, 0x47, 0x83, + 0x5F, 0x45, 0xB9, 0x9C, 0x68, 0x42, 0x3F, 0xF7, 0x02, 0x20, 0x3C, 0x51, 0x7B, 0x47, + 0x87, 0x7F, 0x85, 0x78, 0x2D, 0xE1, 0x00, 0x86, 0xA7, 0x83, 0xD1, 0xE7, 0xDF, 0x4E, + 0x36, 0x39, 0xE7, 0x71, 0xF5, 0xF6, 0xAF, 0xA3, 0x5A, 0xAD, 0x53, 0x73, 0x85, 0x8E, + ], + user: None, + auth_data: expected_auth_data, + }; + + let expected = GetAssertionResult::CTAP2( + AssertionObject(vec![expected_assertion]), + CollectedClientDataWrapper { + client_data, + serialized_data: CLIENT_DATA_VEC.to_vec(), + }, + ); + + assert_eq!(response, expected); + } + + #[test] + fn test_get_assertion_ctap1_long_keys() { + let client_data = CollectedClientData { + webauthn_type: WebauthnType::Create, + challenge: Challenge::from(vec![0x00, 0x01, 0x02, 0x03]), + origin: String::from("example.com"), + cross_origin: false, + token_binding: Some(TokenBinding::Present(String::from("AAECAw"))), + }; + + let too_long_key_handle = PublicKeyCredentialDescriptor { + id: vec![0; 1000], + transports: vec![Transport::USB], + }; + let mut assertion = GetAssertion::new( + client_data.clone(), + RelyingPartyWrapper::Data(RelyingParty { + id: String::from("example.com"), + name: Some(String::from("Acme")), + icon: None, + }), + vec![too_long_key_handle.clone()], + GetAssertionOptions { + user_presence: Some(true), + user_verification: None, + }, + Default::default(), + None, + ) + .expect("Failed to create GetAssertion"); + + let mut device = Device::new("commands/get_assertion").unwrap(); // not really used (all functions ignore it) + // channel id + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + + device.set_cid(cid); + + assert_matches!( + assertion.ctap1_format(&mut device), + Err(HIDError::DeviceNotSupported) + ); + + assertion.allow_list = vec![too_long_key_handle.clone(); 5]; + + assert_matches!( + assertion.ctap1_format(&mut device), + Err(HIDError::DeviceNotSupported) + ); + let ok_key_handle = PublicKeyCredentialDescriptor { + id: vec![ + 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35, 0xEF, + 0xAA, 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D, 0xA4, 0x85, + 0x34, 0xC8, 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5, 0xCC, 0x4E, 0x78, + 0x05, 0x5B, 0xDD, 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC, + 0xD4, 0x15, 0xCD, 0x08, 0xFE, 0x42, 0x00, 0x38, + ], + transports: vec![Transport::USB], + }; + assertion.allow_list = vec![ + too_long_key_handle.clone(), + too_long_key_handle.clone(), + too_long_key_handle.clone(), + ok_key_handle, + too_long_key_handle.clone(), + ]; + + // ctap1 request + fill_device_ctap1( + &mut device, + cid, + U2F_CHECK_IS_REGISTERED, + SW_CONDITIONS_NOT_SATISFIED, + ); + let ctap1_request = assertion.ctap1_format(&mut device).unwrap(); + // Check if the request is going to be correct + assert_eq!(ctap1_request, GET_ASSERTION_SAMPLE_REQUEST_CTAP1); + + // Now do it again, but parse the actual response + fill_device_ctap1( + &mut device, + cid, + U2F_CHECK_IS_REGISTERED, + SW_CONDITIONS_NOT_SATISFIED, + ); + fill_device_ctap1(&mut device, cid, U2F_REQUEST_USER_PRESENCE, SW_NO_ERROR); + + let response = device.send_ctap1(&assertion).unwrap(); + + // Check if response is correct + let expected_auth_data = AuthenticatorData { + rp_id_hash: RpIdHash(RELYING_PARTY_HASH), + flags: AuthenticatorDataFlags::USER_PRESENT, + counter: 0x3B, + credential_data: None, + extensions: Default::default(), + }; + + let expected_assertion = Assertion { + credentials: None, + signature: vec![ + 0x30, 0x44, 0x02, 0x20, 0x7B, 0xDE, 0x0A, 0x52, 0xAC, 0x1F, 0x4C, 0x8B, 0x27, 0xE0, + 0x03, 0xA3, 0x70, 0xCD, 0x66, 0xA4, 0xC7, 0x11, 0x8D, 0xD2, 0x2D, 0x54, 0x47, 0x83, + 0x5F, 0x45, 0xB9, 0x9C, 0x68, 0x42, 0x3F, 0xF7, 0x02, 0x20, 0x3C, 0x51, 0x7B, 0x47, + 0x87, 0x7F, 0x85, 0x78, 0x2D, 0xE1, 0x00, 0x86, 0xA7, 0x83, 0xD1, 0xE7, 0xDF, 0x4E, + 0x36, 0x39, 0xE7, 0x71, 0xF5, 0xF6, 0xAF, 0xA3, 0x5A, 0xAD, 0x53, 0x73, 0x85, 0x8E, + ], + user: None, + auth_data: expected_auth_data, + }; + + let expected = GetAssertionResult::CTAP2( + AssertionObject(vec![expected_assertion]), + CollectedClientDataWrapper { + client_data, + serialized_data: CLIENT_DATA_VEC.to_vec(), + }, + ); + + assert_eq!(response, expected); + } + + // Manually assembled according to https://www.w3.org/TR/webauthn-2/#clientdatajson-serialization + const CLIENT_DATA_VEC: [u8; 140] = [ + 0x7b, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, // {"type": + 0x22, 0x77, 0x65, 0x62, 0x61, 0x75, 0x74, 0x68, 0x6e, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x22, // "webauthn.create" + 0x2c, 0x22, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x22, + 0x3a, // (,"challenge": + 0x22, 0x41, 0x41, 0x45, 0x43, 0x41, 0x77, 0x22, // challenge in base64 + 0x2c, 0x22, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x22, 0x3a, // ,"origin": + 0x22, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, + 0x22, // "example.com" + 0x2c, 0x22, 0x63, 0x72, 0x6f, 0x73, 0x73, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x22, + 0x3a, // ,"crossOrigin": + 0x66, 0x61, 0x6c, 0x73, 0x65, // false + 0x2c, 0x22, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x22, + 0x3a, // ,"tokenBinding": + 0x7b, 0x22, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x3a, // {"status": + 0x22, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x22, // "present" + 0x2c, 0x22, 0x69, 0x64, 0x22, 0x3a, // ,"id": + 0x22, 0x41, 0x41, 0x45, 0x43, 0x41, 0x77, 0x22, // "AAECAw" + 0x7d, // } + 0x7d, // } + ]; + + const CLIENT_DATA_HASH: [u8; 32] = [ + 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, // hash + 0x32, 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, // hash + 0x10, 0x87, 0x54, 0xc3, 0x2d, 0x80, // hash + ]; + + const RELYING_PARTY_HASH: [u8; 32] = [ + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, 0x34, 0xE2, + 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, 0x12, 0x55, 0x86, 0xCE, + 0x19, 0x47, + ]; + const KEY_HANDLE: [u8; 64] = [ + 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35, 0xEF, 0xAA, + 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D, 0xA4, 0x85, 0x34, 0xC8, + 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5, 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD, + 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08, + 0xFE, 0x42, 0x00, 0x38, + ]; + + const GET_ASSERTION_SAMPLE_REQUEST_CTAP1: [u8; 138] = [ + // CBOR Header + 0x0, // CLA + 0x2, // INS U2F_Authenticate + 0x3, // P1 Flags (user presence) + 0x0, // P2 + 0x0, 0x0, 0x81, // Lc + // NOTE: This has been taken from CTAP2.0 spec, but the clientDataHash has been replaced + // to be able to operate with known values for CollectedClientData (spec doesn't say + // what values led to the provided example hash) + // clientDataHash: + 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, // hash + 0x32, 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, // hash + 0x10, 0x87, 0x54, 0xc3, 0x2d, 0x80, // hash + // rpIdHash: + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, 0x34, 0xE2, + 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, 0x12, 0x55, 0x86, 0xCE, + 0x19, 0x47, // .. + // Key Handle Length (1 Byte): + 0x40, // .. + // Key Handle (Key Handle Length Bytes): + 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35, 0xEF, 0xAA, + 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D, 0xA4, 0x85, 0x34, 0xC8, + 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5, 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD, + 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08, + 0xFE, 0x42, 0x00, 0x38, // .. + // Le (Ne=65536): + 0x0, 0x0, + ]; + + const GET_ASSERTION_SAMPLE_REQUEST_CTAP2: [u8; 138] = [ + // CBOR Header + 0x0, // leading zero + 0x2, // CMD U2F_Authenticate + 0x3, // Flags (user presence) + 0x0, 0x0, // zero bits + 0x0, 0x81, // size + // NOTE: This has been taken from CTAP2.0 spec, but the clientDataHash has been replaced + // to be able to operate with known values for CollectedClientData (spec doesn't say + // what values led to the provided example hash) + // clientDataHash: + 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, 0x32, 0x64, + 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, 0x10, 0x87, 0x54, 0xc3, + 0x2d, 0x80, // hash + // rpIdHash: + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, 0x34, 0xE2, + 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, 0x12, 0x55, 0x86, 0xCE, + 0x19, 0x47, // .. + // Key Handle Length (1 Byte): + 0x40, // .. + // Key Handle (Key Handle Length Bytes): + 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35, 0xEF, 0xAA, + 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D, 0xA4, 0x85, 0x34, 0xC8, + 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5, 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD, + 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08, + 0xFE, 0x42, 0x00, 0x38, 0x0, 0x0, // 2 trailing zeros from protocol + ]; + + const GET_ASSERTION_SAMPLE_RESPONSE_CTAP1: [u8; 75] = [ + 0x01, // User Presence (1 Byte) + 0x00, 0x00, 0x00, 0x3B, // Sign Count (4 Bytes) + // Signature (variable Length) + 0x30, 0x44, 0x02, 0x20, 0x7B, 0xDE, 0x0A, 0x52, 0xAC, 0x1F, 0x4C, 0x8B, 0x27, 0xE0, 0x03, + 0xA3, 0x70, 0xCD, 0x66, 0xA4, 0xC7, 0x11, 0x8D, 0xD2, 0x2D, 0x54, 0x47, 0x83, 0x5F, 0x45, + 0xB9, 0x9C, 0x68, 0x42, 0x3F, 0xF7, 0x02, 0x20, 0x3C, 0x51, 0x7B, 0x47, 0x87, 0x7F, 0x85, + 0x78, 0x2D, 0xE1, 0x00, 0x86, 0xA7, 0x83, 0xD1, 0xE7, 0xDF, 0x4E, 0x36, 0x39, 0xE7, 0x71, + 0xF5, 0xF6, 0xAF, 0xA3, 0x5A, 0xAD, 0x53, 0x73, 0x85, 0x8E, + ]; + + const GET_ASSERTION_SAMPLE_RESPONSE_CTAP2: [u8; 348] = [ + 0x00, // status == success + 0xA5, // map(5) + 0x01, // unsigned(1) + 0xA2, // map(2) + 0x62, // text(2) + 0x69, 0x64, // "id" + 0x58, 0x40, // bytes(0x64, ) + 0xF2, 0x20, 0x06, 0xDE, 0x4F, 0x90, 0x5A, 0xF6, 0x8A, 0x43, 0x94, 0x2F, 0x02, 0x4F, 0x2A, + 0x5E, 0xCE, 0x60, 0x3D, 0x9C, 0x6D, 0x4B, 0x3D, 0xF8, 0xBE, 0x08, 0xED, 0x01, 0xFC, 0x44, + 0x26, 0x46, 0xD0, 0x34, 0x85, 0x8A, 0xC7, 0x5B, 0xED, 0x3F, 0xD5, 0x80, 0xBF, 0x98, 0x08, + 0xD9, 0x4F, 0xCB, 0xEE, 0x82, 0xB9, 0xB2, 0xEF, 0x66, 0x77, 0xAF, 0x0A, 0xDC, 0xC3, 0x58, + 0x52, 0xEA, 0x6B, + 0x9E, // "\x0xF2, \x0x06, \x0xDE, O\x0x90, Z\x0xF6, \x0x8A, C\x0x94, /\x0x02, O*^\x0xCE, `=\x0x9C, mK=\x0xF8, \x0xBE, \b\x0xED, \x0x01, \x0xFC, D&F\x0xD0, 4\x0x85, \x0x8A, \x0xC7, [\x0xED, ?\x0xD5, \x0x80, \x0xBF, \x0x98, \b\x0xD9, O\x0xCB, \x0xEE, \x0x82, \x0xB9, \x0xB2, \x0xEF, fw\x0xAF, \n\x0xDC, \x0xC3, 0xXR, \x0xEA, k\x0x9E, " + 0x64, // text(4) + 0x74, 0x79, 0x70, 0x65, // "type" + 0x6A, // text(0x10, ) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // "public-key" + 0x02, // unsigned(2) + 0x58, 0x25, // bytes(0x37, ) + 0x62, 0x5D, 0xDA, 0xDF, 0x74, 0x3F, 0x57, 0x27, 0xE6, 0x6B, 0xBA, 0x8C, 0x2E, 0x38, 0x79, + 0x22, 0xD1, 0xAF, 0x43, 0xC5, 0x03, 0xD9, 0x11, 0x4A, 0x8F, 0xBA, 0x10, 0x4D, 0x84, 0xD0, + 0x2B, 0xFA, 0x01, 0x00, 0x00, 0x00, + 0x11, // "b]\x0xDA, \x0xDF, t?W'\x0xE6, k\x0xBA, \x0x8C, .8y\"\x0xD1, \x0xAF, C\x0xC5, \x0x03, \x0xD9, \x0x11, J\x0x8F, \x0xBA, \x0x10, M\x0x84, \x0xD0, +\x0xFA, \x0x01, \x0x00, \x0x00, \x0x00, \x0x11, " + 0x03, // unsigned(3) + 0x58, 0x47, // bytes(0x71, ) + 0x30, 0x45, 0x02, 0x20, 0x4A, 0x5A, 0x9D, 0xD3, 0x92, 0x98, 0x14, 0x9D, 0x90, 0x47, 0x69, + 0xB5, 0x1A, 0x45, 0x14, 0x33, 0x00, 0x6F, 0x18, 0x2A, 0x34, 0xFB, 0xDF, 0x66, 0xDE, 0x5F, + 0xC7, 0x17, 0xD7, 0x5F, 0xB3, 0x50, 0x02, 0x21, 0x00, 0xA4, 0x6B, 0x8E, 0xA3, 0xC3, 0xB9, + 0x33, 0x82, 0x1C, 0x6E, 0x7F, 0x5E, 0xF9, 0xDA, 0xAE, 0x94, 0xAB, 0x47, 0xF1, 0x8D, 0xB4, + 0x74, 0xC7, 0x47, 0x90, 0xEA, 0xAB, 0xB1, 0x44, 0x11, 0xE7, + 0xA0, // "0x0E, \x0x02, 0xJZ, \x0x9D, \x0xD3, \x0x92, \x0x98, \x0x14, \x0x9D, \x0x90, Gi\x0xB5, \x0x1A, E\x0x14, 3\x0x00, o\x0x18, *4\x0xFB, \x0xDF, f\x0xDE, _\x0xC7, \x0x17, \x0xD7, _\x0xB3, P\x0x02, !\x0x00, \x0xA4, k\x0x8E, \x0xA3, \x0xC3, \x0xB9, 3\x0x82, \x0x1C, n\x0x7F, ^\x0xF9, \x0xDA, \x0xAE, \x0x94, \x0xAB, G\x0xF1, \x0x8D, \x0xB4, t\x0xC7, G\x0x90, \x0xEA, \x0xAB, \x0xB1, D\x0x11, \x0xE7, \x0xA0, " + 0x04, // unsigned(4) + 0xA4, // map(4) + 0x62, // text(2) + 0x69, 0x64, // "id" + 0x58, 0x20, // bytes(0x32, ) + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xA0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82, + 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xA0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82, 0x01, 0x93, + 0x30, + 0x82, // "0\x0x82, \x0x01, \x0x93, 0\x0x82, \x0x01, 8\x0xA0, \x0x03, \x0x02, \x0x01, \x0x02, 0\x0x82, \x0x01, \x0x93, 0\x0x82, \x0x01, 8\x0xA0, \x0x03, \x0x02, \x0x01, \x0x02, 0\x0x82, \x0x01, \x0x93, 0\x0x82, " + 0x64, // text(4) + 0x69, 0x63, 0x6F, 0x6E, // "icon" + 0x78, 0x2B, // text(0x43, ) + 0x68, 0x74, 0x74, 0x70, 0x73, 0x3A, 0x2F, 0x2F, 0x70, 0x69, 0x63, 0x73, 0x2E, 0x65, 0x78, + 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x30, 0x30, 0x2F, 0x70, 0x2F, + 0x61, 0x42, 0x6A, 0x6A, 0x6A, 0x70, 0x71, 0x50, 0x62, 0x2E, 0x70, 0x6E, + 0x67, // "https://pics.example.com/0x00, /p/aBjjjpqPb.png" + 0x64, // text(4) + 0x6E, 0x61, 0x6D, 0x65, // "name" + 0x76, // text(0x22, ) + 0x6A, 0x6F, 0x68, 0x6E, 0x70, 0x73, 0x6D, 0x69, 0x74, 0x68, 0x40, 0x65, 0x78, 0x61, 0x6D, + 0x70, 0x6C, 0x65, 0x2E, 0x63, 0x6F, 0x6D, // "johnpsmith@example.com" + 0x6B, // text(0x11, ) + 0x64, 0x69, 0x73, 0x70, 0x6C, 0x61, 0x79, 0x4E, 0x61, 0x6D, 0x65, // "displayName" + 0x6D, // text(0x13, ) + 0x4A, 0x6F, 0x68, 0x6E, 0x20, 0x50, 0x2E, 0x20, 0x53, 0x6D, 0x69, 0x74, + 0x68, // "John P. Smith" + 0x05, // unsigned(5) + 0x01, // unsigned(1) + ]; +} diff --git a/third_party/rust/authenticator/src/ctap2/commands/get_info.rs b/third_party/rust/authenticator/src/ctap2/commands/get_info.rs new file mode 100644 index 0000000000..ffad083682 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/get_info.rs @@ -0,0 +1,733 @@ +use super::{Command, CommandError, RequestCtap2, StatusCode}; +use crate::ctap2::attestation::AAGuid; +use crate::ctap2::server::PublicKeyCredentialParameters; +use crate::transport::errors::HIDError; +use crate::u2ftypes::U2FDevice; +use serde::{ + de::{Error as SError, IgnoredAny, MapAccess, Visitor}, + Deserialize, Deserializer, +}; +use serde_cbor::{de::from_slice, Value}; +use std::collections::BTreeMap; +use std::fmt; + +#[derive(Debug)] +pub struct GetInfo {} + +impl Default for GetInfo { + fn default() -> GetInfo { + GetInfo {} + } +} + +impl RequestCtap2 for GetInfo { + type Output = AuthenticatorInfo; + + fn command() -> Command { + Command::GetInfo + } + + fn wire_format<Dev>(&self, _dev: &mut Dev) -> Result<Vec<u8>, HIDError> + where + Dev: U2FDevice, + { + Ok(Vec::new()) + } + + fn handle_response_ctap2<Dev>( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result<Self::Output, HIDError> + where + Dev: U2FDevice, + { + if input.is_empty() { + return Err(CommandError::InputTooSmall.into()); + } + + let status: StatusCode = input[0].into(); + + if input.len() > 1 { + if status.is_ok() { + trace!("parsing authenticator info data: {:#04X?}", &input); + let authenticator_info = + from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + Ok(authenticator_info) + } else { + let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + Err(CommandError::StatusCode(status, Some(data)).into()) + } + } else { + Err(CommandError::InputTooSmall.into()) + } + } +} + +fn true_val() -> bool { + true +} + +#[derive(Debug, Deserialize, Clone, Eq, PartialEq)] +pub(crate) struct AuthenticatorOptions { + /// Indicates that the device is attached to the client and therefore can’t + /// be removed and used on another client. + #[serde(rename = "plat", default)] + pub(crate) platform_device: bool, + /// Indicates that the device is capable of storing keys on the device + /// itself and therefore can satisfy the authenticatorGetAssertion request + /// with allowList parameter not specified or empty. + #[serde(rename = "rk", default)] + pub(crate) resident_key: bool, + + /// Client PIN: + /// If present and set to true, it indicates that the device is capable of + /// accepting a PIN from the client and PIN has been set. + /// If present and set to false, it indicates that the device is capable of + /// accepting a PIN from the client and PIN has not been set yet. + /// If absent, it indicates that the device is not capable of accepting a + /// PIN from the client. + /// Client PIN is one of the ways to do user verification. + #[serde(rename = "clientPin")] + pub(crate) client_pin: Option<bool>, + + /// Indicates that the device is capable of testing user presence. + #[serde(rename = "up", default = "true_val")] + pub(crate) user_presence: bool, + + /// Indicates that the device is capable of verifying the user within + /// itself. For example, devices with UI, biometrics fall into this + /// category. + /// If present and set to true, it indicates that the device is capable of + /// user verification within itself and has been configured. + /// If present and set to false, it indicates that the device is capable of + /// user verification within itself and has not been yet configured. For + /// example, a biometric device that has not yet been configured will + /// return this parameter set to false. + /// If absent, it indicates that the device is not capable of user + /// verification within itself. + /// A device that can only do Client PIN will not return the "uv" parameter. + /// If a device is capable of verifying the user within itself as well as + /// able to do Client PIN, it will return both "uv" and the Client PIN + /// option. + // TODO(MS): My Token (key-ID FIDO2) does return Some(false) here, even though + // it has no built-in verification method. Not to be trusted... + #[serde(rename = "uv")] + pub(crate) user_verification: Option<bool>, +} + +impl Default for AuthenticatorOptions { + fn default() -> Self { + AuthenticatorOptions { + platform_device: false, + resident_key: false, + client_pin: None, + user_presence: true, + user_verification: None, + } + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct AuthenticatorInfo { + pub(crate) versions: Vec<String>, + pub(crate) extensions: Vec<String>, + pub(crate) aaguid: AAGuid, + pub(crate) options: AuthenticatorOptions, + pub(crate) max_msg_size: Option<usize>, + pub(crate) pin_protocols: Vec<u64>, + // CTAP 2.1 + pub(crate) max_credential_count_in_list: Option<usize>, + pub(crate) max_credential_id_length: Option<usize>, + pub(crate) transports: Option<Vec<String>>, + pub(crate) algorithms: Option<Vec<PublicKeyCredentialParameters>>, + pub(crate) max_ser_large_blob_array: Option<u64>, + pub(crate) force_pin_change: Option<bool>, + pub(crate) min_pin_length: Option<u64>, + pub(crate) firmware_version: Option<u64>, + pub(crate) max_cred_blob_length: Option<u64>, + pub(crate) max_rpids_for_set_min_pin_length: Option<u64>, + pub(crate) preferred_platform_uv_attempts: Option<u64>, + pub(crate) uv_modality: Option<u64>, + pub(crate) certifications: Option<BTreeMap<String, u64>>, + pub(crate) remaining_discoverable_credentials: Option<u64>, + pub(crate) vendor_prototype_config_commands: Option<Vec<u64>>, +} + +impl AuthenticatorInfo { + pub fn supports_hmac_secret(&self) -> bool { + self.extensions.contains(&"hmac-secret".to_string()) + } +} + +macro_rules! parse_next_optional_value { + ($name:expr, $map:expr) => { + if $name.is_some() { + return Err(serde::de::Error::duplicate_field("$name")); + } + $name = Some($map.next_value()?); + }; +} + +impl<'de> Deserialize<'de> for AuthenticatorInfo { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + struct AuthenticatorInfoVisitor; + + impl<'de> Visitor<'de> for AuthenticatorInfoVisitor { + type Value = AuthenticatorInfo; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a byte array") + } + + fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error> + where + M: MapAccess<'de>, + { + let mut versions = Vec::new(); + let mut extensions = Vec::new(); + let mut aaguid = None; + let mut options = AuthenticatorOptions::default(); + let mut max_msg_size = None; + let mut pin_protocols = Vec::new(); + let mut max_credential_count_in_list = None; + let mut max_credential_id_length = None; + let mut transports = None; + let mut algorithms = None; + let mut max_ser_large_blob_array = None; + let mut force_pin_change = None; + let mut min_pin_length = None; + let mut firmware_version = None; + let mut max_cred_blob_length = None; + let mut max_rpids_for_set_min_pin_length = None; + let mut preferred_platform_uv_attempts = None; + let mut uv_modality = None; + let mut certifications = None; + let mut remaining_discoverable_credentials = None; + let mut vendor_prototype_config_commands = None; + while let Some(key) = map.next_key()? { + match key { + 0x01 => { + if !versions.is_empty() { + return Err(serde::de::Error::duplicate_field("versions")); + } + versions = map.next_value()?; + } + 0x02 => { + if !extensions.is_empty() { + return Err(serde::de::Error::duplicate_field("extensions")); + } + extensions = map.next_value()?; + } + 0x03 => { + parse_next_optional_value!(aaguid, map); + } + 0x04 => { + options = map.next_value()?; + } + 0x05 => { + parse_next_optional_value!(max_msg_size, map); + } + 0x06 => { + if !pin_protocols.is_empty() { + return Err(serde::de::Error::duplicate_field("pin_protocols")); + } + pin_protocols = map.next_value()?; + } + 0x07 => { + parse_next_optional_value!(max_credential_count_in_list, map); + } + 0x08 => { + parse_next_optional_value!(max_credential_id_length, map); + } + 0x09 => { + parse_next_optional_value!(transports, map); + } + 0x0a => { + parse_next_optional_value!(algorithms, map); + } + 0x0b => { + parse_next_optional_value!(max_ser_large_blob_array, map); + } + 0x0c => { + parse_next_optional_value!(force_pin_change, map); + } + 0x0d => { + parse_next_optional_value!(min_pin_length, map); + } + 0x0e => { + parse_next_optional_value!(firmware_version, map); + } + 0x0f => { + parse_next_optional_value!(max_cred_blob_length, map); + } + 0x10 => { + parse_next_optional_value!(max_rpids_for_set_min_pin_length, map); + } + 0x11 => { + parse_next_optional_value!(preferred_platform_uv_attempts, map); + } + 0x12 => { + parse_next_optional_value!(uv_modality, map); + } + 0x13 => { + parse_next_optional_value!(certifications, map); + } + 0x14 => { + parse_next_optional_value!(remaining_discoverable_credentials, map); + } + 0x15 => { + parse_next_optional_value!(vendor_prototype_config_commands, map); + } + k => { + warn!("GetInfo: unexpected key: {:?}", k); + let _ = map.next_value::<IgnoredAny>()?; + continue; + } + } + } + + if versions.is_empty() { + return Err(M::Error::custom( + "expected at least one version, got none".to_string(), + )); + } + + if let Some(aaguid) = aaguid { + Ok(AuthenticatorInfo { + versions, + extensions, + aaguid, + options, + max_msg_size, + pin_protocols, + max_credential_count_in_list, + max_credential_id_length, + transports, + algorithms, + max_ser_large_blob_array, + force_pin_change, + min_pin_length, + firmware_version, + max_cred_blob_length, + max_rpids_for_set_min_pin_length, + preferred_platform_uv_attempts, + uv_modality, + certifications, + remaining_discoverable_credentials, + vendor_prototype_config_commands, + }) + } else { + Err(M::Error::custom("No AAGuid specified".to_string())) + } + } + } + + deserializer.deserialize_bytes(AuthenticatorInfoVisitor) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::consts::{Capability, HIDCmd, CID_BROADCAST}; + use crate::crypto::COSEAlgorithm; + use crate::transport::device_selector::Device; + use crate::transport::platform::device::IN_HID_RPT_SIZE; + use crate::transport::{hid::HIDDevice, FidoDevice, Nonce}; + use crate::u2ftypes::U2FDevice; + use rand::{thread_rng, RngCore}; + use serde_cbor::de::from_slice; + + // Raw data take from https://github.com/Yubico/python-fido2/blob/master/test/test_ctap2.py + pub const AAGUID_RAW: [u8; 16] = [ + 0xF8, 0xA0, 0x11, 0xF3, 0x8C, 0x0A, 0x4D, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1F, 0x9E, 0xDC, + 0x7D, + ]; + + pub const AUTHENTICATOR_INFO_PAYLOAD: [u8; 89] = [ + 0xa6, // map(6) + 0x01, // unsigned(1) + 0x82, // array(2) + 0x66, // text(6) + 0x55, 0x32, 0x46, 0x5f, 0x56, 0x32, // "U2F_V2" + 0x68, // text(8) + 0x46, 0x49, 0x44, 0x4f, 0x5f, 0x32, 0x5f, 0x30, // "FIDO_2_0" + 0x02, // unsigned(2) + 0x82, // array(2) + 0x63, // text(3) + 0x75, 0x76, 0x6d, // "uvm" + 0x6b, // text(11) + 0x68, 0x6d, 0x61, 0x63, 0x2d, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // "hmac-secret" + 0x03, // unsigned(3) + 0x50, // bytes(16) + 0xf8, 0xa0, 0x11, 0xf3, 0x8c, 0x0a, 0x4d, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1f, 0x9e, 0xdc, + 0x7d, // "\xF8\xA0\u0011\xF3\x8C\nM\u0015\x80\u0006\u0017\u0011\u001F\x9E\xDC}" + 0x04, // unsigned(4) + 0xa4, // map(4) + 0x62, // text(2) + 0x72, 0x6b, // "rk" + 0xf5, // primitive(21) + 0x62, // text(2) + 0x75, 0x70, // "up" + 0xf5, // primitive(21) + 0x64, // text(4) + 0x70, 0x6c, 0x61, 0x74, // "plat" + 0xf4, // primitive(20) + 0x69, // text(9) + 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x50, 0x69, 0x6e, // "clientPin" + 0xf4, // primitive(20) + 0x05, // unsigned(5) + 0x19, 0x04, 0xb0, // unsigned(1200) + 0x06, // unsigned(6) + 0x81, // array(1) + 0x01, // unsigned(1) + ]; + + // Real world example from Yubikey Bio + pub const AUTHENTICATOR_INFO_PAYLOAD_YK_BIO_5C: [u8; 409] = [ + 0xB3, // map(19) + 0x01, // unsigned(1) + 0x84, // array(4) + 0x66, // text(6) + 0x55, 0x32, 0x46, 0x5F, 0x56, 0x32, // "U2F_V2" + 0x68, // text(8) + 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30, // "FIDO_2_0" + 0x6C, // text(12) + 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52, + 0x45, // "FIDO_2_1_PRE" + 0x68, // text(8) + 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x31, // "FIDO_2_1" + 0x02, // unsigned(2) + 0x85, // array(5) + 0x6B, // text(11) + 0x63, 0x72, 0x65, 0x64, 0x50, 0x72, 0x6F, 0x74, 0x65, 0x63, 0x74, // "credProtect" + 0x6B, // text(11) + 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // "hmac-secret" + 0x6C, // text(12) + 0x6C, 0x61, 0x72, 0x67, 0x65, 0x42, 0x6C, 0x6F, 0x62, 0x4B, 0x65, + 0x79, // "largeBlobKey" + 0x68, // text(8) + 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, // "credBlob" + 0x6C, // text(12) + 0x6D, 0x69, 0x6E, 0x50, 0x69, 0x6E, 0x4C, 0x65, 0x6E, 0x67, 0x74, + 0x68, // "minPinLength" + 0x03, // unsigned(3) + 0x50, // bytes(16) + 0xD8, 0x52, 0x2D, 0x9F, 0x57, 0x5B, 0x48, 0x66, 0x88, 0xA9, 0xBA, 0x99, 0xFA, 0x02, 0xF3, + 0x5B, // "\xD8R-\x9FW[Hf\x88\xA9\xBA\x99\xFA\u0002\xF3[" + 0x04, // unsigned(4) + 0xB0, // map(16) + 0x62, // text(2) + 0x72, 0x6B, // "rk" + 0xF5, // primitive(21) + 0x62, // text(2) + 0x75, 0x70, // "up" + 0xF5, // primitive(21) + 0x62, // text(2) + 0x75, 0x76, // "uv" + 0xF5, // primitive(21) + 0x64, // text(4) + 0x70, 0x6C, 0x61, 0x74, // "plat" + 0xF4, // primitive(20) + 0x67, // text(7) + 0x75, 0x76, 0x54, 0x6F, 0x6B, 0x65, 0x6E, // "uvToken" + 0xF5, // primitive(21) + 0x68, // text(8) + 0x61, 0x6C, 0x77, 0x61, 0x79, 0x73, 0x55, 0x76, // "alwaysUv" + 0xF5, // primitive(21) + 0x68, // text(8) + 0x63, 0x72, 0x65, 0x64, 0x4D, 0x67, 0x6D, 0x74, // "credMgmt" + 0xF5, // primitive(21) + 0x69, // text(9) + 0x61, 0x75, 0x74, 0x68, 0x6E, 0x72, 0x43, 0x66, 0x67, // "authnrCfg" + 0xF5, // primitive(21) + 0x69, // text(9) + 0x62, 0x69, 0x6F, 0x45, 0x6E, 0x72, 0x6F, 0x6C, 0x6C, // "bioEnroll" + 0xF5, // primitive(21) + 0x69, // text(9) + 0x63, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x50, 0x69, 0x6E, // "clientPin" + 0xF5, // primitive(21) + 0x6A, // text(10) + 0x6C, 0x61, 0x72, 0x67, 0x65, 0x42, 0x6C, 0x6F, 0x62, 0x73, // "largeBlobs" + 0xF5, // primitive(21) + 0x6E, // text(14) + 0x70, 0x69, 0x6E, 0x55, 0x76, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6F, 0x6B, 0x65, + 0x6E, // "pinUvAuthToken" + 0xF5, // primitive(21) + 0x6F, // text(15) + 0x73, 0x65, 0x74, 0x4D, 0x69, 0x6E, 0x50, 0x49, 0x4E, 0x4C, 0x65, 0x6E, 0x67, 0x74, + 0x68, // "setMinPINLength" + 0xF5, // primitive(21) + 0x70, // text(16) + 0x6D, 0x61, 0x6B, 0x65, 0x43, 0x72, 0x65, 0x64, 0x55, 0x76, 0x4E, 0x6F, 0x74, 0x52, 0x71, + 0x64, // "makeCredUvNotRqd" + 0xF4, // primitive(20) + 0x75, // text(21) + 0x63, 0x72, 0x65, 0x64, 0x65, 0x6E, 0x74, 0x69, 0x61, 0x6C, 0x4D, 0x67, 0x6D, 0x74, 0x50, + 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, // "credentialMgmtPreview" + 0xF5, // primitive(21) + 0x78, 0x1B, // text(27) + 0x75, 0x73, 0x65, 0x72, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, + 0x6E, 0x4D, 0x67, 0x6D, 0x74, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65, + 0x77, // "userVerificationMgmtPreview" + 0xF5, // primitive(21) + 0x05, // unsigned(5) + 0x19, 0x04, 0xB0, // unsigned(1200) + 0x06, // unsigned(6) + 0x82, // array(2) + 0x02, // unsigned(2) + 0x01, // unsigned(1) + 0x07, // unsigned(7) + 0x08, // unsigned(8) + 0x08, // unsigned(8) + 0x18, 0x80, // unsigned(128) + 0x09, // unsigned(9) + 0x81, // array(1) + 0x63, // text(3) + 0x75, 0x73, 0x62, // "usb" + 0x0A, // unsigned(10) + 0x82, // array(2) + 0xA2, // map(2) + 0x63, // text(3) + 0x61, 0x6C, 0x67, // "alg" + 0x26, // negative(6) + 0x64, // text(4) + 0x74, 0x79, 0x70, 0x65, // "type" + 0x6A, // text(10) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // "public-key" + 0xA2, // map(2) + 0x63, // text(3) + 0x61, 0x6C, 0x67, // "alg" + 0x27, // negative(7) + 0x64, // text(4) + 0x74, 0x79, 0x70, 0x65, // "type" + 0x6A, // text(10) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // "public-key" + 0x0B, // unsigned(11) + 0x19, 0x04, 0x00, // unsigned(1024) + 0x0C, // unsigned(12) + 0xF4, // primitive(20) + 0x0D, // unsigned(13) + 0x04, // unsigned(4) + 0x0E, // unsigned(14) + 0x1A, 0x00, 0x05, 0x05, 0x06, // unsigned(328966) + 0x0F, // unsigned(15) + 0x18, 0x20, // unsigned(32) + 0x10, // unsigned(16) + 0x01, // unsigned(1) + 0x11, // unsigned(17) + 0x03, // unsigned(3) + 0x12, // unsigned(18) + 0x02, // unsigned(2) + 0x14, // unsigned(20) + 0x18, 0x18, // unsigned(24) + ]; + + #[test] + fn parse_authenticator_info() { + let authenticator_info: AuthenticatorInfo = + from_slice(&AUTHENTICATOR_INFO_PAYLOAD).unwrap(); + + let expected = AuthenticatorInfo { + versions: vec!["U2F_V2".to_string(), "FIDO_2_0".to_string()], + extensions: vec!["uvm".to_string(), "hmac-secret".to_string()], + aaguid: AAGuid(AAGUID_RAW), + options: AuthenticatorOptions { + platform_device: false, + resident_key: true, + client_pin: Some(false), + user_presence: true, + user_verification: None, + }, + max_msg_size: Some(1200), + pin_protocols: vec![1], + max_credential_count_in_list: None, + max_credential_id_length: None, + transports: None, + algorithms: None, + max_ser_large_blob_array: None, + force_pin_change: None, + min_pin_length: None, + firmware_version: None, + max_cred_blob_length: None, + max_rpids_for_set_min_pin_length: None, + preferred_platform_uv_attempts: None, + uv_modality: None, + certifications: None, + remaining_discoverable_credentials: None, + vendor_prototype_config_commands: None, + }; + + assert_eq!(authenticator_info, expected); + + // Test broken auth info + let mut broken_payload = AUTHENTICATOR_INFO_PAYLOAD.to_vec(); + // Have one more entry in the map + broken_payload[0] += 1; + // Add the additional entry at the back with an invalid key + broken_payload.extend_from_slice(&[ + 0x17, // unsigned(23) -> invalid key-number. CTAP2.1 goes only to 0x15 + 0x6B, // text(11) + 0x69, 0x6E, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x5F, 0x6B, 0x65, 0x79, // "invalid_key" + ]); + + let authenticator_info: AuthenticatorInfo = from_slice(&broken_payload).unwrap(); + assert_eq!(authenticator_info, expected); + } + + #[test] + fn parse_authenticator_info_yk_bio_5c() { + let authenticator_info: AuthenticatorInfo = + from_slice(&AUTHENTICATOR_INFO_PAYLOAD_YK_BIO_5C).unwrap(); + + let expected = AuthenticatorInfo { + versions: vec![ + "U2F_V2".to_string(), + "FIDO_2_0".to_string(), + "FIDO_2_1_PRE".to_string(), + "FIDO_2_1".to_string(), + ], + extensions: vec![ + "credProtect".to_string(), + "hmac-secret".to_string(), + "largeBlobKey".to_string(), + "credBlob".to_string(), + "minPinLength".to_string(), + ], + aaguid: AAGuid([ + 0xd8, 0x52, 0x2d, 0x9f, 0x57, 0x5b, 0x48, 0x66, 0x88, 0xa9, 0xba, 0x99, 0xfa, 0x02, + 0xf3, 0x5b, + ]), + options: AuthenticatorOptions { + platform_device: false, + resident_key: true, + client_pin: Some(true), + user_presence: true, + user_verification: Some(true), + }, + max_msg_size: Some(1200), + pin_protocols: vec![2, 1], + max_credential_count_in_list: Some(8), + max_credential_id_length: Some(128), + transports: Some(vec!["usb".to_string()]), + algorithms: Some(vec![ + PublicKeyCredentialParameters { + alg: COSEAlgorithm::ES256, + }, + PublicKeyCredentialParameters { + alg: COSEAlgorithm::EDDSA, + }, + ]), + max_ser_large_blob_array: Some(1024), + force_pin_change: Some(false), + min_pin_length: Some(4), + firmware_version: Some(328966), + max_cred_blob_length: Some(32), + max_rpids_for_set_min_pin_length: Some(1), + preferred_platform_uv_attempts: Some(3), + uv_modality: Some(2), + certifications: None, + remaining_discoverable_credentials: Some(24), + vendor_prototype_config_commands: None, + }; + + assert_eq!(authenticator_info, expected); + } + + #[test] + fn test_get_info_ctap2_only() { + let mut device = Device::new("commands/get_info").unwrap(); + let nonce = [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]; + + // channel id + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + + // init packet + let mut msg = CID_BROADCAST.to_vec(); + msg.extend(vec![HIDCmd::Init.into(), 0x00, 0x08]); // cmd + bcnt + msg.extend_from_slice(&nonce); + device.add_write(&msg, 0); + + // init_resp packet + let mut msg = CID_BROADCAST.to_vec(); + msg.extend(vec![ + 0x06, /*HIDCmd::Init without TYPE_INIT*/ + 0x00, 0x11, + ]); // cmd + bcnt + msg.extend_from_slice(&nonce); + msg.extend_from_slice(&cid); // new channel id + + // We are setting NMSG, to signal that the device does not support CTAP1 + msg.extend(vec![0x02, 0x04, 0x01, 0x08, 0x01 | 0x04 | 0x08]); // versions + flags (wink+cbor+nmsg) + device.add_read(&msg, 0); + + // ctap2 request + let mut msg = cid.to_vec(); + msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x1]); // cmd + bcnt + msg.extend(vec![0x04]); // authenticatorGetInfo + device.add_write(&msg, 0); + + // ctap2 response + let mut msg = cid.to_vec(); + msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x5A]); // cmd + bcnt + msg.extend(vec![0]); // Status code: Success + msg.extend(&AUTHENTICATOR_INFO_PAYLOAD[0..(IN_HID_RPT_SIZE - 8)]); + device.add_read(&msg, 0); + // Continuation package + let mut msg = cid.to_vec(); + msg.extend(vec![0x00]); // SEQ + msg.extend(&AUTHENTICATOR_INFO_PAYLOAD[(IN_HID_RPT_SIZE - 8)..]); + device.add_read(&msg, 0); + device + .init(Nonce::Use(nonce)) + .expect("Failed to init device"); + + assert_eq!(device.get_cid(), &cid); + + let dev_info = device.get_device_info(); + assert_eq!( + dev_info.cap_flags, + Capability::WINK | Capability::CBOR | Capability::NMSG + ); + + let result = device + .get_authenticator_info() + .expect("Didn't get any authenticator_info"); + let expected = AuthenticatorInfo { + versions: vec!["U2F_V2".to_string(), "FIDO_2_0".to_string()], + extensions: vec!["uvm".to_string(), "hmac-secret".to_string()], + aaguid: AAGuid(AAGUID_RAW), + options: AuthenticatorOptions { + platform_device: false, + resident_key: true, + client_pin: Some(false), + user_presence: true, + user_verification: None, + }, + max_msg_size: Some(1200), + pin_protocols: vec![1], + max_credential_count_in_list: None, + max_credential_id_length: None, + transports: None, + algorithms: None, + max_ser_large_blob_array: None, + force_pin_change: None, + min_pin_length: None, + firmware_version: None, + max_cred_blob_length: None, + max_rpids_for_set_min_pin_length: None, + preferred_platform_uv_attempts: None, + uv_modality: None, + certifications: None, + remaining_discoverable_credentials: None, + vendor_prototype_config_commands: None, + }; + + assert_eq!(result, &expected); + } +} diff --git a/third_party/rust/authenticator/src/ctap2/commands/get_next_assertion.rs b/third_party/rust/authenticator/src/ctap2/commands/get_next_assertion.rs new file mode 100644 index 0000000000..dd5b23edcd --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/get_next_assertion.rs @@ -0,0 +1,53 @@ +use super::{Command, CommandError, RequestCtap2, StatusCode}; +use crate::ctap2::commands::get_assertion::GetAssertionResponse; +use crate::transport::errors::HIDError; +use crate::u2ftypes::U2FDevice; +use serde_cbor::{de::from_slice, Value}; + +#[derive(Debug)] +pub(crate) struct GetNextAssertion; + +impl RequestCtap2 for GetNextAssertion { + type Output = GetAssertionResponse; + + fn command() -> Command { + Command::GetNextAssertion + } + + fn wire_format<Dev>(&self, _dev: &mut Dev) -> Result<Vec<u8>, HIDError> + where + Dev: U2FDevice, + { + Ok(Vec::new()) + } + + fn handle_response_ctap2<Dev>( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result<Self::Output, HIDError> + where + Dev: U2FDevice, + { + if input.is_empty() { + return Err(CommandError::InputTooSmall.into()); + } + + let status: StatusCode = input[0].into(); + debug!("response status code: {:?}", status); + if input.len() > 1 { + if status.is_ok() { + let assertion = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + // TODO(baloo): check assertion response does not have numberOfCredentials + Ok(assertion) + } else { + let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + Err(CommandError::StatusCode(status, Some(data)).into()) + } + } else if status.is_ok() { + Err(CommandError::InputTooSmall.into()) + } else { + Err(CommandError::StatusCode(status, None).into()) + } + } +} diff --git a/third_party/rust/authenticator/src/ctap2/commands/get_version.rs b/third_party/rust/authenticator/src/ctap2/commands/get_version.rs new file mode 100644 index 0000000000..6f088da1bf --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/get_version.rs @@ -0,0 +1,118 @@ +use super::{CommandError, RequestCtap1, Retryable}; +use crate::consts::U2F_VERSION; +use crate::transport::errors::{ApduErrorStatus, HIDError}; +use crate::u2ftypes::CTAP1RequestAPDU; +use crate::u2ftypes::U2FDevice; + +#[allow(non_camel_case_types)] +pub enum U2FInfo { + U2F_V2, +} + +#[derive(Debug)] +// TODO(baloo): if one does not issue U2F_VERSION before makecredentials or getassertion, token +// will return error (ConditionsNotSatified), test this in unit tests +pub struct GetVersion {} + +impl Default for GetVersion { + fn default() -> GetVersion { + GetVersion {} + } +} + +impl RequestCtap1 for GetVersion { + type Output = U2FInfo; + + fn handle_response_ctap1( + &self, + _status: Result<(), ApduErrorStatus>, + input: &[u8], + ) -> Result<Self::Output, Retryable<HIDError>> { + if input.is_empty() { + return Err(Retryable::Error(HIDError::Command( + CommandError::InputTooSmall, + ))); + } + + let expected = String::from("U2F_V2"); + let result = String::from_utf8_lossy(input); + match result { + ref data if data == &expected => Ok(U2FInfo::U2F_V2), + _ => Err(Retryable::Error(HIDError::UnexpectedVersion)), + } + } + + fn ctap1_format<Dev>(&self, _dev: &mut Dev) -> Result<Vec<u8>, HIDError> + where + Dev: U2FDevice, + { + let flags = 0; + + let cmd = U2F_VERSION; + let data = CTAP1RequestAPDU::serialize(cmd, flags, &[])?; + Ok(data) + } +} + +#[cfg(test)] +pub mod tests { + use crate::consts::{Capability, HIDCmd, CID_BROADCAST, SW_NO_ERROR}; + use crate::transport::device_selector::Device; + use crate::transport::{hid::HIDDevice, FidoDevice, Nonce}; + use crate::u2ftypes::U2FDevice; + use rand::{thread_rng, RngCore}; + + #[test] + fn test_get_version_ctap1_only() { + let mut device = Device::new("commands/get_version").unwrap(); + let nonce = [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]; + + // channel id + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + + // init packet + let mut msg = CID_BROADCAST.to_vec(); + msg.extend(&[HIDCmd::Init.into(), 0x00, 0x08]); // cmd + bcnt + msg.extend_from_slice(&nonce); + device.add_write(&msg, 0); + + // init_resp packet + let mut msg = CID_BROADCAST.to_vec(); + msg.extend(vec![ + 0x06, /*HIDCmd::Init without !TYPE_INIT*/ + 0x00, 0x11, + ]); // cmd + bcnt + msg.extend_from_slice(&nonce); + msg.extend_from_slice(&cid); // new channel id + + // We are not setting CBOR, to signal that the device does not support CTAP1 + msg.extend(&[0x02, 0x04, 0x01, 0x08, 0x01]); // versions + flags (wink) + device.add_read(&msg, 0); + + // ctap1 U2F_VERSION request + let mut msg = cid.to_vec(); + msg.extend(&[HIDCmd::Msg.into(), 0x0, 0x7]); // cmd + bcnt + msg.extend(&[0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0]); + device.add_write(&msg, 0); + + // fido response + let mut msg = cid.to_vec(); + msg.extend(&[HIDCmd::Msg.into(), 0x0, 0x08]); // cmd + bcnt + msg.extend(&[0x55, 0x32, 0x46, 0x5f, 0x56, 0x32]); // 'U2F_V2' + msg.extend(&SW_NO_ERROR); + device.add_read(&msg, 0); + + device + .init(Nonce::Use(nonce)) + .expect("Failed to init device"); + + assert_eq!(device.get_cid(), &cid); + + let dev_info = device.get_device_info(); + assert_eq!(dev_info.cap_flags, Capability::WINK); + + let result = device.get_authenticator_info(); + assert!(result.is_none()); + } +} diff --git a/third_party/rust/authenticator/src/ctap2/commands/make_credentials.rs b/third_party/rust/authenticator/src/ctap2/commands/make_credentials.rs new file mode 100644 index 0000000000..7f3fa0bf79 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/make_credentials.rs @@ -0,0 +1,1033 @@ +use super::{ + Command, CommandError, PinAuthCommand, Request, RequestCtap1, RequestCtap2, Retryable, + StatusCode, +}; +use crate::consts::{PARAMETER_SIZE, U2F_REGISTER, U2F_REQUEST_USER_PRESENCE}; +use crate::crypto::{ + parse_u2f_der_certificate, serialize_key, COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, + ECDSACurve, +}; +use crate::ctap2::attestation::{ + AAGuid, AttestationObject, AttestationStatement, AttestationStatementFidoU2F, + AttestedCredentialData, AuthenticatorData, AuthenticatorDataFlags, +}; +use crate::ctap2::client_data::{ + Challenge, ClientDataHash, CollectedClientData, CollectedClientDataWrapper, WebauthnType, +}; +use crate::ctap2::commands::client_pin::{Pin, PinAuth}; +use crate::ctap2::commands::get_assertion::CheckKeyHandle; +use crate::ctap2::server::{ + PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, + RelyingPartyWrapper, User, +}; +use crate::transport::{ + errors::{ApduErrorStatus, HIDError}, + FidoDevice, +}; +use crate::u2ftypes::{CTAP1RequestAPDU, U2FDevice}; +use nom::{ + bytes::complete::{tag, take}, + error::VerboseError, + number::complete::be_u8, +}; +#[cfg(test)] +use serde::Deserialize; +use serde::{ + de::Error as DesError, + ser::{Error as SerError, SerializeMap}, + Serialize, Serializer, +}; +use serde_cbor::{self, de::from_slice, ser, Value}; +use std::fmt; +use std::io; + +#[derive(Debug)] +pub enum MakeCredentialsResult { + CTAP1(Vec<u8>), + CTAP2(AttestationObject, CollectedClientDataWrapper), +} + +#[derive(Copy, Clone, Debug, Serialize)] +#[cfg_attr(test, derive(Deserialize))] +pub struct MakeCredentialsOptions { + #[serde(rename = "rk", skip_serializing_if = "Option::is_none")] + pub resident_key: Option<bool>, + #[serde(rename = "uv", skip_serializing_if = "Option::is_none")] + pub user_verification: Option<bool>, + // TODO(MS): ctap2.1 supports user_presence, but ctap2.0 does not and tokens will error out + // Commands need a version-flag to know what to de/serialize and what to ignore. +} + +impl Default for MakeCredentialsOptions { + fn default() -> Self { + Self { + resident_key: None, + user_verification: None, + } + } +} + +impl MakeCredentialsOptions { + pub(crate) fn has_some(&self) -> bool { + self.resident_key.is_some() || self.user_verification.is_some() + } +} + +pub(crate) trait UserVerification { + fn ask_user_verification(&self) -> bool; +} + +impl UserVerification for MakeCredentialsOptions { + fn ask_user_verification(&self) -> bool { + if let Some(e) = self.user_verification { + e + } else { + false + } + } +} + +#[derive(Debug, Clone, Serialize, Default)] +pub struct MakeCredentialsExtensions { + #[serde(rename = "pinMinLength", skip_serializing_if = "Option::is_none")] + pub pin_min_length: Option<bool>, + #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")] + pub hmac_secret: Option<bool>, +} + +impl MakeCredentialsExtensions { + fn has_extensions(&self) -> bool { + self.pin_min_length.or(self.hmac_secret).is_some() + } +} + +#[derive(Debug, Clone)] +pub struct MakeCredentials { + pub(crate) client_data_wrapper: CollectedClientDataWrapper, + pub(crate) rp: RelyingPartyWrapper, + // Note(baloo): If none -> ctap1 + pub(crate) user: Option<User>, + pub(crate) pub_cred_params: Vec<PublicKeyCredentialParameters>, + pub(crate) exclude_list: Vec<PublicKeyCredentialDescriptor>, + + // https://www.w3.org/TR/webauthn/#client-extension-input + // The client extension input, which is a value that can be encoded in JSON, + // is passed from the WebAuthn Relying Party to the client in the get() or + // create() call, while the CBOR authenticator extension input is passed + // from the client to the authenticator for authenticator extensions during + // the processing of these calls. + pub(crate) extensions: MakeCredentialsExtensions, + pub(crate) options: MakeCredentialsOptions, + pub(crate) pin: Option<Pin>, + pub(crate) pin_auth: Option<PinAuth>, + pub(crate) pin_auth_protocol: Option<u64>, + pub(crate) enterprise_attestation: Option<u64>, +} + +impl MakeCredentials { + pub fn new( + client_data: CollectedClientData, + rp: RelyingPartyWrapper, + user: Option<User>, + pub_cred_params: Vec<PublicKeyCredentialParameters>, + exclude_list: Vec<PublicKeyCredentialDescriptor>, + options: MakeCredentialsOptions, + extensions: MakeCredentialsExtensions, + pin: Option<Pin>, + ) -> Result<Self, HIDError> { + let client_data_wrapper = CollectedClientDataWrapper::new(client_data)?; + Ok(Self { + client_data_wrapper, + rp, + user, + pub_cred_params, + exclude_list, + extensions, + options, + pin, + pin_auth: None, + pin_auth_protocol: None, + enterprise_attestation: None, + }) + } +} + +impl PinAuthCommand for MakeCredentials { + fn pin(&self) -> &Option<Pin> { + &self.pin + } + + fn set_pin(&mut self, pin: Option<Pin>) { + self.pin = pin; + } + + fn pin_auth(&self) -> &Option<PinAuth> { + &self.pin_auth + } + + fn set_pin_auth(&mut self, pin_auth: Option<PinAuth>, pin_auth_protocol: Option<u64>) { + self.pin_auth = pin_auth; + self.pin_auth_protocol = pin_auth_protocol; + } + + fn client_data_hash(&self) -> ClientDataHash { + self.client_data_wrapper.hash() + } + + fn unset_uv_option(&mut self) { + self.options.user_verification = None; + } +} + +impl Serialize for MakeCredentials { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + debug!("Serialize MakeCredentials"); + // Need to define how many elements are going to be in the map + // beforehand + let mut map_len = 4; + if !self.exclude_list.is_empty() { + map_len += 1; + } + if self.extensions.has_extensions() { + map_len += 1; + } + if self.options.has_some() { + map_len += 1; + } + if self.pin_auth.is_some() { + map_len += 1; + } + if self.pin_auth_protocol.is_some() { + map_len += 1; + } + if self.enterprise_attestation.is_some() { + map_len += 1; + } + + let mut map = serializer.serialize_map(Some(map_len))?; + let client_data_hash = self.client_data_hash(); + map.serialize_entry(&0x01, &client_data_hash)?; + match self.rp { + RelyingPartyWrapper::Data(ref d) => { + map.serialize_entry(&0x02, &d)?; + } + _ => { + return Err(S::Error::custom( + "Can't serialize a RelyingParty::Hash for CTAP2", + )); + } + } + map.serialize_entry(&0x03, &self.user)?; + map.serialize_entry(&0x04, &self.pub_cred_params)?; + if !self.exclude_list.is_empty() { + map.serialize_entry(&0x05, &self.exclude_list)?; + } + if self.extensions.has_extensions() { + map.serialize_entry(&0x06, &self.extensions)?; + } + if self.options.has_some() { + map.serialize_entry(&0x07, &self.options)?; + } + if let Some(pin_auth) = &self.pin_auth { + map.serialize_entry(&0x08, &pin_auth)?; + } + if let Some(pin_auth_protocol) = &self.pin_auth_protocol { + map.serialize_entry(&0x09, &pin_auth_protocol)?; + } + if let Some(enterprise_attestation) = self.enterprise_attestation { + map.serialize_entry(&0x0a, &enterprise_attestation)?; + } + map.end() + } +} + +impl Request<MakeCredentialsResult> for MakeCredentials { + fn is_ctap2_request(&self) -> bool { + self.user.is_some() + } +} + +impl RequestCtap1 for MakeCredentials { + type Output = MakeCredentialsResult; + + fn ctap1_format<Dev>(&self, dev: &mut Dev) -> Result<Vec<u8>, HIDError> + where + Dev: io::Read + io::Write + fmt::Debug + FidoDevice, + { + // TODO(MS): Mandatory sanity checks are missing: + // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#u2f-authenticatorMakeCredential-interoperability + // If any of the below conditions is not true, platform errors out with CTAP2_ERR_UNSUPPORTED_OPTION. + // * pubKeyCredParams must use the ES256 algorithm (-7). + // * Options must not include "rk" set to true. + // * Options must not include "uv" set to true. + + let is_already_registered = self + .exclude_list + .iter() + // key-handles in CTAP1 are limited to 255 bytes, but are not limited in CTAP2. + // Filter out key-handles that are too long (can happen if this is a CTAP2-request, + // but the token only speaks CTAP1). If none is found, return an error. + .filter(|exclude_handle| exclude_handle.id.len() < 256) + .map(|exclude_handle| { + let check_command = CheckKeyHandle { + key_handle: exclude_handle.id.as_ref(), + client_data_wrapper: &self.client_data_wrapper, + rp: &self.rp, + }; + let res = dev.send_ctap1(&check_command); + res.is_ok() + }) + .any(|x| x == true); + + if is_already_registered { + // Now we need to send a dummy registration request, to make the token blink + // Spec says "dummy appid and invalid challenge". We use the same, as we do for + // making the token blink upon device selection. + let msg = dummy_make_credentials_cmd()?; + let _ = dev.send_ctap1(&msg); // Ignore answer, return "CrednetialExcluded" + return Err(HIDError::Command(CommandError::StatusCode( + StatusCode::CredentialExcluded, + None, + ))); + } + + let flags = if self.options.ask_user_verification() { + U2F_REQUEST_USER_PRESENCE + } else { + 0 + }; + + let mut register_data = Vec::with_capacity(2 * PARAMETER_SIZE); + if self.is_ctap2_request() { + register_data.extend_from_slice(self.client_data_hash().as_ref()); + } else { + let decoded = base64::decode_config( + &self.client_data_wrapper.client_data.challenge.0, + base64::URL_SAFE_NO_PAD, + ) + .map_err(|_| HIDError::DeviceError)?; // We encoded it, so this should never fail + register_data.extend_from_slice(&decoded); + } + register_data.extend_from_slice(self.rp.hash().as_ref()); + let cmd = U2F_REGISTER; + let apdu = CTAP1RequestAPDU::serialize(cmd, flags, ®ister_data)?; + + Ok(apdu) + } + + fn handle_response_ctap1( + &self, + status: Result<(), ApduErrorStatus>, + input: &[u8], + ) -> Result<Self::Output, Retryable<HIDError>> { + if Err(ApduErrorStatus::ConditionsNotSatisfied) == status { + return Err(Retryable::Retry); + } + if let Err(err) = status { + return Err(Retryable::Error(HIDError::ApduStatus(err))); + } + + if self.is_ctap2_request() { + let parse_register = |input| { + let (rest, _) = tag(&[0x05])(input)?; + let (rest, public_key) = take(65u8)(rest)?; + let (rest, key_handle_len) = be_u8(rest)?; + let (rest, key_handle) = take(key_handle_len)(rest)?; + Ok((rest, public_key, key_handle)) + }; + let (rest, public_key, key_handle) = parse_register(input) + .map_err(|e: nom::Err<VerboseError<_>>| { + error!("error while parsing registration: {:?}", e); + CommandError::Deserializing(DesError::custom("unable to parse registration")) + }) + .map_err(HIDError::Command) + .map_err(Retryable::Error)?; + + let cert_and_sig = parse_u2f_der_certificate(rest) + .map_err(|e| HIDError::Command(CommandError::Crypto(e))) + .map_err(Retryable::Error)?; + + let (x, y) = serialize_key(ECDSACurve::SECP256R1, public_key) + .map_err(|e| HIDError::Command(CommandError::Crypto(e.into()))) + .map_err(Retryable::Error)?; + let credential_public_key = COSEKey { + alg: COSEAlgorithm::ES256, + key: COSEKeyType::EC2(COSEEC2Key { + curve: ECDSACurve::SECP256R1, + x: x.to_vec(), + y: y.to_vec(), + }), + }; + let auth_data = AuthenticatorData { + rp_id_hash: self.rp.hash(), + // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#u2f-authenticatorMakeCredential-interoperability + // "Let flags be a byte whose zeroth bit (bit 0, UP) is set, and whose sixth bit + // (bit 6, AT) is set, and all other bits are zero (bit zero is the least significant bit)" + flags: AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::ATTESTED, + counter: 0, + credential_data: Some(AttestedCredentialData { + aaguid: AAGuid::default(), + credential_id: Vec::from(key_handle), + credential_public_key, + }), + extensions: Default::default(), + }; + + let att_statement = AttestationStatement::FidoU2F(AttestationStatementFidoU2F::new( + cert_and_sig.certificate, + cert_and_sig.signature, + )); + + let attestation_object = AttestationObject { + auth_data, + att_statement, + }; + let client_data = self.client_data_wrapper.clone(); + + Ok(MakeCredentialsResult::CTAP2( + attestation_object, + client_data, + )) + } else { + Ok(MakeCredentialsResult::CTAP1(input.to_vec())) + } + } +} + +impl RequestCtap2 for MakeCredentials { + type Output = MakeCredentialsResult; + + fn command() -> Command { + Command::MakeCredentials + } + + fn wire_format<Dev>(&self, _dev: &mut Dev) -> Result<Vec<u8>, HIDError> + where + Dev: U2FDevice + io::Read + io::Write + fmt::Debug, + { + Ok(ser::to_vec(&self).map_err(CommandError::Serializing)?) + } + + fn handle_response_ctap2<Dev>( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result<Self::Output, HIDError> + where + Dev: U2FDevice + io::Read + io::Write + fmt::Debug, + { + if input.is_empty() { + return Err(HIDError::Command(CommandError::InputTooSmall)); + } + + let status: StatusCode = input[0].into(); + debug!("response status code: {:?}", status); + if input.len() > 1 { + if status.is_ok() { + let attestation = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + let client_data = self.client_data_wrapper.clone(); + Ok(MakeCredentialsResult::CTAP2(attestation, client_data)) + } else { + let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + Err(HIDError::Command(CommandError::StatusCode( + status, + Some(data), + ))) + } + } else if status.is_ok() { + Err(HIDError::Command(CommandError::InputTooSmall)) + } else { + Err(HIDError::Command(CommandError::StatusCode(status, None))) + } + } +} + +pub(crate) fn dummy_make_credentials_cmd() -> Result<MakeCredentials, HIDError> { + MakeCredentials::new( + CollectedClientData { + webauthn_type: WebauthnType::Create, + challenge: Challenge::new(vec![0, 1, 2, 3, 4]), + origin: String::new(), + cross_origin: false, + token_binding: None, + }, + RelyingPartyWrapper::Data(RelyingParty { + id: String::from("make.me.blink"), + ..Default::default() + }), + Some(User { + id: vec![0], + name: Some(String::from("make.me.blink")), + ..Default::default() + }), + vec![PublicKeyCredentialParameters { + alg: crate::COSEAlgorithm::ES256, + }], + vec![], + MakeCredentialsOptions::default(), + MakeCredentialsExtensions::default(), + None, + ) +} + +#[cfg(test)] +pub mod test { + use super::{MakeCredentials, MakeCredentialsOptions, MakeCredentialsResult}; + use crate::crypto::{COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, ECDSACurve}; + use crate::ctap2::attestation::{ + AAGuid, AttestationCertificate, AttestationObject, AttestationStatement, + AttestationStatementFidoU2F, AttestationStatementPacked, AttestedCredentialData, + AuthenticatorData, AuthenticatorDataFlags, Signature, + }; + use crate::ctap2::client_data::{Challenge, CollectedClientData, TokenBinding, WebauthnType}; + use crate::ctap2::commands::{RequestCtap1, RequestCtap2}; + use crate::ctap2::server::RpIdHash; + use crate::ctap2::server::{ + PublicKeyCredentialParameters, RelyingParty, RelyingPartyWrapper, User, + }; + use crate::transport::device_selector::Device; + use crate::transport::hid::HIDDevice; + use serde_bytes::ByteBuf; + + #[test] + fn test_make_credentials_ctap2() { + let req = MakeCredentials::new( + CollectedClientData { + webauthn_type: WebauthnType::Create, + challenge: Challenge::from(vec![0x00, 0x01, 0x02, 0x03]), + origin: String::from("example.com"), + cross_origin: false, + token_binding: Some(TokenBinding::Present(String::from("AAECAw"))), + }, + RelyingPartyWrapper::Data(RelyingParty { + id: String::from("example.com"), + name: Some(String::from("Acme")), + icon: None, + }), + Some(User { + id: base64::decode_config( + "MIIBkzCCATigAwIBAjCCAZMwggE4oAMCAQIwggGTMII=", + base64::URL_SAFE_NO_PAD, + ) + .unwrap(), + icon: Some("https://pics.example.com/00/p/aBjjjpqPb.png".to_string()), + name: Some(String::from("johnpsmith@example.com")), + display_name: Some(String::from("John P. Smith")), + }), + vec![ + PublicKeyCredentialParameters { + alg: COSEAlgorithm::ES256, + }, + PublicKeyCredentialParameters { + alg: COSEAlgorithm::RS256, + }, + ], + Vec::new(), + MakeCredentialsOptions { + resident_key: Some(true), + user_verification: None, + }, + Default::default(), + None, + ) + .expect("Failed to create MakeCredentials"); + + let mut device = Device::new("commands/make_credentials").unwrap(); // not really used (all functions ignore it) + let req_serialized = req + .wire_format(&mut device) + .expect("Failed to serialize MakeCredentials request"); + assert_eq!(req_serialized, MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP2); + let (attestation_object, _collected_client_data) = match req + .handle_response_ctap2(&mut device, &MAKE_CREDENTIALS_SAMPLE_RESPONSE_CTAP2) + .expect("Failed to handle CTAP2 response") + { + MakeCredentialsResult::CTAP2(attestation_object, _collected_client_data) => { + (attestation_object, _collected_client_data) + } + _ => panic!("Got CTAP1 Result, but CTAP2 expected"), + }; + + let expected = AttestationObject { + auth_data: AuthenticatorData { + rp_id_hash: RpIdHash::from(&[ + 0xc2, 0x89, 0xc5, 0xca, 0x9b, 0x04, 0x60, 0xf9, 0x34, 0x6a, 0xb4, 0xe4, 0x2d, + 0x84, 0x27, 0x43, 0x40, 0x4d, 0x31, 0xf4, 0x84, 0x68, 0x25, 0xa6, 0xd0, 0x65, + 0xbe, 0x59, 0x7a, 0x87, 0x5, 0x1d, + ]) + .unwrap(), + flags: AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::ATTESTED, + counter: 11, + credential_data: Some(AttestedCredentialData { + aaguid: AAGuid::from(&[ + 0xf8, 0xa0, 0x11, 0xf3, 0x8c, 0x0a, 0x4d, 0x15, 0x80, 0x06, 0x17, 0x11, + 0x1f, 0x9e, 0xdc, 0x7d, + ]) + .unwrap(), + credential_id: vec![ + 0x89, 0x59, 0xce, 0xad, 0x5b, 0x5c, 0x48, 0x16, 0x4e, 0x8a, 0xbc, 0xd6, + 0xd9, 0x43, 0x5c, 0x6f, + ], + credential_public_key: COSEKey { + alg: COSEAlgorithm::ES256, + key: COSEKeyType::EC2(COSEEC2Key { + curve: ECDSACurve::SECP256R1, + x: vec![ + 0xA5, 0xFD, 0x5C, 0xE1, 0xB1, 0xC4, 0x58, 0xC5, 0x30, 0xA5, 0x4F, + 0xA6, 0x1B, 0x31, 0xBF, 0x6B, 0x04, 0xBE, 0x8B, 0x97, 0xAF, 0xDE, + 0x54, 0xDD, 0x8C, 0xBB, 0x69, 0x27, 0x5A, 0x8A, 0x1B, 0xE1, + ], + y: vec![ + 0xFA, 0x3A, 0x32, 0x31, 0xDD, 0x9D, 0xEE, 0xD9, 0xD1, 0x89, 0x7B, + 0xE5, 0xA6, 0x22, 0x8C, 0x59, 0x50, 0x1E, 0x4B, 0xCD, 0x12, 0x97, + 0x5D, 0x3D, 0xFF, 0x73, 0x0F, 0x01, 0x27, 0x8E, 0xA6, 0x1C, + ], + }), + }, + }), + extensions: Default::default(), + }, + att_statement: AttestationStatement::Packed(AttestationStatementPacked { + alg: COSEAlgorithm::ES256, + sig: Signature(ByteBuf::from([ + 0x30, 0x45, 0x02, 0x20, 0x13, 0xf7, 0x3c, 0x5d, 0x9d, 0x53, 0x0e, 0x8c, 0xc1, + 0x5c, 0xc9, 0xbd, 0x96, 0xad, 0x58, 0x6d, 0x39, 0x36, 0x64, 0xe4, 0x62, 0xd5, + 0xf0, 0x56, 0x12, 0x35, 0xe6, 0x35, 0x0f, 0x2b, 0x72, 0x89, 0x02, 0x21, 0x00, + 0x90, 0x35, 0x7f, 0xf9, 0x10, 0xcc, 0xb5, 0x6a, 0xc5, 0xb5, 0x96, 0x51, 0x19, + 0x48, 0x58, 0x1c, 0x8f, 0xdd, 0xb4, 0xa2, 0xb7, 0x99, 0x59, 0x94, 0x80, 0x78, + 0xb0, 0x9f, 0x4b, 0xdc, 0x62, 0x29, + ])), + attestation_cert: vec![AttestationCertificate(vec![ + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, + 0x02, 0x09, 0x00, 0x85, 0x9b, 0x72, 0x6c, 0xb2, 0x4b, 0x4c, 0x29, 0x30, 0x0a, + 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30, 0x47, 0x31, + 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, + 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75, 0x62, + 0x69, 0x63, 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, 0x06, + 0x03, 0x55, 0x04, 0x0b, 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, + 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x1e, 0x17, 0x0d, 0x31, 0x36, 0x31, 0x32, + 0x30, 0x34, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x17, 0x0d, 0x32, 0x36, + 0x31, 0x32, 0x30, 0x32, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x30, 0x47, + 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, + 0x31, 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75, + 0x62, 0x69, 0x63, 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, + 0x06, 0x03, 0x55, 0x04, 0x0b, 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, + 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, + 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, + 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, + 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xad, 0x11, 0xeb, 0x0e, 0x88, 0x52, + 0xe5, 0x3a, 0xd5, 0xdf, 0xed, 0x86, 0xb4, 0x1e, 0x61, 0x34, 0xa1, 0x8e, 0xc4, + 0xe1, 0xaf, 0x8f, 0x22, 0x1a, 0x3c, 0x7d, 0x6e, 0x63, 0x6c, 0x80, 0xea, 0x13, + 0xc3, 0xd5, 0x04, 0xff, 0x2e, 0x76, 0x21, 0x1b, 0xb4, 0x45, 0x25, 0xb1, 0x96, + 0xc4, 0x4c, 0xb4, 0x84, 0x99, 0x79, 0xcf, 0x6f, 0x89, 0x6e, 0xcd, 0x2b, 0xb8, + 0x60, 0xde, 0x1b, 0xf4, 0x37, 0x6b, 0xa3, 0x0d, 0x30, 0x0b, 0x30, 0x09, 0x06, + 0x03, 0x55, 0x1d, 0x13, 0x04, 0x02, 0x30, 0x00, 0x30, 0x0a, 0x06, 0x08, 0x2a, + 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x49, 0x00, 0x30, 0x46, 0x02, + 0x21, 0x00, 0xe9, 0xa3, 0x9f, 0x1b, 0x03, 0x19, 0x75, 0x25, 0xf7, 0x37, 0x3e, + 0x10, 0xce, 0x77, 0xe7, 0x80, 0x21, 0x73, 0x1b, 0x94, 0xd0, 0xc0, 0x3f, 0x3f, + 0xda, 0x1f, 0xd2, 0x2d, 0xb3, 0xd0, 0x30, 0xe7, 0x02, 0x21, 0x00, 0xc4, 0xfa, + 0xec, 0x34, 0x45, 0xa8, 0x20, 0xcf, 0x43, 0x12, 0x9c, 0xdb, 0x00, 0xaa, 0xbe, + 0xfd, 0x9a, 0xe2, 0xd8, 0x74, 0xf9, 0xc5, 0xd3, 0x43, 0xcb, 0x2f, 0x11, 0x3d, + 0xa2, 0x37, 0x23, 0xf3, + ])], + }), + }; + + assert_eq!(attestation_object, expected); + } + + #[test] + fn test_make_credentials_ctap1() { + let req = MakeCredentials::new( + CollectedClientData { + webauthn_type: WebauthnType::Create, + challenge: Challenge::new(vec![0x00, 0x01, 0x02, 0x03]), + origin: String::from("example.com"), + cross_origin: false, + token_binding: Some(TokenBinding::Present(String::from("AAECAw"))), + }, + RelyingPartyWrapper::Data(RelyingParty { + id: String::from("example.com"), + name: Some(String::from("Acme")), + icon: None, + }), + Some(User { + id: base64::decode_config( + "MIIBkzCCATigAwIBAjCCAZMwggE4oAMCAQIwggGTMII=", + base64::URL_SAFE_NO_PAD, + ) + .unwrap(), + icon: Some("https://pics.example.com/00/p/aBjjjpqPb.png".to_string()), + name: Some(String::from("johnpsmith@example.com")), + display_name: Some(String::from("John P. Smith")), + }), + vec![ + PublicKeyCredentialParameters { + alg: COSEAlgorithm::ES256, + }, + PublicKeyCredentialParameters { + alg: COSEAlgorithm::RS256, + }, + ], + Vec::new(), + MakeCredentialsOptions { + resident_key: Some(true), + user_verification: None, + }, + Default::default(), + None, + ) + .expect("Failed to create MakeCredentials"); + + let mut device = Device::new("commands/make_credentials").unwrap(); // not really used (all functions ignore it) + let req_serialized = req + .ctap1_format(&mut device) + .expect("Failed to serialize MakeCredentials request"); + assert_eq!( + req_serialized, MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP1, + "\nGot: {:X?}\nExpected: {:X?}", + req_serialized, MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP1 + ); + let (attestation_object, _collected_client_data) = match req + .handle_response_ctap1(Ok(()), &MAKE_CREDENTIALS_SAMPLE_RESPONSE_CTAP1) + .expect("Failed to handle CTAP1 response") + { + MakeCredentialsResult::CTAP2(attestation_object, _collected_client_data) => { + (attestation_object, _collected_client_data) + } + _ => panic!("Got CTAP1 Result, but CTAP2 expected"), + }; + + let expected = AttestationObject { + auth_data: AuthenticatorData { + rp_id_hash: RpIdHash::from(&[ + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, + 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, + 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, + ]) + .unwrap(), + flags: AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::ATTESTED, + counter: 0, + credential_data: Some(AttestedCredentialData { + aaguid: AAGuid::default(), + credential_id: vec![ + 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, + 0x35, 0xEF, 0xAA, 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, + 0x71, 0x7D, 0xA4, 0x85, 0x34, 0xC8, 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, + 0x5F, 0x50, 0xB5, 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD, 0x39, 0x6B, 0x64, + 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08, + 0xFE, 0x42, 0x00, 0x38, + ], + credential_public_key: COSEKey { + alg: COSEAlgorithm::ES256, + key: COSEKeyType::EC2(COSEEC2Key { + curve: ECDSACurve::SECP256R1, + x: vec![ + 0xE8, 0x76, 0x25, 0x89, 0x6E, 0xE4, 0xE4, 0x6D, 0xC0, 0x32, 0x76, + 0x6E, 0x80, 0x87, 0x96, 0x2F, 0x36, 0xDF, 0x9D, 0xFE, 0x8B, 0x56, + 0x7F, 0x37, 0x63, 0x01, 0x5B, 0x19, 0x90, 0xA6, 0x0E, 0x14, + ], + y: vec![ + 0x27, 0xDE, 0x61, 0x2D, 0x66, 0x41, 0x8B, 0xDA, 0x19, 0x50, 0x58, + 0x1E, 0xBC, 0x5C, 0x8C, 0x1D, 0xAD, 0x71, 0x0C, 0xB1, 0x4C, 0x22, + 0xF8, 0xC9, 0x70, 0x45, 0xF4, 0x61, 0x2F, 0xB2, 0x0C, 0x91, + ], + }), + }, + }), + extensions: Default::default(), + }, + att_statement: AttestationStatement::FidoU2F(AttestationStatementFidoU2F { + sig: Signature(ByteBuf::from([ + 0x30, 0x45, 0x02, 0x20, 0x32, 0x47, 0x79, 0xC6, 0x8F, 0x33, 0x80, 0x28, 0x8A, + 0x11, 0x97, 0xB6, 0x09, 0x5F, 0x7A, 0x6E, 0xB9, 0xB1, 0xB1, 0xC1, 0x27, 0xF6, + 0x6A, 0xE1, 0x2A, 0x99, 0xFE, 0x85, 0x32, 0xEC, 0x23, 0xB9, 0x02, 0x21, 0x00, + 0xE3, 0x95, 0x16, 0xAC, 0x4D, 0x61, 0xEE, 0x64, 0x04, 0x4D, 0x50, 0xB4, 0x15, + 0xA6, 0xA4, 0xD4, 0xD8, 0x4B, 0xA6, 0xD8, 0x95, 0xCB, 0x5A, 0xB7, 0xA1, 0xAA, + 0x7D, 0x08, 0x1D, 0xE3, 0x41, 0xFA, + ])), + attestation_cert: vec![AttestationCertificate(vec![ + 0x30, 0x82, 0x02, 0x4A, 0x30, 0x82, 0x01, 0x32, 0xA0, 0x03, 0x02, 0x01, 0x02, + 0x02, 0x04, 0x04, 0x6C, 0x88, 0x22, 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, + 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B, 0x05, 0x00, 0x30, 0x2E, 0x31, 0x2C, 0x30, + 0x2A, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x23, 0x59, 0x75, 0x62, 0x69, 0x63, + 0x6F, 0x20, 0x55, 0x32, 0x46, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, + 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6C, 0x20, 0x34, 0x35, 0x37, 0x32, 0x30, + 0x30, 0x36, 0x33, 0x31, 0x30, 0x20, 0x17, 0x0D, 0x31, 0x34, 0x30, 0x38, 0x30, + 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5A, 0x18, 0x0F, 0x32, 0x30, 0x35, + 0x30, 0x30, 0x39, 0x30, 0x34, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5A, 0x30, + 0x2C, 0x31, 0x2A, 0x30, 0x28, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C, 0x21, 0x59, + 0x75, 0x62, 0x69, 0x63, 0x6F, 0x20, 0x55, 0x32, 0x46, 0x20, 0x45, 0x45, 0x20, + 0x53, 0x65, 0x72, 0x69, 0x61, 0x6C, 0x20, 0x32, 0x34, 0x39, 0x31, 0x38, 0x32, + 0x33, 0x32, 0x34, 0x37, 0x37, 0x30, 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2A, + 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01, 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, + 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0x3C, 0xCA, 0xB9, 0x2C, 0xCB, 0x97, + 0x28, 0x7E, 0xE8, 0xE6, 0x39, 0x43, 0x7E, 0x21, 0xFC, 0xD6, 0xB6, 0xF1, 0x65, + 0xB2, 0xD5, 0xA3, 0xF3, 0xDB, 0x13, 0x1D, 0x31, 0xC1, 0x6B, 0x74, 0x2B, 0xB4, + 0x76, 0xD8, 0xD1, 0xE9, 0x90, 0x80, 0xEB, 0x54, 0x6C, 0x9B, 0xBD, 0xF5, 0x56, + 0xE6, 0x21, 0x0F, 0xD4, 0x27, 0x85, 0x89, 0x9E, 0x78, 0xCC, 0x58, 0x9E, 0xBE, + 0x31, 0x0F, 0x6C, 0xDB, 0x9F, 0xF4, 0xA3, 0x3B, 0x30, 0x39, 0x30, 0x22, 0x06, + 0x09, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0xC4, 0x0A, 0x02, 0x04, 0x15, 0x31, + 0x2E, 0x33, 0x2E, 0x36, 0x2E, 0x31, 0x2E, 0x34, 0x2E, 0x31, 0x2E, 0x34, 0x31, + 0x34, 0x38, 0x32, 0x2E, 0x31, 0x2E, 0x32, 0x30, 0x13, 0x06, 0x0B, 0x2B, 0x06, + 0x01, 0x04, 0x01, 0x82, 0xE5, 0x1C, 0x02, 0x01, 0x01, 0x04, 0x04, 0x03, 0x02, + 0x04, 0x30, 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, + 0x01, 0x0B, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, 0x00, 0x9F, 0x9B, 0x05, 0x22, + 0x48, 0xBC, 0x4C, 0xF4, 0x2C, 0xC5, 0x99, 0x1F, 0xCA, 0xAB, 0xAC, 0x9B, 0x65, + 0x1B, 0xBE, 0x5B, 0xDC, 0xDC, 0x8E, 0xF0, 0xAD, 0x2C, 0x1C, 0x1F, 0xFB, 0x36, + 0xD1, 0x87, 0x15, 0xD4, 0x2E, 0x78, 0xB2, 0x49, 0x22, 0x4F, 0x92, 0xC7, 0xE6, + 0xE7, 0xA0, 0x5C, 0x49, 0xF0, 0xE7, 0xE4, 0xC8, 0x81, 0xBF, 0x2E, 0x94, 0xF4, + 0x5E, 0x4A, 0x21, 0x83, 0x3D, 0x74, 0x56, 0x85, 0x1D, 0x0F, 0x6C, 0x14, 0x5A, + 0x29, 0x54, 0x0C, 0x87, 0x4F, 0x30, 0x92, 0xC9, 0x34, 0xB4, 0x3D, 0x22, 0x2B, + 0x89, 0x62, 0xC0, 0xF4, 0x10, 0xCE, 0xF1, 0xDB, 0x75, 0x89, 0x2A, 0xF1, 0x16, + 0xB4, 0x4A, 0x96, 0xF5, 0xD3, 0x5A, 0xDE, 0xA3, 0x82, 0x2F, 0xC7, 0x14, 0x6F, + 0x60, 0x04, 0x38, 0x5B, 0xCB, 0x69, 0xB6, 0x5C, 0x99, 0xE7, 0xEB, 0x69, 0x19, + 0x78, 0x67, 0x03, 0xC0, 0xD8, 0xCD, 0x41, 0xE8, 0xF7, 0x5C, 0xCA, 0x44, 0xAA, + 0x8A, 0xB7, 0x25, 0xAD, 0x8E, 0x79, 0x9F, 0xF3, 0xA8, 0x69, 0x6A, 0x6F, 0x1B, + 0x26, 0x56, 0xE6, 0x31, 0xB1, 0xE4, 0x01, 0x83, 0xC0, 0x8F, 0xDA, 0x53, 0xFA, + 0x4A, 0x8F, 0x85, 0xA0, 0x56, 0x93, 0x94, 0x4A, 0xE1, 0x79, 0xA1, 0x33, 0x9D, + 0x00, 0x2D, 0x15, 0xCA, 0xBD, 0x81, 0x00, 0x90, 0xEC, 0x72, 0x2E, 0xF5, 0xDE, + 0xF9, 0x96, 0x5A, 0x37, 0x1D, 0x41, 0x5D, 0x62, 0x4B, 0x68, 0xA2, 0x70, 0x7C, + 0xAD, 0x97, 0xBC, 0xDD, 0x17, 0x85, 0xAF, 0x97, 0xE2, 0x58, 0xF3, 0x3D, 0xF5, + 0x6A, 0x03, 0x1A, 0xA0, 0x35, 0x6D, 0x8E, 0x8D, 0x5E, 0xBC, 0xAD, 0xC7, 0x4E, + 0x07, 0x16, 0x36, 0xC6, 0xB1, 0x10, 0xAC, 0xE5, 0xCC, 0x9B, 0x90, 0xDF, 0xEA, + 0xCA, 0xE6, 0x40, 0xFF, 0x1B, 0xB0, 0xF1, 0xFE, 0x5D, 0xB4, 0xEF, 0xF7, 0xA9, + 0x5F, 0x06, 0x07, 0x33, 0xF5, + ])], + }), + }; + + assert_eq!(attestation_object, expected); + } + + #[rustfmt::skip] + pub const MAKE_CREDENTIALS_SAMPLE_RESPONSE_CTAP2: [u8; 660] = [ + 0x00, // status = success + 0xa3, // map(3) + 0x01, // unsigned(1) + 0x66, // text(6) + 0x70, 0x61, 0x63, 0x6b, 0x65, 0x64, // "packed" + 0x02, // unsigned(2) + 0x58, 0x94, // bytes(148) + // authData + 0xc2, 0x89, 0xc5, 0xca, 0x9b, 0x04, 0x60, 0xf9, 0x34, 0x6a, 0xb4, 0xe4, 0x2d, 0x84, 0x27, // rp_id_hash + 0x43, 0x40, 0x4d, 0x31, 0xf4, 0x84, 0x68, 0x25, 0xa6, 0xd0, 0x65, 0xbe, 0x59, 0x7a, 0x87, // rp_id_hash + 0x05, 0x1d, // rp_id_hash + 0x41, // authData Flags + 0x00, 0x00, 0x00, 0x0b, // authData counter + 0xf8, 0xa0, 0x11, 0xf3, 0x8c, 0x0a, 0x4d, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1f, 0x9e, 0xdc, 0x7d, // AAGUID + 0x00, 0x10, // credential id length + 0x89, 0x59, 0xce, 0xad, 0x5b, 0x5c, 0x48, 0x16, 0x4e, 0x8a, 0xbc, 0xd6, 0xd9, 0x43, 0x5c, 0x6f, // credential id + // credential public key + 0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20, 0xa5, 0xfd, 0x5c, 0xe1, 0xb1, 0xc4, + 0x58, 0xc5, 0x30, 0xa5, 0x4f, 0xa6, 0x1b, 0x31, 0xbf, 0x6b, 0x04, 0xbe, 0x8b, 0x97, 0xaf, 0xde, + 0x54, 0xdd, 0x8c, 0xbb, 0x69, 0x27, 0x5a, 0x8a, 0x1b, 0xe1, 0x22, 0x58, 0x20, 0xfa, 0x3a, 0x32, + 0x31, 0xdd, 0x9d, 0xee, 0xd9, 0xd1, 0x89, 0x7b, 0xe5, 0xa6, 0x22, 0x8c, 0x59, 0x50, 0x1e, 0x4b, + 0xcd, 0x12, 0x97, 0x5d, 0x3d, 0xff, 0x73, 0x0f, 0x01, 0x27, 0x8e, 0xa6, 0x1c, + 0x03, // unsigned(3) + 0xa3, // map(3) + 0x63, // text(3) + 0x61, 0x6c, 0x67, // "alg" + 0x26, // -7 (ES256) + 0x63, // text(3) + 0x73, 0x69, 0x67, // "sig" + 0x58, 0x47, // bytes(71) + 0x30, 0x45, 0x02, 0x20, 0x13, 0xf7, 0x3c, 0x5d, 0x9d, 0x53, 0x0e, 0x8c, 0xc1, 0x5c, 0xc9, // signature + 0xbd, 0x96, 0xad, 0x58, 0x6d, 0x39, 0x36, 0x64, 0xe4, 0x62, 0xd5, 0xf0, 0x56, 0x12, 0x35, // .. + 0xe6, 0x35, 0x0f, 0x2b, 0x72, 0x89, 0x02, 0x21, 0x00, 0x90, 0x35, 0x7f, 0xf9, 0x10, 0xcc, // .. + 0xb5, 0x6a, 0xc5, 0xb5, 0x96, 0x51, 0x19, 0x48, 0x58, 0x1c, 0x8f, 0xdd, 0xb4, 0xa2, 0xb7, // .. + 0x99, 0x59, 0x94, 0x80, 0x78, 0xb0, 0x9f, 0x4b, 0xdc, 0x62, 0x29, // .. + 0x63, // text(3) + 0x78, 0x35, 0x63, // "x5c" + 0x81, // array(1) + 0x59, 0x01, 0x97, // bytes(407) + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, //certificate... + 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x09, 0x00, 0x85, 0x9b, 0x72, 0x6c, 0xb2, 0x4b, + 0x4c, 0x29, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30, + 0x47, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, + 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75, 0x62, 0x69, 0x63, + 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, 0x06, 0x03, 0x55, 0x04, 0x0b, + 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, + 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x1e, 0x17, + 0x0d, 0x31, 0x36, 0x31, 0x32, 0x30, 0x34, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x17, + 0x0d, 0x32, 0x36, 0x31, 0x32, 0x30, 0x32, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x30, + 0x47, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, + 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75, 0x62, 0x69, 0x63, + 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, 0x06, 0x03, 0x55, 0x04, 0x0b, + 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, + 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x59, 0x30, + 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, + 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xad, 0x11, 0xeb, 0x0e, 0x88, 0x52, + 0xe5, 0x3a, 0xd5, 0xdf, 0xed, 0x86, 0xb4, 0x1e, 0x61, 0x34, 0xa1, 0x8e, 0xc4, 0xe1, 0xaf, + 0x8f, 0x22, 0x1a, 0x3c, 0x7d, 0x6e, 0x63, 0x6c, 0x80, 0xea, 0x13, 0xc3, 0xd5, 0x04, 0xff, + 0x2e, 0x76, 0x21, 0x1b, 0xb4, 0x45, 0x25, 0xb1, 0x96, 0xc4, 0x4c, 0xb4, 0x84, 0x99, 0x79, + 0xcf, 0x6f, 0x89, 0x6e, 0xcd, 0x2b, 0xb8, 0x60, 0xde, 0x1b, 0xf4, 0x37, 0x6b, 0xa3, 0x0d, + 0x30, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x04, 0x02, 0x30, 0x00, 0x30, 0x0a, + 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x49, 0x00, 0x30, 0x46, + 0x02, 0x21, 0x00, 0xe9, 0xa3, 0x9f, 0x1b, 0x03, 0x19, 0x75, 0x25, 0xf7, 0x37, 0x3e, 0x10, + 0xce, 0x77, 0xe7, 0x80, 0x21, 0x73, 0x1b, 0x94, 0xd0, 0xc0, 0x3f, 0x3f, 0xda, 0x1f, 0xd2, + 0x2d, 0xb3, 0xd0, 0x30, 0xe7, 0x02, 0x21, 0x00, 0xc4, 0xfa, 0xec, 0x34, 0x45, 0xa8, 0x20, + 0xcf, 0x43, 0x12, 0x9c, 0xdb, 0x00, 0xaa, 0xbe, 0xfd, 0x9a, 0xe2, 0xd8, 0x74, 0xf9, 0xc5, + 0xd3, 0x43, 0xcb, 0x2f, 0x11, 0x3d, 0xa2, 0x37, 0x23, 0xf3, + ]; + + #[rustfmt::skip] + pub const MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP2: [u8; 260] = [ + // NOTE: This has been taken from CTAP2.0 spec, but the clientDataHash has been replaced + // to be able to operate with known values for CollectedClientData (spec doesn't say + // what values led to the provided example hash (see client_data.rs)) + 0xa5, // map(5) + 0x01, // unsigned(1) - clientDataHash + 0x58, 0x20, // bytes(32) + 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, // hash + 0x32, 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, // hash + 0x10, 0x87, 0x54, 0xc3, 0x2d, 0x80, // hash + 0x02, // unsigned(2) - rp + 0xa2, // map(2) Replace line below with this one, once RelyingParty supports "name" + 0x62, // text(2) + 0x69, 0x64, // "id" + 0x6b, // text(11) + 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, // "example.com" + 0x64, // text(4) + 0x6e, 0x61, 0x6d, 0x65, // "name" + 0x64, // text(4) + 0x41, 0x63, 0x6d, 0x65, // "Acme" + 0x03, // unsigned(3) - user + 0xa4, // map(4) + 0x62, // text(2) + 0x69, 0x64, // "id" + 0x58, 0x20, // bytes(32) + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, // userid + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, // ... + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, // ... + 0x64, // text(4) + 0x69, 0x63, 0x6f, 0x6e, // "icon" + 0x78, 0x2b, // text(43) + 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, // "https://pics.example.com/00/p/aBjjjpqPb.png" + 0x2f, 0x70, 0x69, 0x63, 0x73, 0x2e, 0x65, 0x78, // .. + 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x30, 0x30, 0x2f, 0x70, // .. + 0x2f, 0x61, 0x42, 0x6a, 0x6a, 0x6a, 0x70, 0x71, 0x50, 0x62, 0x2e, 0x70, 0x6e, 0x67, // .. + 0x64, // text(4) + 0x6e, 0x61, 0x6d, 0x65, // "name" + 0x76, // text(22) + 0x6a, 0x6f, 0x68, 0x6e, 0x70, 0x73, 0x6d, 0x69, 0x74, // "johnpsmith@example.com" + 0x68, 0x40, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, // ... + 0x6b, // text(11) + 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, // "displayName" + 0x6d, // text(13) + 0x4a, 0x6f, 0x68, 0x6e, 0x20, 0x50, 0x2e, 0x20, 0x53, 0x6d, 0x69, 0x74, 0x68, // "John P. Smith" + 0x04, // unsigned(4) - pubKeyCredParams + 0x82, // array(2) + 0xa2, // map(2) + 0x63, // text(3) + 0x61, 0x6c, 0x67, // "alg" + 0x26, // -7 (ES256) + 0x64, // text(4) + 0x74, 0x79, 0x70, 0x65, // "type" + 0x6a, // text(10) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // "public-key" + 0xa2, // map(2) + 0x63, // text(3) + 0x61, 0x6c, 0x67, // "alg" + 0x39, 0x01, 0x00, // -257 (RS256) + 0x64, // text(4) + 0x74, 0x79, 0x70, 0x65, // "type" + 0x6a, // text(10) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // "public-key" + // TODO(MS): Options seem to be parsed differently than in the example here. + 0x07, // unsigned(7) - options + 0xa1, // map(1) + 0x62, // text(2) + 0x72, 0x6b, // "rk" + 0xf5, // primitive(21) + ]; + + pub const MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP1: [u8; 73] = [ + // CBOR Header + 0x0, // CLA + 0x1, // INS U2F_Register + 0x0, // P1 Flags + 0x0, // P2 + 0x0, 0x0, 0x40, // Lc + // NOTE: This has been taken from CTAP2.0 spec, but the clientDataHash has been replaced + // to be able to operate with known values for CollectedClientData (spec doesn't say + // what values led to the provided example hash) + // clientDataHash: + 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, // hash + 0x32, 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, // hash + 0x10, 0x87, 0x54, 0xc3, 0x2d, 0x80, // hash + // rpIdHash: + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, 0x34, 0xE2, + 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, 0x12, 0x55, 0x86, 0xCE, + 0x19, 0x47, // .. + // Le (Ne=65536): + 0x0, 0x0, + ]; + + pub const MAKE_CREDENTIALS_SAMPLE_RESPONSE_CTAP1: [u8; 792] = [ + 0x05, // Reserved Byte (1 Byte) + // User Public Key (65 Bytes) + 0x04, 0xE8, 0x76, 0x25, 0x89, 0x6E, 0xE4, 0xE4, 0x6D, 0xC0, 0x32, 0x76, 0x6E, 0x80, 0x87, + 0x96, 0x2F, 0x36, 0xDF, 0x9D, 0xFE, 0x8B, 0x56, 0x7F, 0x37, 0x63, 0x01, 0x5B, 0x19, 0x90, + 0xA6, 0x0E, 0x14, 0x27, 0xDE, 0x61, 0x2D, 0x66, 0x41, 0x8B, 0xDA, 0x19, 0x50, 0x58, 0x1E, + 0xBC, 0x5C, 0x8C, 0x1D, 0xAD, 0x71, 0x0C, 0xB1, 0x4C, 0x22, 0xF8, 0xC9, 0x70, 0x45, 0xF4, + 0x61, 0x2F, 0xB2, 0x0C, 0x91, // ... + 0x40, // Key Handle Length (1 Byte) + // Key Handle (Key Handle Length Bytes) + 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35, 0xEF, 0xAA, + 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D, 0xA4, 0x85, 0x34, 0xC8, + 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5, 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD, + 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08, + 0xFE, 0x42, 0x00, 0x38, // ... + // X.509 Cert (Variable length Cert) + 0x30, 0x82, 0x02, 0x4A, 0x30, 0x82, 0x01, 0x32, 0xA0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x04, + 0x04, 0x6C, 0x88, 0x22, 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, + 0x01, 0x0B, 0x05, 0x00, 0x30, 0x2E, 0x31, 0x2C, 0x30, 0x2A, 0x06, 0x03, 0x55, 0x04, 0x03, + 0x13, 0x23, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6F, 0x20, 0x55, 0x32, 0x46, 0x20, 0x52, 0x6F, + 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6C, 0x20, 0x34, 0x35, + 0x37, 0x32, 0x30, 0x30, 0x36, 0x33, 0x31, 0x30, 0x20, 0x17, 0x0D, 0x31, 0x34, 0x30, 0x38, + 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5A, 0x18, 0x0F, 0x32, 0x30, 0x35, 0x30, + 0x30, 0x39, 0x30, 0x34, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5A, 0x30, 0x2C, 0x31, 0x2A, + 0x30, 0x28, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C, 0x21, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6F, + 0x20, 0x55, 0x32, 0x46, 0x20, 0x45, 0x45, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6C, 0x20, + 0x32, 0x34, 0x39, 0x31, 0x38, 0x32, 0x33, 0x32, 0x34, 0x37, 0x37, 0x30, 0x30, 0x59, 0x30, + 0x13, 0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01, 0x06, 0x08, 0x2A, 0x86, 0x48, + 0xCE, 0x3D, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0x3C, 0xCA, 0xB9, 0x2C, 0xCB, 0x97, + 0x28, 0x7E, 0xE8, 0xE6, 0x39, 0x43, 0x7E, 0x21, 0xFC, 0xD6, 0xB6, 0xF1, 0x65, 0xB2, 0xD5, + 0xA3, 0xF3, 0xDB, 0x13, 0x1D, 0x31, 0xC1, 0x6B, 0x74, 0x2B, 0xB4, 0x76, 0xD8, 0xD1, 0xE9, + 0x90, 0x80, 0xEB, 0x54, 0x6C, 0x9B, 0xBD, 0xF5, 0x56, 0xE6, 0x21, 0x0F, 0xD4, 0x27, 0x85, + 0x89, 0x9E, 0x78, 0xCC, 0x58, 0x9E, 0xBE, 0x31, 0x0F, 0x6C, 0xDB, 0x9F, 0xF4, 0xA3, 0x3B, + 0x30, 0x39, 0x30, 0x22, 0x06, 0x09, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0xC4, 0x0A, 0x02, + 0x04, 0x15, 0x31, 0x2E, 0x33, 0x2E, 0x36, 0x2E, 0x31, 0x2E, 0x34, 0x2E, 0x31, 0x2E, 0x34, + 0x31, 0x34, 0x38, 0x32, 0x2E, 0x31, 0x2E, 0x32, 0x30, 0x13, 0x06, 0x0B, 0x2B, 0x06, 0x01, + 0x04, 0x01, 0x82, 0xE5, 0x1C, 0x02, 0x01, 0x01, 0x04, 0x04, 0x03, 0x02, 0x04, 0x30, 0x30, + 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B, 0x05, 0x00, 0x03, + 0x82, 0x01, 0x01, 0x00, 0x9F, 0x9B, 0x05, 0x22, 0x48, 0xBC, 0x4C, 0xF4, 0x2C, 0xC5, 0x99, + 0x1F, 0xCA, 0xAB, 0xAC, 0x9B, 0x65, 0x1B, 0xBE, 0x5B, 0xDC, 0xDC, 0x8E, 0xF0, 0xAD, 0x2C, + 0x1C, 0x1F, 0xFB, 0x36, 0xD1, 0x87, 0x15, 0xD4, 0x2E, 0x78, 0xB2, 0x49, 0x22, 0x4F, 0x92, + 0xC7, 0xE6, 0xE7, 0xA0, 0x5C, 0x49, 0xF0, 0xE7, 0xE4, 0xC8, 0x81, 0xBF, 0x2E, 0x94, 0xF4, + 0x5E, 0x4A, 0x21, 0x83, 0x3D, 0x74, 0x56, 0x85, 0x1D, 0x0F, 0x6C, 0x14, 0x5A, 0x29, 0x54, + 0x0C, 0x87, 0x4F, 0x30, 0x92, 0xC9, 0x34, 0xB4, 0x3D, 0x22, 0x2B, 0x89, 0x62, 0xC0, 0xF4, + 0x10, 0xCE, 0xF1, 0xDB, 0x75, 0x89, 0x2A, 0xF1, 0x16, 0xB4, 0x4A, 0x96, 0xF5, 0xD3, 0x5A, + 0xDE, 0xA3, 0x82, 0x2F, 0xC7, 0x14, 0x6F, 0x60, 0x04, 0x38, 0x5B, 0xCB, 0x69, 0xB6, 0x5C, + 0x99, 0xE7, 0xEB, 0x69, 0x19, 0x78, 0x67, 0x03, 0xC0, 0xD8, 0xCD, 0x41, 0xE8, 0xF7, 0x5C, + 0xCA, 0x44, 0xAA, 0x8A, 0xB7, 0x25, 0xAD, 0x8E, 0x79, 0x9F, 0xF3, 0xA8, 0x69, 0x6A, 0x6F, + 0x1B, 0x26, 0x56, 0xE6, 0x31, 0xB1, 0xE4, 0x01, 0x83, 0xC0, 0x8F, 0xDA, 0x53, 0xFA, 0x4A, + 0x8F, 0x85, 0xA0, 0x56, 0x93, 0x94, 0x4A, 0xE1, 0x79, 0xA1, 0x33, 0x9D, 0x00, 0x2D, 0x15, + 0xCA, 0xBD, 0x81, 0x00, 0x90, 0xEC, 0x72, 0x2E, 0xF5, 0xDE, 0xF9, 0x96, 0x5A, 0x37, 0x1D, + 0x41, 0x5D, 0x62, 0x4B, 0x68, 0xA2, 0x70, 0x7C, 0xAD, 0x97, 0xBC, 0xDD, 0x17, 0x85, 0xAF, + 0x97, 0xE2, 0x58, 0xF3, 0x3D, 0xF5, 0x6A, 0x03, 0x1A, 0xA0, 0x35, 0x6D, 0x8E, 0x8D, 0x5E, + 0xBC, 0xAD, 0xC7, 0x4E, 0x07, 0x16, 0x36, 0xC6, 0xB1, 0x10, 0xAC, 0xE5, 0xCC, 0x9B, 0x90, + 0xDF, 0xEA, 0xCA, 0xE6, 0x40, 0xFF, 0x1B, 0xB0, 0xF1, 0xFE, 0x5D, 0xB4, 0xEF, 0xF7, 0xA9, + 0x5F, 0x06, 0x07, 0x33, 0xF5, // ... + // Signature (variable Length) + 0x30, 0x45, 0x02, 0x20, 0x32, 0x47, 0x79, 0xC6, 0x8F, 0x33, 0x80, 0x28, 0x8A, 0x11, 0x97, + 0xB6, 0x09, 0x5F, 0x7A, 0x6E, 0xB9, 0xB1, 0xB1, 0xC1, 0x27, 0xF6, 0x6A, 0xE1, 0x2A, 0x99, + 0xFE, 0x85, 0x32, 0xEC, 0x23, 0xB9, 0x02, 0x21, 0x00, 0xE3, 0x95, 0x16, 0xAC, 0x4D, 0x61, + 0xEE, 0x64, 0x04, 0x4D, 0x50, 0xB4, 0x15, 0xA6, 0xA4, 0xD4, 0xD8, 0x4B, 0xA6, 0xD8, 0x95, + 0xCB, 0x5A, 0xB7, 0xA1, 0xAA, 0x7D, 0x08, 0x1D, 0xE3, 0x41, 0xFA, // ... + ]; +} diff --git a/third_party/rust/authenticator/src/ctap2/commands/mod.rs b/third_party/rust/authenticator/src/ctap2/commands/mod.rs new file mode 100644 index 0000000000..788de842ba --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/mod.rs @@ -0,0 +1,470 @@ +use crate::crypto; +use crate::ctap2::client_data::ClientDataHash; +use crate::ctap2::commands::client_pin::{GetPinToken, GetRetries, Pin, PinAuth, PinError}; +use crate::errors::AuthenticatorError; +use crate::transport::errors::{ApduErrorStatus, HIDError}; +use crate::transport::FidoDevice; +use serde_cbor::{error::Error as CborError, Value}; +use serde_json as json; +use std::error::Error as StdErrorT; +use std::fmt; +use std::io::{Read, Write}; + +pub(crate) mod client_pin; +pub(crate) mod get_assertion; +pub(crate) mod get_info; +pub(crate) mod get_next_assertion; +pub(crate) mod get_version; +pub(crate) mod make_credentials; +pub(crate) mod reset; +pub(crate) mod selection; + +pub trait Request<T> +where + Self: fmt::Debug, + Self: RequestCtap1<Output = T>, + Self: RequestCtap2<Output = T>, +{ + fn is_ctap2_request(&self) -> bool; +} + +/// Retryable wraps an error type and may ask manager to retry sending a +/// command, this is useful for ctap1 where token will reply with "condition not +/// sufficient" because user needs to press the button. +#[derive(Debug)] +pub enum Retryable<T> { + Retry, + Error(T), +} + +impl<T> Retryable<T> { + pub fn is_retry(&self) -> bool { + matches!(*self, Retryable::Retry) + } + + pub fn is_error(&self) -> bool { + !self.is_retry() + } +} + +impl<T> From<T> for Retryable<T> { + fn from(e: T) -> Self { + Retryable::Error(e) + } +} + +pub trait RequestCtap1: fmt::Debug { + type Output; + + /// Serializes a request into FIDO v1.x / CTAP1 / U2F format. + /// + /// See [`crate::u2ftypes::CTAP1RequestAPDU::serialize()`] + fn ctap1_format<Dev>(&self, dev: &mut Dev) -> Result<Vec<u8>, HIDError> + where + Dev: FidoDevice + Read + Write + fmt::Debug; + + /// Deserializes a response from FIDO v1.x / CTAP1 / U2Fv2 format. + fn handle_response_ctap1( + &self, + status: Result<(), ApduErrorStatus>, + input: &[u8], + ) -> Result<Self::Output, Retryable<HIDError>>; +} + +pub trait RequestCtap2: fmt::Debug { + type Output; + + fn command() -> Command; + + fn wire_format<Dev>(&self, dev: &mut Dev) -> Result<Vec<u8>, HIDError> + where + Dev: FidoDevice + Read + Write + fmt::Debug; + + fn handle_response_ctap2<Dev>( + &self, + dev: &mut Dev, + input: &[u8], + ) -> Result<Self::Output, HIDError> + where + Dev: FidoDevice + Read + Write + fmt::Debug; +} + +pub(crate) trait PinAuthCommand { + fn pin(&self) -> &Option<Pin>; + fn set_pin(&mut self, pin: Option<Pin>); + fn pin_auth(&self) -> &Option<PinAuth>; + fn set_pin_auth(&mut self, pin_auth: Option<PinAuth>, pin_auth_protocol: Option<u64>); + fn client_data_hash(&self) -> ClientDataHash; + fn unset_uv_option(&mut self); + fn determine_pin_auth<D: FidoDevice>(&mut self, dev: &mut D) -> Result<(), AuthenticatorError> { + if !dev.supports_ctap2() { + self.set_pin_auth(None, None); + return Ok(()); + } + + let client_data_hash = self.client_data_hash(); + let (pin_auth, pin_auth_protocol) = + match calculate_pin_auth(dev, &client_data_hash, &self.pin()) { + Ok((pin_auth, pin_auth_protocol)) => (pin_auth, pin_auth_protocol), + Err(e) => { + return Err(repackage_pin_errors(dev, e)); + } + }; + self.set_pin_auth(pin_auth, pin_auth_protocol); + Ok(()) + } +} + +pub(crate) fn repackage_pin_errors<D: FidoDevice>( + dev: &mut D, + error: AuthenticatorError, +) -> AuthenticatorError { + match error { + AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode( + StatusCode::PinInvalid, + _, + ))) => { + // If the given PIN was wrong, determine no. of left retries + let cmd = GetRetries::new(); + let retries = dev.send_cbor(&cmd).ok(); // If we got retries, wrap it in Some, otherwise ignore err + return AuthenticatorError::PinError(PinError::InvalidPin(retries)); + } + AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode( + StatusCode::PinAuthBlocked, + _, + ))) => { + return AuthenticatorError::PinError(PinError::PinAuthBlocked); + } + AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode( + StatusCode::PinBlocked, + _, + ))) => { + return AuthenticatorError::PinError(PinError::PinBlocked); + } + AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode( + StatusCode::PinRequired, + _, + ))) => { + return AuthenticatorError::PinError(PinError::PinRequired); + } + AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode( + StatusCode::PinNotSet, + _, + ))) => { + return AuthenticatorError::PinError(PinError::PinNotSet); + } + // TODO(MS): Add "PinPolicyViolated" + err => { + return err; + } + } +} + +// Spec: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticator-api +// and: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#authenticator-api +#[repr(u8)] +#[derive(Debug)] +pub enum Command { + MakeCredentials = 0x01, + GetAssertion = 0x02, + GetInfo = 0x04, + ClientPin = 0x06, + Reset = 0x07, + GetNextAssertion = 0x08, + Selection = 0x0B, +} + +impl Command { + #[cfg(test)] + pub fn from_u8(v: u8) -> Option<Command> { + match v { + 0x01 => Some(Command::MakeCredentials), + 0x02 => Some(Command::GetAssertion), + 0x04 => Some(Command::GetInfo), + 0x06 => Some(Command::ClientPin), + 0x07 => Some(Command::Reset), + 0x08 => Some(Command::GetNextAssertion), + _ => None, + } + } +} + +#[derive(Debug)] +pub enum StatusCode { + /// Indicates successful response. + OK, + /// The command is not a valid CTAP command. + InvalidCommand, + /// The command included an invalid parameter. + InvalidParameter, + /// Invalid message or item length. + InvalidLength, + /// Invalid message sequencing. + InvalidSeq, + /// Message timed out. + Timeout, + /// Channel busy. + ChannelBusy, + /// Command requires channel lock. + LockRequired, + /// Command not allowed on this cid. + InvalidChannel, + /// Invalid/unexpected CBOR error. + CBORUnexpectedType, + /// Error when parsing CBOR. + InvalidCBOR, + /// Missing non-optional parameter. + MissingParameter, + /// Limit for number of items exceeded. + LimitExceeded, + /// Unsupported extension. + UnsupportedExtension, + /// Valid credential found in the exclude list. + CredentialExcluded, + /// Processing (Lengthy operation is in progress). + Processing, + /// Credential not valid for the authenticator. + InvalidCredential, + /// Authentication is waiting for user interaction. + UserActionPending, + /// Processing, lengthy operation is in progress. + OperationPending, + /// No request is pending. + NoOperations, + /// Authenticator does not support requested algorithm. + UnsupportedAlgorithm, + /// Not authorized for requested operation. + OperationDenied, + /// Internal key storage is full. + KeyStoreFull, + /// No outstanding operations. + NoOperationPending, + /// Unsupported option. + UnsupportedOption, + /// Not a valid option for current operation. + InvalidOption, + /// Pending keep alive was cancelled. + KeepaliveCancel, + /// No valid credentials provided. + NoCredentials, + /// Timeout waiting for user interaction. + UserActionTimeout, + /// Continuation command, such as, authenticatorGetNextAssertion not + /// allowed. + NotAllowed, + /// PIN Invalid. + PinInvalid, + /// PIN Blocked. + PinBlocked, + /// PIN authentication,pinAuth, verification failed. + PinAuthInvalid, + /// PIN authentication,pinAuth, blocked. Requires power recycle to reset. + PinAuthBlocked, + /// No PIN has been set. + PinNotSet, + /// PIN is required for the selected operation. + PinRequired, + /// PIN policy violation. Currently only enforces minimum length. + PinPolicyViolation, + /// pinToken expired on authenticator. + PinTokenExpired, + /// Authenticator cannot handle this request due to memory constraints. + RequestTooLarge, + /// The current operation has timed out. + ActionTimeout, + /// User presence is required for the requested operation. + UpRequired, + + /// Unknown status. + Unknown(u8), +} + +impl StatusCode { + fn is_ok(&self) -> bool { + matches!(*self, StatusCode::OK) + } + + fn device_busy(&self) -> bool { + matches!(*self, StatusCode::ChannelBusy) + } +} + +impl From<u8> for StatusCode { + fn from(value: u8) -> StatusCode { + match value { + 0x00 => StatusCode::OK, + 0x01 => StatusCode::InvalidCommand, + 0x02 => StatusCode::InvalidParameter, + 0x03 => StatusCode::InvalidLength, + 0x04 => StatusCode::InvalidSeq, + 0x05 => StatusCode::Timeout, + 0x06 => StatusCode::ChannelBusy, + 0x0A => StatusCode::LockRequired, + 0x0B => StatusCode::InvalidChannel, + 0x11 => StatusCode::CBORUnexpectedType, + 0x12 => StatusCode::InvalidCBOR, + 0x14 => StatusCode::MissingParameter, + 0x15 => StatusCode::LimitExceeded, + 0x16 => StatusCode::UnsupportedExtension, + 0x19 => StatusCode::CredentialExcluded, + 0x21 => StatusCode::Processing, + 0x22 => StatusCode::InvalidCredential, + 0x23 => StatusCode::UserActionPending, + 0x24 => StatusCode::OperationPending, + 0x25 => StatusCode::NoOperations, + 0x26 => StatusCode::UnsupportedAlgorithm, + 0x27 => StatusCode::OperationDenied, + 0x28 => StatusCode::KeyStoreFull, + 0x2A => StatusCode::NoOperationPending, + 0x2B => StatusCode::UnsupportedOption, + 0x2C => StatusCode::InvalidOption, + 0x2D => StatusCode::KeepaliveCancel, + 0x2E => StatusCode::NoCredentials, + 0x2f => StatusCode::UserActionTimeout, + 0x30 => StatusCode::NotAllowed, + 0x31 => StatusCode::PinInvalid, + 0x32 => StatusCode::PinBlocked, + 0x33 => StatusCode::PinAuthInvalid, + 0x34 => StatusCode::PinAuthBlocked, + 0x35 => StatusCode::PinNotSet, + 0x36 => StatusCode::PinRequired, + 0x37 => StatusCode::PinPolicyViolation, + 0x38 => StatusCode::PinTokenExpired, + 0x39 => StatusCode::RequestTooLarge, + 0x3A => StatusCode::ActionTimeout, + 0x3B => StatusCode::UpRequired, + + othr => StatusCode::Unknown(othr), + } + } +} + +#[cfg(test)] +impl Into<u8> for StatusCode { + fn into(self) -> u8 { + match self { + StatusCode::OK => 0x00, + StatusCode::InvalidCommand => 0x01, + StatusCode::InvalidParameter => 0x02, + StatusCode::InvalidLength => 0x03, + StatusCode::InvalidSeq => 0x04, + StatusCode::Timeout => 0x05, + StatusCode::ChannelBusy => 0x06, + StatusCode::LockRequired => 0x0A, + StatusCode::InvalidChannel => 0x0B, + StatusCode::CBORUnexpectedType => 0x11, + StatusCode::InvalidCBOR => 0x12, + StatusCode::MissingParameter => 0x14, + StatusCode::LimitExceeded => 0x15, + StatusCode::UnsupportedExtension => 0x16, + StatusCode::CredentialExcluded => 0x19, + StatusCode::Processing => 0x21, + StatusCode::InvalidCredential => 0x22, + StatusCode::UserActionPending => 0x23, + StatusCode::OperationPending => 0x24, + StatusCode::NoOperations => 0x25, + StatusCode::UnsupportedAlgorithm => 0x26, + StatusCode::OperationDenied => 0x27, + StatusCode::KeyStoreFull => 0x28, + StatusCode::NoOperationPending => 0x2A, + StatusCode::UnsupportedOption => 0x2B, + StatusCode::InvalidOption => 0x2C, + StatusCode::KeepaliveCancel => 0x2D, + StatusCode::NoCredentials => 0x2E, + StatusCode::UserActionTimeout => 0x2f, + StatusCode::NotAllowed => 0x30, + StatusCode::PinInvalid => 0x31, + StatusCode::PinBlocked => 0x32, + StatusCode::PinAuthInvalid => 0x33, + StatusCode::PinAuthBlocked => 0x34, + StatusCode::PinNotSet => 0x35, + StatusCode::PinRequired => 0x36, + StatusCode::PinPolicyViolation => 0x37, + StatusCode::PinTokenExpired => 0x38, + StatusCode::RequestTooLarge => 0x39, + StatusCode::ActionTimeout => 0x3A, + StatusCode::UpRequired => 0x3B, + + StatusCode::Unknown(othr) => othr, + } + } +} + +#[derive(Debug)] +pub enum CommandError { + InputTooSmall, + MissingRequiredField(&'static str), + Deserializing(CborError), + Serializing(CborError), + StatusCode(StatusCode, Option<Value>), + Json(json::Error), + Crypto(crypto::CryptoError), + UnsupportedPinProtocol, +} + +impl fmt::Display for CommandError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + CommandError::InputTooSmall => write!(f, "CommandError: Input is too small"), + CommandError::MissingRequiredField(field) => { + write!(f, "CommandError: Missing required field {}", field) + } + CommandError::Deserializing(ref e) => { + write!(f, "CommandError: Error while parsing: {}", e) + } + CommandError::Serializing(ref e) => { + write!(f, "CommandError: Error while serializing: {}", e) + } + CommandError::StatusCode(ref code, ref value) => { + write!(f, "CommandError: Unexpected code: {:?} ({:?})", code, value) + } + CommandError::Json(ref e) => write!(f, "CommandError: Json serializing error: {}", e), + CommandError::Crypto(ref e) => write!(f, "CommandError: Crypto error: {:?}", e), + CommandError::UnsupportedPinProtocol => { + write!(f, "CommandError: Pin protocol is not supported") + } + } + } +} + +impl StdErrorT for CommandError {} + +pub(crate) fn calculate_pin_auth<Dev>( + dev: &mut Dev, + client_data_hash: &ClientDataHash, + pin: &Option<Pin>, +) -> Result<(Option<PinAuth>, Option<u64>), AuthenticatorError> +where + Dev: FidoDevice, +{ + // Not reusing the shared secret here, if it exists, since we might start again + // with a different PIN (e.g. if the last one was wrong) + let (shared_secret, info) = dev.establish_shared_secret()?; + + // TODO(MS): What to do if token supports client_pin, but none has been set: Some(false) + // AND a Pin is not None? + let pin_auth = if info.options.client_pin == Some(true) { + let pin = pin + .as_ref() + .ok_or(HIDError::Command(CommandError::StatusCode( + StatusCode::PinRequired, + None, + )))?; + + let pin_command = GetPinToken::new(&info, &shared_secret, &pin)?; + let pin_token = dev.send_cbor(&pin_command)?; + + ( + Some( + pin_token + .auth(client_data_hash.as_ref()) + .map_err(CommandError::Crypto)?, + ), + Some(1), // Currently only pin_auth_protocol 1 supported + ) + } else { + (None, None) + }; + + Ok(pin_auth) +} diff --git a/third_party/rust/authenticator/src/ctap2/commands/reset.rs b/third_party/rust/authenticator/src/ctap2/commands/reset.rs new file mode 100644 index 0000000000..a40ae34ad0 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/reset.rs @@ -0,0 +1,129 @@ +use super::{Command, CommandError, RequestCtap2, StatusCode}; +use crate::transport::errors::HIDError; +use crate::u2ftypes::U2FDevice; +use serde_cbor::{de::from_slice, Value}; + +#[derive(Debug)] +pub struct Reset {} + +impl Default for Reset { + fn default() -> Reset { + Reset {} + } +} + +impl RequestCtap2 for Reset { + type Output = (); + + fn command() -> Command { + Command::Reset + } + + fn wire_format<Dev>(&self, _dev: &mut Dev) -> Result<Vec<u8>, HIDError> + where + Dev: U2FDevice, + { + Ok(Vec::new()) + } + + fn handle_response_ctap2<Dev>( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result<Self::Output, HIDError> + where + Dev: U2FDevice, + { + if input.is_empty() { + return Err(CommandError::InputTooSmall.into()); + } + + let status: StatusCode = input[0].into(); + + if status.is_ok() { + Ok(()) + } else { + let msg = if input.len() > 1 { + let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + Some(data) + } else { + None + }; + Err(CommandError::StatusCode(status, msg).into()) + } + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::consts::HIDCmd; + use crate::transport::device_selector::Device; + use crate::transport::{hid::HIDDevice, FidoDevice}; + use crate::u2ftypes::U2FDevice; + use rand::{thread_rng, RngCore}; + use serde_cbor::{de::from_slice, Value}; + + fn issue_command_and_get_response(cmd: u8, add: &[u8]) -> Result<(), HIDError> { + let mut device = Device::new("commands/Reset").unwrap(); + // ctap2 request + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + device.set_cid(cid); + + let mut msg = cid.to_vec(); + msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x1]); // cmd + bcnt + msg.extend(vec![0x07]); // authenticatorReset + device.add_write(&msg, 0); + + // ctap2 response + let len = 0x1 + add.len() as u8; + let mut msg = cid.to_vec(); + msg.extend(vec![HIDCmd::Cbor.into(), 0x00, len]); // cmd + bcnt + msg.push(cmd); // Status code + msg.extend(add); // + maybe additional data + device.add_read(&msg, 0); + + device.send_cbor(&Reset {}) + } + + #[test] + fn test_select_ctap2_only() { + // Test, if we can parse the status codes specified by the spec + + // Ok() + let response = issue_command_and_get_response(0, &vec![]).expect("Unexpected error"); + assert_eq!(response, ()); + + // Denied by the user + let response = issue_command_and_get_response(0x27, &vec![]).expect_err("Not an error!"); + assert!(matches!( + response, + HIDError::Command(CommandError::StatusCode(StatusCode::OperationDenied, None)) + )); + + // Timeout + let response = issue_command_and_get_response(0x2F, &vec![]).expect_err("Not an error!"); + assert!(matches!( + response, + HIDError::Command(CommandError::StatusCode( + StatusCode::UserActionTimeout, + None + )) + )); + + // Unexpected error with more random CBOR-data + let add_data = vec![ + 0x63, // text(3) + 0x61, 0x6c, 0x67, // "alg" + ]; + let response = issue_command_and_get_response(0x02, &add_data).expect_err("Not an error!"); + match response { + HIDError::Command(CommandError::StatusCode(StatusCode::InvalidParameter, Some(d))) => { + let expected: Value = from_slice(&add_data).unwrap(); + assert_eq!(d, expected) + } + e => panic!("Not the expected response: {:?}", e), + } + } +} diff --git a/third_party/rust/authenticator/src/ctap2/commands/selection.rs b/third_party/rust/authenticator/src/ctap2/commands/selection.rs new file mode 100644 index 0000000000..9f4491a734 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/selection.rs @@ -0,0 +1,129 @@ +use super::{Command, CommandError, RequestCtap2, StatusCode}; +use crate::transport::errors::HIDError; +use crate::u2ftypes::U2FDevice; +use serde_cbor::{de::from_slice, Value}; + +#[derive(Debug)] +pub struct Selection {} + +impl Default for Selection { + fn default() -> Selection { + Selection {} + } +} + +impl RequestCtap2 for Selection { + type Output = (); + + fn command() -> Command { + Command::Selection + } + + fn wire_format<Dev>(&self, _dev: &mut Dev) -> Result<Vec<u8>, HIDError> + where + Dev: U2FDevice, + { + Ok(Vec::new()) + } + + fn handle_response_ctap2<Dev>( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result<Self::Output, HIDError> + where + Dev: U2FDevice, + { + if input.is_empty() { + return Err(CommandError::InputTooSmall.into()); + } + + let status: StatusCode = input[0].into(); + + if status.is_ok() { + Ok(()) + } else { + let msg = if input.len() > 1 { + let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + Some(data) + } else { + None + }; + Err(CommandError::StatusCode(status, msg).into()) + } + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::consts::HIDCmd; + use crate::transport::device_selector::Device; + use crate::transport::{hid::HIDDevice, FidoDevice}; + use crate::u2ftypes::U2FDevice; + use rand::{thread_rng, RngCore}; + use serde_cbor::{de::from_slice, Value}; + + fn issue_command_and_get_response(cmd: u8, add: &[u8]) -> Result<(), HIDError> { + let mut device = Device::new("commands/selection").unwrap(); + // ctap2 request + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + device.set_cid(cid); + + let mut msg = cid.to_vec(); + msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x1]); // cmd + bcnt + msg.extend(vec![0x0B]); // authenticatorSelection + device.add_write(&msg, 0); + + // ctap2 response + let len = 0x1 + add.len() as u8; + let mut msg = cid.to_vec(); + msg.extend(vec![HIDCmd::Cbor.into(), 0x00, len]); // cmd + bcnt + msg.push(cmd); // Status code + msg.extend(add); // + maybe additional data + device.add_read(&msg, 0); + + device.send_cbor(&Selection {}) + } + + #[test] + fn test_select_ctap2_only() { + // Test, if we can parse the status codes specified by the spec + + // Ok() + let response = issue_command_and_get_response(0, &vec![]).expect("Unexpected error"); + assert_eq!(response, ()); + + // Denied by the user + let response = issue_command_and_get_response(0x27, &vec![]).expect_err("Not an error!"); + assert!(matches!( + response, + HIDError::Command(CommandError::StatusCode(StatusCode::OperationDenied, None)) + )); + + // Timeout + let response = issue_command_and_get_response(0x2F, &vec![]).expect_err("Not an error!"); + assert!(matches!( + response, + HIDError::Command(CommandError::StatusCode( + StatusCode::UserActionTimeout, + None + )) + )); + + // Unexpected error with more random CBOR-data + let add_data = vec![ + 0x63, // text(3) + 0x61, 0x6c, 0x67, // "alg" + ]; + let response = issue_command_and_get_response(0x02, &add_data).expect_err("Not an error!"); + match response { + HIDError::Command(CommandError::StatusCode(StatusCode::InvalidParameter, Some(d))) => { + let expected: Value = from_slice(&add_data).unwrap(); + assert_eq!(d, expected) + } + e => panic!("Not the expected response: {:?}", e), + } + } +} diff --git a/third_party/rust/authenticator/src/ctap2/mod.rs b/third_party/rust/authenticator/src/ctap2/mod.rs new file mode 100644 index 0000000000..aa5dd73170 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/mod.rs @@ -0,0 +1,9 @@ +#[allow(dead_code)] // TODO(MS): Remove me asap +pub mod commands; +pub use commands::get_assertion::AssertionObject; + +pub(crate) mod attestation; + +pub mod client_data; +pub mod server; +pub(crate) mod utils; diff --git a/third_party/rust/authenticator/src/ctap2/server.rs b/third_party/rust/authenticator/src/ctap2/server.rs new file mode 100644 index 0000000000..3b46584cf5 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/server.rs @@ -0,0 +1,510 @@ +use crate::crypto::COSEAlgorithm; +use crate::{errors::AuthenticatorError, AuthenticatorTransports, KeyHandle}; +use serde::de::MapAccess; +use serde::{ + de::{Error as SerdeError, Visitor}, + ser::SerializeMap, + Deserialize, Deserializer, Serialize, Serializer, +}; +use serde_bytes::ByteBuf; +use sha2::{Digest, Sha256}; +use std::convert::{Into, TryFrom}; +use std::fmt; + +#[derive(Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct RpIdHash(pub [u8; 32]); + +impl fmt::Debug for RpIdHash { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let value = base64::encode_config(&self.0, base64::URL_SAFE_NO_PAD); + write!(f, "RpIdHash({})", value) + } +} + +impl AsRef<[u8]> for RpIdHash { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl RpIdHash { + pub fn from(src: &[u8]) -> Result<RpIdHash, AuthenticatorError> { + let mut payload = [0u8; 32]; + if src.len() != payload.len() { + Err(AuthenticatorError::InvalidRelyingPartyInput) + } else { + payload.copy_from_slice(src); + Ok(RpIdHash(payload)) + } + } +} + +#[derive(Debug, Serialize, Clone, Default)] +#[cfg_attr(test, derive(Deserialize))] +pub struct RelyingParty { + // TODO(baloo): spec is wrong !!!!111 + // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#commands + // in the example "A PublicKeyCredentialRpEntity DOM object defined as follows:" + // inconsistent with https://w3c.github.io/webauthn/#sctn-rp-credential-params + pub id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option<String>, +} + +// Note: This enum is provided to make old CTAP1/U2F API work. This should be deprecated at some point +#[derive(Debug, Clone)] +pub enum RelyingPartyWrapper { + Data(RelyingParty), + // CTAP1 hash can be derived from full object, see RelyingParty::hash below, + // but very old backends might still provide application IDs. + Hash(RpIdHash), +} + +impl RelyingPartyWrapper { + pub fn hash(&self) -> RpIdHash { + match *self { + RelyingPartyWrapper::Data(ref d) => { + let mut hasher = Sha256::new(); + hasher.update(&d.id); + + let mut output = [0u8; 32]; + output.copy_from_slice(&hasher.finalize().as_slice()); + + RpIdHash(output) + } + RelyingPartyWrapper::Hash(ref d) => d.clone(), + } + } +} + +// TODO(baloo): should we rename this PublicKeyCredentialUserEntity ? +#[derive(Debug, Serialize, Clone, Eq, PartialEq, Deserialize, Default)] +pub struct User { + #[serde(with = "serde_bytes")] + pub id: Vec<u8>, + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option<String>, // This has been removed from Webauthn-2 + pub name: Option<String>, + #[serde(skip_serializing_if = "Option::is_none", rename = "displayName")] + pub display_name: Option<String>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PublicKeyCredentialParameters { + pub alg: COSEAlgorithm, +} + +impl TryFrom<i32> for PublicKeyCredentialParameters { + type Error = AuthenticatorError; + fn try_from(arg: i32) -> Result<Self, Self::Error> { + let alg = COSEAlgorithm::try_from(arg as i64)?; + Ok(PublicKeyCredentialParameters { alg }) + } +} + +impl Serialize for PublicKeyCredentialParameters { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(2))?; + map.serialize_entry("alg", &self.alg)?; + map.serialize_entry("type", "public-key")?; + map.end() + } +} + +impl<'de> Deserialize<'de> for PublicKeyCredentialParameters { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + struct PublicKeyCredentialParametersVisitor; + + impl<'de> Visitor<'de> for PublicKeyCredentialParametersVisitor { + type Value = PublicKeyCredentialParameters; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map") + } + + fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error> + where + M: MapAccess<'de>, + { + let mut found_type = false; + let mut alg = None; + while let Some(key) = map.next_key()? { + match key { + "alg" => { + if alg.is_some() { + return Err(SerdeError::duplicate_field("alg")); + } + alg = Some(map.next_value()?); + } + "type" => { + if found_type { + return Err(SerdeError::duplicate_field("type")); + } + + let v: &str = map.next_value()?; + if v != "public-key" { + return Err(SerdeError::custom(format!("invalid value: {}", v))); + } + found_type = true; + } + v => { + return Err(SerdeError::unknown_field(v, &[])); + } + } + } + + if !found_type { + return Err(SerdeError::missing_field("type")); + } + + let alg = alg.ok_or(SerdeError::missing_field("alg"))?; + + Ok(PublicKeyCredentialParameters { alg }) + } + } + + deserializer.deserialize_bytes(PublicKeyCredentialParametersVisitor) + } +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Eq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum Transport { + USB, + NFC, + BLE, + Internal, +} + +impl From<AuthenticatorTransports> for Vec<Transport> { + fn from(t: AuthenticatorTransports) -> Self { + let mut transports = Vec::new(); + if t.contains(AuthenticatorTransports::USB) { + transports.push(Transport::USB); + } + if t.contains(AuthenticatorTransports::NFC) { + transports.push(Transport::NFC); + } + if t.contains(AuthenticatorTransports::BLE) { + transports.push(Transport::BLE); + } + + transports + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PublicKeyCredentialDescriptor { + pub id: Vec<u8>, + pub transports: Vec<Transport>, +} + +impl Serialize for PublicKeyCredentialDescriptor { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + // TODO(MS): Transports is OPTIONAL, but some older tokens don't understand it + // and return a CBOR-Parsing error. It is only a hint for the token, + // so we'll leave it out for the moment + let mut map = serializer.serialize_map(Some(2))?; + // let mut map = serializer.serialize_map(Some(3))?; + map.serialize_entry("type", "public-key")?; + map.serialize_entry("id", &ByteBuf::from(self.id.clone()))?; + // map.serialize_entry("transports", &self.transports)?; + map.end() + } +} + +impl<'de> Deserialize<'de> for PublicKeyCredentialDescriptor { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + struct PublicKeyCredentialDescriptorVisitor; + + impl<'de> Visitor<'de> for PublicKeyCredentialDescriptorVisitor { + type Value = PublicKeyCredentialDescriptor; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map") + } + + fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error> + where + M: MapAccess<'de>, + { + let mut found_type = false; + let mut id = None; + let mut transports = None; + while let Some(key) = map.next_key()? { + match key { + "id" => { + if id.is_some() { + return Err(SerdeError::duplicate_field("id")); + } + let id_bytes: ByteBuf = map.next_value()?; + id = Some(id_bytes.into_vec()); + } + "transports" => { + if transports.is_some() { + return Err(SerdeError::duplicate_field("transports")); + } + transports = Some(map.next_value()?); + } + "type" => { + if found_type { + return Err(SerdeError::duplicate_field("type")); + } + let v: &str = map.next_value()?; + if v != "public-key" { + return Err(SerdeError::custom(format!("invalid value: {}", v))); + } + found_type = true; + } + v => { + return Err(SerdeError::unknown_field(v, &[])); + } + } + } + + if !found_type { + return Err(SerdeError::missing_field("type")); + } + + let id = id.ok_or(SerdeError::missing_field("id"))?; + let transports = transports.unwrap_or(Vec::new()); + + Ok(PublicKeyCredentialDescriptor { id, transports }) + } + } + + deserializer.deserialize_bytes(PublicKeyCredentialDescriptorVisitor) + } +} + +impl From<&KeyHandle> for PublicKeyCredentialDescriptor { + fn from(kh: &KeyHandle) -> Self { + Self { + id: kh.credential.clone(), + transports: kh.transports.into(), + } + } +} + +#[cfg(test)] +mod test { + use super::{ + COSEAlgorithm, PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, + Transport, User, + }; + + #[test] + fn serialize_rp() { + let rp = RelyingParty { + id: String::from("Acme"), + name: None, + icon: None, + }; + + let payload = ser::to_vec(&rp).unwrap(); + assert_eq!( + &payload, + &[ + 0xa1, // map(1) + 0x62, // text(2) + 0x69, 0x64, // "id" + 0x64, // text(4) + 0x41, 0x63, 0x6d, 0x65 + ] + ); + } + + #[test] + fn serialize_user() { + let user = User { + id: vec![ + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30, + 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82, + 0x01, 0x93, 0x30, 0x82, + ], + icon: Some(String::from("https://pics.example.com/00/p/aBjjjpqPb.png")), + name: Some(String::from("johnpsmith@example.com")), + display_name: Some(String::from("John P. Smith")), + }; + + let payload = ser::to_vec(&user).unwrap(); + println!("payload = {:?}", payload); + assert_eq!( + payload, + vec![ + 0xa4, // map(4) + 0x62, // text(2) + 0x69, 0x64, // "id" + 0x58, 0x20, // bytes(32) + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, // userid + 0x02, 0x01, 0x02, 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, // ... + 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82, 0x01, 0x93, // ... + 0x30, 0x82, // ... + 0x64, // text(4) + 0x69, 0x63, 0x6f, 0x6e, // "icon" + 0x78, 0x2b, // text(43) + 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x70, + 0x69, // "https://pics.example.com/00/p/aBjjjpqPb.png" + 0x63, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, // ... + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x30, 0x30, 0x2f, 0x70, 0x2f, // ... + 0x61, 0x42, 0x6a, 0x6a, 0x6a, 0x70, 0x71, 0x50, 0x62, 0x2e, // ... + 0x70, 0x6e, 0x67, // ... + 0x64, // text(4) + 0x6e, 0x61, 0x6d, 0x65, // "name" + 0x76, // text(22) + 0x6a, 0x6f, 0x68, 0x6e, 0x70, 0x73, 0x6d, 0x69, 0x74, + 0x68, // "johnpsmith@example.com" + 0x40, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, // ... + 0x6f, 0x6d, // ... + 0x6b, // text(11) + 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, // "displayName" + 0x65, // ... + 0x6d, // text(13) + 0x4a, 0x6f, 0x68, 0x6e, 0x20, 0x50, 0x2e, 0x20, 0x53, 0x6d, // "John P. Smith" + 0x69, 0x74, 0x68, // ... + ] + ); + } + + #[test] + fn serialize_user_noicon_nodisplayname() { + let user = User { + id: vec![ + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30, + 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82, + 0x01, 0x93, 0x30, 0x82, + ], + icon: None, + name: Some(String::from("johnpsmith@example.com")), + display_name: None, + }; + + let payload = ser::to_vec(&user).unwrap(); + println!("payload = {:?}", payload); + assert_eq!( + payload, + vec![ + 0xa2, // map(2) + 0x62, // text(2) + 0x69, 0x64, // "id" + 0x58, 0x20, // bytes(32) + 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, // userid + 0x02, 0x01, 0x02, 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, // ... + 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82, 0x01, 0x93, // ... + 0x30, 0x82, // ... + 0x64, // text(4) + 0x6e, 0x61, 0x6d, 0x65, // "name" + 0x76, // text(22) + 0x6a, 0x6f, 0x68, 0x6e, 0x70, 0x73, 0x6d, 0x69, 0x74, + 0x68, // "johnpsmith@example.com" + 0x40, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, // ... + 0x6f, 0x6d, // ... + ] + ); + } + + use serde_cbor::ser; + + #[test] + fn public_key() { + let keys = vec![ + PublicKeyCredentialParameters { + alg: COSEAlgorithm::ES256, + }, + PublicKeyCredentialParameters { + alg: COSEAlgorithm::RS256, + }, + ]; + + let payload = ser::to_vec(&keys); + println!("payload = {:?}", payload); + let payload = payload.unwrap(); + assert_eq!( + payload, + vec![ + 0x82, // array(2) + 0xa2, // map(2) + 0x63, // text(3) + 0x61, 0x6c, 0x67, // "alg" + 0x26, // -7 (ES256) + 0x64, // text(4) + 0x74, 0x79, 0x70, 0x65, // "type" + 0x6a, // text(10) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, // "public-key" + 0x2D, 0x6B, 0x65, 0x79, // ... + 0xa2, // map(2) + 0x63, // text(3) + 0x61, 0x6c, 0x67, // "alg" + 0x39, 0x01, 0x00, // -257 (RS256) + 0x64, // text(4) + 0x74, 0x79, 0x70, 0x65, // "type" + 0x6a, // text(10) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, // "public-key" + 0x2D, 0x6B, 0x65, 0x79 // ... + ] + ); + } + + #[test] + fn public_key_desc() { + let key = PublicKeyCredentialDescriptor { + id: vec![ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, + 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, + 0x1c, 0x1d, 0x1e, 0x1f, + ], + transports: vec![Transport::BLE, Transport::USB], + }; + + let payload = ser::to_vec(&key); + println!("payload = {:?}", payload); + let payload = payload.unwrap(); + + assert_eq!( + payload, + vec![ + // 0xa3, // map(3) + 0xa2, // map(2) + 0x64, // text(4) + 0x74, 0x79, 0x70, 0x65, // "type" + 0x6a, // text(10) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, // "public-key" + 0x2D, 0x6B, 0x65, 0x79, // ... + 0x62, // text(2) + 0x69, 0x64, // "id" + 0x58, 0x20, // bytes(32) + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, // key id + 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, // ... + 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, // ... + 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, // ... + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, // ... + 0x1e, + 0x1f, // ... + + // Deactivated for now + //0x6a, // text(10) + //0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, // "transports" + //0x6f, 0x72, 0x74, 0x73, // ... + //0x82, // array(2) + //0x63, // text(3) + //0x62, 0x6c, 0x65, // "ble" + //0x63, // text(3) + //0x75, 0x73, 0x62 // "usb" + ] + ); + } +} diff --git a/third_party/rust/authenticator/src/ctap2/utils.rs b/third_party/rust/authenticator/src/ctap2/utils.rs new file mode 100644 index 0000000000..ba9c7db3b4 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/utils.rs @@ -0,0 +1,14 @@ +use serde::de; +use serde_cbor::error::Result; +use serde_cbor::Deserializer; + +pub fn from_slice_stream<'a, T>(slice: &'a [u8]) -> Result<(&'a [u8], T)> +where + T: de::Deserialize<'a>, +{ + let mut deserializer = Deserializer::from_slice(slice); + let value = de::Deserialize::deserialize(&mut deserializer)?; + let rest = &slice[deserializer.byte_offset()..]; + + Ok((rest, value)) +} diff --git a/third_party/rust/authenticator/src/ctap2_capi.rs b/third_party/rust/authenticator/src/ctap2_capi.rs new file mode 100644 index 0000000000..f1dc4bb8f9 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2_capi.rs @@ -0,0 +1,945 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::authenticatorservice::{ + AuthenticatorService, CtapVersion, RegisterArgsCtap2, SignArgsCtap2, +}; +use crate::ctap2::attestation::AttestationStatement; +use crate::ctap2::commands::get_assertion::{Assertion, AssertionObject, GetAssertionOptions}; +use crate::ctap2::commands::make_credentials::MakeCredentialsOptions; +use crate::ctap2::server::{ + PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, User, +}; +use crate::errors::{AuthenticatorError, U2FTokenError}; +use crate::statecallback::StateCallback; +use crate::{AttestationObject, CollectedClientDataWrapper, Pin, StatusUpdate}; +use crate::{RegisterResult, SignResult}; +use libc::size_t; +use rand::{thread_rng, Rng}; +use serde_cbor; +use std::convert::TryFrom; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; +use std::sync::mpsc::channel; +use std::thread; +use std::{ptr, slice}; + +type Ctap2RegisterResult = Result<(AttestationObject, CString), AuthenticatorError>; +type Ctap2PubKeyCredDescriptors = Vec<PublicKeyCredentialDescriptor>; +type Ctap2RegisterCallback = extern "C" fn(u64, *mut Ctap2RegisterResult); +type Ctap2SignResult = Result<(AssertionObject, CString), AuthenticatorError>; +type Ctap2SignCallback = extern "C" fn(u64, *mut Ctap2SignResult); +type Ctap2StatusUpdateCallback = extern "C" fn(*mut StatusUpdate); + +const SIGN_RESULT_PUBKEY_CRED_ID: u8 = 1; +const SIGN_RESULT_AUTH_DATA: u8 = 2; +const SIGN_RESULT_SIGNATURE: u8 = 3; +const SIGN_RESULT_USER_ID: u8 = 4; +const SIGN_RESULT_USER_NAME: u8 = 5; + +#[repr(C)] +pub struct AuthenticatorArgsUser { + id_ptr: *const u8, + id_len: usize, + name: *const c_char, +} + +#[repr(C)] +pub struct AuthenticatorArgsChallenge { + ptr: *const u8, + len: usize, +} + +#[repr(C)] +pub struct AuthenticatorArgsPubCred { + ptr: *const i32, + len: usize, +} + +#[repr(C)] +pub struct AuthenticatorArgsOptions { + resident_key: bool, + user_verification: bool, + user_presence: bool, + force_none_attestation: bool, +} + +// Generates a new 64-bit transaction id with collision probability 2^-32. +fn new_tid() -> u64 { + thread_rng().gen::<u64>() +} + +unsafe fn from_raw(ptr: *const u8, len: usize) -> Vec<u8> { + slice::from_raw_parts(ptr, len).to_vec() +} + +/// # Safety +/// +/// This method must not be called on a handle twice, and the handle is unusable +/// after. +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_mgr_free(mgr: *mut AuthenticatorService) { + if !mgr.is_null() { + drop(Box::from_raw(mgr)); + } +} + +/// # Safety +/// +/// The handle returned by this method must be freed by the caller. +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_pkcd_new() -> *mut Ctap2PubKeyCredDescriptors { + Box::into_raw(Box::new(vec![])) +} + +/// # Safety +/// +/// This method must be used on an actual Ctap2PubKeyCredDescriptors CredDescriptorse +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_pkcd_add( + pkcd: *mut Ctap2PubKeyCredDescriptors, + id_ptr: *const u8, + id_len: usize, + transports: u8, +) { + (*pkcd).push(PublicKeyCredentialDescriptor { + id: from_raw(id_ptr, id_len), + transports: crate::AuthenticatorTransports::from_bits_truncate(transports).into(), + }); +} + +/// # Safety +/// +/// This method must not be called on a handle twice, and the handle is unusable +/// after. +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_pkcd_free(khs: *mut Ctap2PubKeyCredDescriptors) { + if !khs.is_null() { + drop(Box::from_raw(khs)); + } +} + +/// # Safety +/// +/// The handle returned by this method must be freed by the caller. +/// The returned handle can be used with all rust_u2f_mgr_*-functions as well +/// but uses CTAP2 as the underlying protocol. CTAP1 requests will be repackaged +/// into CTAP2 (if the device supports it) +#[no_mangle] +pub extern "C" fn rust_ctap2_mgr_new() -> *mut AuthenticatorService { + if let Ok(mut mgr) = AuthenticatorService::new(CtapVersion::CTAP2) { + mgr.add_detected_transports(); + Box::into_raw(Box::new(mgr)) + } else { + ptr::null_mut() + } +} + +fn rewrap_client_data( + client_data: CollectedClientDataWrapper, +) -> Result<CString, AuthenticatorError> { + let s = CString::new(client_data.serialized_data.clone()).map_err(|_| { + AuthenticatorError::Custom("Failed to transform client_data to C String".to_string()) + })?; + Ok(s) +} + +fn rewrap_register_result( + attestation_object: AttestationObject, + client_data: CollectedClientDataWrapper, +) -> Ctap2RegisterResult { + let s = rewrap_client_data(client_data)?; + Ok((attestation_object, s)) +} + +fn rewrap_sign_result( + assertion_object: AssertionObject, + client_data: CollectedClientDataWrapper, +) -> Ctap2SignResult { + let s = rewrap_client_data(client_data)?; + Ok((assertion_object, s)) +} + +/// # Safety +/// +/// This method should not be called on AuthenticatorService handles after +/// they've been freed +/// All input is copied and it is the callers responsibility to free appropriately. +/// Note: `KeyHandles` are used as `PublicKeyCredentialDescriptor`s for the exclude_list +/// to keep the API smaller, as they are essentially the same thing. +/// `PublicKeyCredentialParameters` in pub_cred_params are represented as i32 with +/// their COSE value (see: https://www.iana.org/assignments/cose/cose.xhtml#table-algorithms) +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_mgr_register( + mgr: *mut AuthenticatorService, + timeout: u64, + callback: Ctap2RegisterCallback, + status_callback: Ctap2StatusUpdateCallback, + challenge: AuthenticatorArgsChallenge, + relying_party_id: *const c_char, + origin_ptr: *const c_char, + user: AuthenticatorArgsUser, + pub_cred_params: AuthenticatorArgsPubCred, + exclude_list: *const Ctap2PubKeyCredDescriptors, + options: AuthenticatorArgsOptions, + pin_ptr: *const c_char, +) -> u64 { + if mgr.is_null() { + return 0; + } + + // Check buffers. + if challenge.ptr.is_null() + || origin_ptr.is_null() + || relying_party_id.is_null() + || user.id_ptr.is_null() + || user.name.is_null() + || exclude_list.is_null() + { + return 0; + } + + let pub_cred_params = match slice::from_raw_parts(pub_cred_params.ptr, pub_cred_params.len) + .iter() + .map(|x| PublicKeyCredentialParameters::try_from(*x)) + .collect() + { + Ok(x) => x, + Err(_) => { + return 0; + } + }; + let pin = if pin_ptr.is_null() { + None + } else { + Some(Pin::new(&CStr::from_ptr(pin_ptr).to_string_lossy())) + }; + let user = User { + id: from_raw(user.id_ptr, user.id_len), + name: Some(CStr::from_ptr(user.name).to_string_lossy().to_string()), // TODO(MS): Use to_str() and error out on failure? + display_name: None, + icon: None, + }; + let rp = RelyingParty { + id: CStr::from_ptr(relying_party_id) + .to_string_lossy() + .to_string(), + name: None, + icon: None, + }; + let origin = CStr::from_ptr(origin_ptr).to_string_lossy().to_string(); + let challenge = from_raw(challenge.ptr, challenge.len); + let exclude_list = (*exclude_list).clone(); + let force_none_attestation = options.force_none_attestation; + + let (status_tx, status_rx) = channel::<crate::StatusUpdate>(); + thread::spawn(move || loop { + match status_rx.recv() { + Ok(r) => { + let rb = Box::new(r); + status_callback(Box::into_raw(rb)); + } + Err(e) => { + status_callback(ptr::null_mut()); + error!("Error when receiving status update: {:?}", e); + return; + } + } + }); + + let tid = new_tid(); + + let state_callback = StateCallback::<crate::Result<RegisterResult>>::new(Box::new(move |rv| { + let res = match rv { + Ok(RegisterResult::CTAP1(..)) => Err(AuthenticatorError::VersionMismatch( + "rust_ctap2_mgr_register", + 2, + )), + Ok(RegisterResult::CTAP2(mut attestation_object, client_data)) => { + if force_none_attestation { + attestation_object.att_statement = AttestationStatement::None; + } + rewrap_register_result(attestation_object, client_data) + } + Err(e) => Err(e), + }; + + callback(tid, Box::into_raw(Box::new(res))); + })); + + let ctap_args = RegisterArgsCtap2 { + challenge, + relying_party: rp, + origin, + user, + pub_cred_params, + exclude_list, + options: MakeCredentialsOptions { + resident_key: options.resident_key.then(|| true), + user_verification: options.user_verification.then(|| true), + }, + extensions: Default::default(), + pin, + }; + + let res = (*mgr).register(timeout, ctap_args.into(), status_tx, state_callback); + + if res.is_ok() { + tid + } else { + 0 + } +} + +/// # Safety +/// +/// This method should not be called on AuthenticatorService handles after +/// they've been freed +/// Note: `KeyHandles` are used as `PublicKeyCredentialDescriptor`s for the exclude_list +/// to keep the API smaller, as they are essentially the same thing. +/// `PublicKeyCredentialParameters` in pub_cred_params are represented as i32 with +/// their COSE value (see: https://www.iana.org/assignments/cose/cose.xhtml#table-algorithms) +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_mgr_sign( + mgr: *mut AuthenticatorService, + timeout: u64, + callback: Ctap2SignCallback, + status_callback: Ctap2StatusUpdateCallback, + challenge: AuthenticatorArgsChallenge, + relying_party_id: *const c_char, + origin_ptr: *const c_char, + allow_list: *const Ctap2PubKeyCredDescriptors, + options: AuthenticatorArgsOptions, + pin_ptr: *const c_char, +) -> u64 { + if mgr.is_null() { + return 0; + } + + // Check buffers. + if challenge.ptr.is_null() + || origin_ptr.is_null() + || relying_party_id.is_null() + || allow_list.is_null() + { + return 0; + } + + let pin = if pin_ptr.is_null() { + None + } else { + Some(Pin::new(&CStr::from_ptr(pin_ptr).to_string_lossy())) + }; + let rpid = CStr::from_ptr(relying_party_id) + .to_string_lossy() + .to_string(); + let origin = CStr::from_ptr(origin_ptr).to_string_lossy().to_string(); + let challenge = from_raw(challenge.ptr, challenge.len); + let allow_list: Vec<_> = (*allow_list).clone(); + + let (status_tx, status_rx) = channel::<crate::StatusUpdate>(); + thread::spawn(move || loop { + match status_rx.recv() { + Ok(r) => { + let rb = Box::new(r); + status_callback(Box::into_raw(rb)); + } + Err(e) => { + status_callback(ptr::null_mut()); + error!("Error when receiving status update: {:?}", e); + return; + } + } + }); + + let single_key_handle = if allow_list.len() == 1 { + Some(allow_list.first().unwrap().clone()) + } else { + None + }; + + let tid = new_tid(); + let state_callback = StateCallback::<crate::Result<SignResult>>::new(Box::new(move |rv| { + let res = match rv { + Ok(SignResult::CTAP1(..)) => Err(AuthenticatorError::VersionMismatch( + "rust_ctap2_mgr_register", + 2, + )), + Ok(SignResult::CTAP2(mut assertion_object, client_data)) => { + // The token can omit sending back credentials, if the allow_list had only one + // entry. Thus we re-add that here now for all found assertions before handing it out. + assertion_object.0.iter_mut().for_each(|x| { + x.credentials = x.credentials.clone().or(single_key_handle.clone()); + }); + rewrap_sign_result(assertion_object, client_data) + } + Err(e) => Err(e), + }; + + callback(tid, Box::into_raw(Box::new(res))); + })); + let ctap_args = SignArgsCtap2 { + challenge, + origin, + relying_party_id: rpid, + allow_list, + options: GetAssertionOptions { + user_presence: options.user_presence.then(|| true), + user_verification: options.user_verification.then(|| true), + }, + extensions: Default::default(), + pin, + }; + + let res = (*mgr).sign(timeout, ctap_args.into(), status_tx, state_callback); + + if res.is_ok() { + tid + } else { + 0 + } +} + +// /// # Safety +// /// +// /// This method must be used on an actual U2FResult handle +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_register_result_error(res: *const Ctap2RegisterResult) -> u8 { + if res.is_null() { + return U2FTokenError::Unknown as u8; + } + + match &*res { + Ok(..) => 0, // No error, the request succeeded. + Err(e) => e.as_u2f_errorcode(), + } +} + +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_sign_result_error(res: *const Ctap2SignResult) -> u8 { + if res.is_null() { + return U2FTokenError::Unknown as u8; + } + + match &*res { + Ok(..) => 0, // No error, the request succeeded. + Err(e) => e.as_u2f_errorcode(), + } +} + +/// # Safety +/// +/// This method should not be called on RegisterResult handles after they've been +/// freed or a double-free will occur +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_register_res_free(res: *mut Ctap2RegisterResult) { + if !res.is_null() { + drop(Box::from_raw(res)); + } +} + +/// # Safety +/// +/// This method should not be called on SignResult handles after they've been +/// freed or a double-free will occur +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_sign_res_free(res: *mut Ctap2SignResult) { + if !res.is_null() { + drop(Box::from_raw(res)); + } +} + +/// # Safety +/// +/// This method should not be called AuthenticatorService handles after they've +/// been freed +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_mgr_cancel(mgr: *mut AuthenticatorService) { + if !mgr.is_null() { + // Ignore return value. + let _ = (*mgr).cancel(); + } +} + +unsafe fn client_data_len<T>( + res: *const Result<(T, CString), AuthenticatorError>, + len: *mut size_t, +) -> bool { + if res.is_null() || len.is_null() { + return false; + } + + match &*res { + Ok((_, client_data)) => { + *len = client_data.as_bytes().len(); + true + } + Err(_) => false, + } +} + +/// This function is used to get the length, prior to calling +/// rust_ctap2_register_result_client_data_copy() +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_register_result_client_data_len( + res: *const Ctap2RegisterResult, + len: *mut size_t, +) -> bool { + client_data_len(res, len) +} + +/// This function is used to get the length, prior to calling +/// rust_ctap2_sign_result_client_data_copy() +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_sign_result_client_data_len( + res: *const Ctap2SignResult, + len: *mut size_t, +) -> bool { + client_data_len(res, len) +} + +unsafe fn client_data_copy<T>( + res: *const Result<(T, CString), AuthenticatorError>, + dst: *mut c_char, +) -> bool { + if dst.is_null() || res.is_null() { + return false; + } + + match &*res { + Ok((_, client_data)) => { + ptr::copy_nonoverlapping(client_data.as_ptr(), dst, client_data.as_bytes().len()); + return true; + } + Err(_) => false, + } +} + +/// # Safety +/// +/// This method does not ensure anything about dst before copying, so +/// ensure it is long enough (using rust_ctap2_register_result_client_data_len) +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_register_result_client_data_copy( + res: *const Ctap2RegisterResult, + dst: *mut c_char, +) -> bool { + client_data_copy(res, dst) +} + +/// # Safety +/// +/// This method does not ensure anything about dst before copying, so +/// ensure it is long enough (using rust_ctap2_register_result_client_data_len) +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_sign_result_client_data_copy( + res: *const Ctap2SignResult, + dst: *mut c_char, +) -> bool { + client_data_copy(res, dst) +} + +/// # Safety +/// +/// This function is used to get how long the specific item is. +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_register_result_attestation_len( + res: *const Ctap2RegisterResult, + len: *mut size_t, +) -> bool { + if res.is_null() || len.is_null() { + return false; + } + + if let Ok((attestation, _)) = &*res { + if let Some(item_len) = serde_cbor::to_vec(&attestation).ok().map(|x| x.len()) { + *len = item_len; + return true; + } + } + false +} + +/// # Safety +/// +/// This method does not ensure anything about dst before copying, so +/// ensure it is long enough (using rust_ctap2_register_result_item_len) +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_register_result_attestation_copy( + res: *const Ctap2RegisterResult, + dst: *mut u8, +) -> bool { + if res.is_null() || dst.is_null() { + return false; + } + + if let Ok((attestation, _)) = &*res { + if let Ok(item) = serde_cbor::to_vec(&attestation) { + ptr::copy_nonoverlapping(item.as_ptr(), dst, item.len()); + return true; + } + } + false +} + +/// This function is used to get how many assertions there are in total +/// The returned number can be used as index-maximum to access individual +/// fields +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_sign_result_assertions_len( + res: *const Ctap2SignResult, + len: *mut size_t, +) -> bool { + if res.is_null() || len.is_null() { + return false; + } + + match &*res { + Ok((assertions, _)) => { + *len = assertions.0.len(); + return true; + } + Err(_) => false, + } +} + +fn sign_result_item_len(assertion: &Assertion, item_idx: u8) -> Option<usize> { + match item_idx { + SIGN_RESULT_PUBKEY_CRED_ID => assertion.credentials.as_ref().map(|x| x.id.len()), + // This is inefficent! Converting twice here. Once for len, once for copy + SIGN_RESULT_AUTH_DATA => assertion.auth_data.to_vec().ok().map(|x| x.len()), + SIGN_RESULT_SIGNATURE => Some(assertion.signature.len()), + SIGN_RESULT_USER_ID => assertion.user.as_ref().map(|u| u.id.len()), + SIGN_RESULT_USER_NAME => assertion + .user + .as_ref() + .map(|u| { + u.display_name + .as_ref() + .or(u.name.as_ref()) + .map(|n| n.as_bytes().len()) + }) + .flatten(), + _ => None, + } +} + +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_sign_result_item_contains( + res: *const Ctap2SignResult, + assertion_idx: usize, + item_idx: u8, +) -> bool { + if res.is_null() { + return false; + } + + match &*res { + Ok((assertions, _)) => { + if assertion_idx >= assertions.0.len() { + return false; + } + if item_idx == SIGN_RESULT_AUTH_DATA { + // Short-cut to avoid serializing auth_data + return true; + } + sign_result_item_len(&assertions.0[assertion_idx], item_idx).is_some() + } + Err(_) => false, + } +} + +/// # Safety +/// +/// This function is used to get how long the specific item is. +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_sign_result_item_len( + res: *const Ctap2SignResult, + assertion_idx: usize, + item_idx: u8, + len: *mut size_t, +) -> bool { + if res.is_null() || len.is_null() { + return false; + } + + match &*res { + Ok((assertions, _)) => { + if assertion_idx >= assertions.0.len() { + return false; + } + + if let Some(item_len) = sign_result_item_len(&assertions.0[assertion_idx], item_idx) { + *len = item_len; + true + } else { + false + } + } + Err(_) => false, + } +} + +unsafe fn sign_result_item_copy(assertion: &Assertion, item_idx: u8, dst: *mut u8) -> bool { + if dst.is_null() { + return false; + } + + let tmp_val; + let item = match item_idx { + SIGN_RESULT_PUBKEY_CRED_ID => assertion.credentials.as_ref().map(|x| x.id.as_ref()), + // This is inefficent! Converting twice here. Once for len, once for copy + SIGN_RESULT_AUTH_DATA => { + tmp_val = assertion.auth_data.to_vec().ok(); + tmp_val.as_ref().map(|x| x.as_ref()) + } + SIGN_RESULT_SIGNATURE => Some(assertion.signature.as_ref()), + SIGN_RESULT_USER_ID => assertion.user.as_ref().map(|u| u.id.as_ref()), + SIGN_RESULT_USER_NAME => assertion + .user + .as_ref() + .map(|u| { + u.display_name + .as_ref() + .or(u.name.as_ref()) + .map(|n| n.as_bytes().as_ref()) + }) + .flatten(), + _ => None, + }; + + if let Some(item) = item { + ptr::copy_nonoverlapping(item.as_ptr(), dst, item.len()); + true + } else { + false + } +} + +/// # Safety +/// +/// This method does not ensure anything about dst before copying, so +/// ensure it is long enough (using rust_ctap2_sign_result_item_len) +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_sign_result_item_copy( + res: *const Ctap2SignResult, + assertion_idx: usize, + item_idx: u8, + dst: *mut u8, +) -> bool { + if res.is_null() || dst.is_null() { + return false; + } + + match &*res { + Ok((assertions, _)) => { + if assertion_idx >= assertions.0.len() { + return false; + } + + sign_result_item_copy(&assertions.0[assertion_idx], item_idx, dst) + } + Err(_) => false, + } +} + +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_sign_result_contains_username( + res: *const Ctap2SignResult, + assertion_idx: usize, +) -> bool { + if res.is_null() { + return false; + } + + match &*res { + Ok((assertions, _)) => { + if assertion_idx >= assertions.0.len() { + return false; + } + assertions.0[assertion_idx] + .user + .as_ref() + .map(|u| u.display_name.as_ref().or(u.name.as_ref())) + .is_some() + } + Err(_) => false, + } +} + +/// # Safety +/// +/// This function is used to get how long the specific username is. +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_sign_result_username_len( + res: *const Ctap2SignResult, + assertion_idx: usize, + len: *mut size_t, +) -> bool { + if res.is_null() || len.is_null() { + return false; + } + + match &*res { + Ok((assertions, _)) => { + if assertion_idx >= assertions.0.len() { + return false; + } + + if let Some(name_len) = assertions.0[assertion_idx] + .user + .as_ref() + .map(|u| u.display_name.as_ref().or(u.name.as_ref())) + .flatten() + .map(|x| x.as_bytes().len()) + { + *len = name_len; + true + } else { + false + } + } + Err(_) => false, + } +} + +/// # Safety +/// +/// This method does not ensure anything about dst before copying, so +/// ensure it is long enough (using rust_ctap2_sign_result_username_len) +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_sign_result_username_copy( + res: *const Ctap2SignResult, + assertion_idx: usize, + dst: *mut c_char, +) -> bool { + if res.is_null() || dst.is_null() { + return false; + } + + match &*res { + Ok((assertions, _)) => { + if assertion_idx >= assertions.0.len() { + return false; + } + + if let Some(name) = assertions.0[assertion_idx] + .user + .as_ref() + .map(|u| u.display_name.as_ref().or(u.name.as_ref())) + .flatten() + .map(|u| CString::new(u.clone()).ok()) + .flatten() + { + ptr::copy_nonoverlapping(name.as_ptr(), dst, name.as_bytes().len()); + true + } else { + false + } + } + + Err(_) => false, + } +} + +/// # Safety +/// +/// This function is used to get how long the JSON-representation of a status update is. +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_status_update_len( + res: *const StatusUpdate, + len: *mut size_t, +) -> bool { + if res.is_null() || len.is_null() { + return false; + } + + match serde_json::to_string(&*res) { + Ok(s) => { + *len = s.len(); + true + } + Err(e) => { + error!("Failed to parse {:?} into json: {:?}", &*res, e); + false + } + } +} + +/// # Safety +/// +/// This method does not ensure anything about dst before copying, so +/// ensure it is long enough (using rust_ctap2_status_update_len) +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_status_update_copy_json( + res: *const StatusUpdate, + dst: *mut c_char, +) -> bool { + if res.is_null() || dst.is_null() { + return false; + } + + match serde_json::to_string(&*res) { + Ok(s) => { + if let Ok(cs) = CString::new(s) { + ptr::copy_nonoverlapping(cs.as_ptr(), dst, cs.as_bytes().len()); + true + } else { + error!("Failed to convert String to CString"); + false + } + } + Err(e) => { + error!("Failed to parse {:?} into json: {:?}", &*res, e); + false + } + } +} + +/// # Safety +/// +/// We copy the pin, so it is the callers responsibility to free the argument +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_status_update_send_pin( + res: *const StatusUpdate, + c_pin: *mut c_char, +) -> bool { + if res.is_null() || c_pin.is_null() { + return false; + } + + match &*res { + StatusUpdate::PinError(_, sender) => { + if let Ok(pin) = CStr::from_ptr(c_pin).to_str() { + sender + .send(Pin::new(pin)) + .map_err(|e| { + error!("Failed to send PIN to device-thread"); + e + }) + .is_ok() + } else { + error!("Failed to convert PIN from c_char to String"); + false + } + } + _ => { + error!("Wrong state!"); + false + } + } +} + +/// # Safety +/// +/// This function frees the memory of res! +#[no_mangle] +pub unsafe extern "C" fn rust_ctap2_destroy_status_update_res(res: *mut StatusUpdate) -> bool { + if res.is_null() { + return false; + } + // Dropping it when we go out of scope + drop(Box::from_raw(res)); + true +} diff --git a/third_party/rust/authenticator/src/errors.rs b/third_party/rust/authenticator/src/errors.rs new file mode 100644 index 0000000000..ee8959a9a8 --- /dev/null +++ b/third_party/rust/authenticator/src/errors.rs @@ -0,0 +1,140 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +pub use crate::ctap2::commands::{client_pin::PinError, CommandError}; +pub use crate::transport::errors::HIDError; +use std::fmt; +use std::io; +use std::sync::mpsc; + +// This composite error type is patterned from Phil Daniels' blog: +// https://www.philipdaniels.com/blog/2019/defining-rust-error-types/ + +#[derive(Debug)] +pub enum UnsupportedOption { + MaxPinLength, + HmacSecret, +} + +#[derive(Debug)] +pub enum AuthenticatorError { + // Errors from external libraries... + Io(io::Error), + // Errors raised by us... + InvalidRelyingPartyInput, + NoConfiguredTransports, + Platform, + InternalError(String), + U2FToken(U2FTokenError), + Custom(String), + VersionMismatch(&'static str, u32), + HIDError(HIDError), + CryptoError, + PinError(PinError), + UnsupportedOption(UnsupportedOption), +} + +impl AuthenticatorError { + pub fn as_u2f_errorcode(&self) -> u8 { + match *self { + AuthenticatorError::U2FToken(ref err) => *err as u8, + // TODO: This is somewhat ugly, as we hardcode the error code here, instead of using the + // const defined in `u2fhid-capi.h`, which we should. + AuthenticatorError::PinError(PinError::PinRequired) => 6u8, + AuthenticatorError::PinError(PinError::InvalidPin(_)) => 7u8, + AuthenticatorError::PinError(PinError::PinAuthBlocked) => 8u8, + AuthenticatorError::PinError(PinError::PinBlocked) => 9u8, + _ => U2FTokenError::Unknown as u8, + } + } +} + +impl std::error::Error for AuthenticatorError {} + +impl fmt::Display for AuthenticatorError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + AuthenticatorError::Io(ref err) => err.fmt(f), + AuthenticatorError::InvalidRelyingPartyInput => { + write!(f, "invalid input from relying party") + } + AuthenticatorError::NoConfiguredTransports => write!( + f, + "no transports were configured in the authenticator service" + ), + AuthenticatorError::Platform => write!(f, "unknown platform error"), + AuthenticatorError::InternalError(ref err) => write!(f, "internal error: {}", err), + AuthenticatorError::U2FToken(ref err) => { + write!(f, "A u2f token error occurred {:?}", err) + } + AuthenticatorError::Custom(ref err) => write!(f, "A custom error occurred {:?}", err), + AuthenticatorError::VersionMismatch(manager, version) => write!( + f, + "{} expected arguments of version CTAP{}", + manager, version + ), + AuthenticatorError::HIDError(ref e) => write!(f, "Device error: {}", e), + AuthenticatorError::CryptoError => { + write!(f, "The cryptography implementation encountered an error") + } + AuthenticatorError::PinError(ref e) => write!(f, "PIN Error: {}", e), + AuthenticatorError::UnsupportedOption(ref e) => { + write!(f, "Unsupported option: {:?}", e) + } + } + } +} + +impl From<io::Error> for AuthenticatorError { + fn from(err: io::Error) -> AuthenticatorError { + AuthenticatorError::Io(err) + } +} + +impl From<HIDError> for AuthenticatorError { + fn from(err: HIDError) -> AuthenticatorError { + AuthenticatorError::HIDError(err) + } +} + +impl From<CommandError> for AuthenticatorError { + fn from(err: CommandError) -> AuthenticatorError { + AuthenticatorError::HIDError(HIDError::Command(err)) + } +} + +impl<T> From<mpsc::SendError<T>> for AuthenticatorError { + fn from(err: mpsc::SendError<T>) -> AuthenticatorError { + AuthenticatorError::InternalError(err.to_string()) + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum U2FTokenError { + Unknown = 1, + NotSupported = 2, + InvalidState = 3, + ConstraintError = 4, + NotAllowed = 5, +} + +impl U2FTokenError { + fn as_str(&self) -> &str { + match *self { + U2FTokenError::Unknown => "unknown", + U2FTokenError::NotSupported => "not supported", + U2FTokenError::InvalidState => "invalid state", + U2FTokenError::ConstraintError => "constraint error", + U2FTokenError::NotAllowed => "not allowed", + } + } +} + +impl std::fmt::Display for U2FTokenError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +impl std::error::Error for U2FTokenError {} diff --git a/third_party/rust/authenticator/src/lib.rs b/third_party/rust/authenticator/src/lib.rs new file mode 100644 index 0000000000..1b271af315 --- /dev/null +++ b/third_party/rust/authenticator/src/lib.rs @@ -0,0 +1,110 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#[macro_use] +mod util; + +#[cfg(any(target_os = "linux"))] +extern crate libudev; + +#[cfg(any(target_os = "freebsd"))] +extern crate devd_rs; + +#[cfg(any(target_os = "macos"))] +extern crate core_foundation; + +extern crate libc; +#[macro_use] +extern crate log; +extern crate rand; +extern crate runloop; + +#[macro_use] +extern crate bitflags; + +pub mod authenticatorservice; +mod consts; +mod statemachine; +mod u2fprotocol; +mod u2ftypes; + +mod manager; +pub use crate::manager::U2FManager; + +mod capi; +pub use crate::capi::*; + +pub mod ctap2; +pub use ctap2::attestation::AttestationObject; +pub use ctap2::client_data::{CollectedClientData, CollectedClientDataWrapper}; +pub use ctap2::commands::client_pin::{Pin, PinError}; +pub use ctap2::AssertionObject; + +mod ctap2_capi; +pub use crate::ctap2_capi::*; + +pub mod errors; +pub mod statecallback; +mod transport; +mod virtualdevices; + +mod status_update; +pub use status_update::*; + +mod crypto; +pub use crypto::COSEAlgorithm; + +// Keep this in sync with the constants in u2fhid-capi.h. +bitflags! { + pub struct RegisterFlags: u64 { + const REQUIRE_RESIDENT_KEY = 1; + const REQUIRE_USER_VERIFICATION = 2; + const REQUIRE_PLATFORM_ATTACHMENT = 4; + } +} +bitflags! { + pub struct SignFlags: u64 { + const REQUIRE_USER_VERIFICATION = 1; + } +} +bitflags! { + pub struct AuthenticatorTransports: u8 { + const USB = 1; + const NFC = 2; + const BLE = 4; + } +} + +#[derive(Debug, Clone)] +pub struct KeyHandle { + pub credential: Vec<u8>, + pub transports: AuthenticatorTransports, +} + +pub type AppId = Vec<u8>; + +pub enum RegisterResult { + CTAP1(Vec<u8>, u2ftypes::U2FDeviceInfo), + CTAP2(AttestationObject, CollectedClientDataWrapper), +} + +pub enum SignResult { + CTAP1(AppId, Vec<u8>, Vec<u8>, u2ftypes::U2FDeviceInfo), + CTAP2(AssertionObject, CollectedClientDataWrapper), +} + +pub type ResetResult = (); + +pub type Result<T> = std::result::Result<T, errors::AuthenticatorError>; + +#[cfg(test)] +#[macro_use] +extern crate assert_matches; + +#[cfg(fuzzing)] +pub use consts::*; +#[cfg(fuzzing)] +pub use u2fprotocol::*; +#[cfg(fuzzing)] +pub use u2ftypes::*; diff --git a/third_party/rust/authenticator/src/manager.rs b/third_party/rust/authenticator/src/manager.rs new file mode 100644 index 0000000000..5b95c252d9 --- /dev/null +++ b/third_party/rust/authenticator/src/manager.rs @@ -0,0 +1,576 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::authenticatorservice::AuthenticatorTransport; +use crate::authenticatorservice::{RegisterArgs, RegisterArgsCtap1, SignArgs}; +use crate::consts::PARAMETER_SIZE; +use crate::crypto::COSEAlgorithm; +use crate::ctap2::client_data::{CollectedClientData, WebauthnType}; +use crate::ctap2::commands::get_assertion::{GetAssertion, GetAssertionOptions}; +use crate::ctap2::commands::make_credentials::MakeCredentials; +use crate::ctap2::commands::make_credentials::MakeCredentialsOptions; +use crate::ctap2::server::{ + PublicKeyCredentialParameters, RelyingParty, RelyingPartyWrapper, RpIdHash, +}; +use crate::errors::*; +use crate::statecallback::StateCallback; +use crate::statemachine::{StateMachine, StateMachineCtap2}; +use crate::{Pin, SignFlags}; +use runloop::RunLoop; +use std::io; +use std::sync::mpsc::{channel, RecvTimeoutError, Sender}; +use std::time::Duration; + +enum QueueAction { + RegisterCtap1 { + timeout: u64, + ctap_args: RegisterArgsCtap1, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::RegisterResult>>, + }, + RegisterCtap2 { + timeout: u64, + make_credentials: MakeCredentials, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::RegisterResult>>, + }, + SignCtap1 { + flags: crate::SignFlags, + timeout: u64, + challenge: Vec<u8>, + app_ids: Vec<crate::AppId>, + key_handles: Vec<crate::KeyHandle>, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::SignResult>>, + }, + SignCtap2 { + timeout: u64, + get_assertion: GetAssertion, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::SignResult>>, + }, + Cancel, + Reset { + timeout: u64, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::ResetResult>>, + }, + SetPin { + timeout: u64, + new_pin: Pin, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::ResetResult>>, + }, +} + +pub struct U2FManager { + queue: RunLoop, + tx: Sender<QueueAction>, +} + +impl U2FManager { + pub fn new() -> io::Result<Self> { + let (tx, rx) = channel(); + + // Start a new work queue thread. + let queue = RunLoop::new(move |alive| { + let mut sm = StateMachine::new(); + + while alive() { + match rx.recv_timeout(Duration::from_millis(50)) { + Ok(QueueAction::RegisterCtap1 { + timeout, + ctap_args, + status, + callback, + }) => { + // This must not block, otherwise we can't cancel. + sm.register( + ctap_args.flags, + timeout, + ctap_args.challenge, + ctap_args.application, + ctap_args.key_handles, + status, + callback, + ); + } + Ok(QueueAction::SignCtap1 { + flags, + timeout, + challenge, + app_ids, + key_handles, + status, + callback, + }) => { + // This must not block, otherwise we can't cancel. + sm.sign( + flags, + timeout, + challenge, + app_ids, + key_handles, + status, + callback, + ); + } + Ok(QueueAction::Cancel) => { + // Cancelling must block so that we don't start a new + // polling thread before the old one has shut down. + sm.cancel(); + } + Ok(QueueAction::RegisterCtap2 { .. }) => { + // TODO(MS): What to do here? Error out? Silently ignore? + unimplemented!(); + } + Ok(QueueAction::SignCtap2 { .. }) => { + // TODO(MS): What to do here? Error out? Silently ignore? + unimplemented!(); + } + Ok(QueueAction::Reset { .. }) | Ok(QueueAction::SetPin { .. }) => { + unimplemented!(); + } + Err(RecvTimeoutError::Disconnected) => { + break; + } + _ => { /* continue */ } + } + } + + // Cancel any ongoing activity. + sm.cancel(); + })?; + + Ok(Self { queue, tx }) + } +} + +impl AuthenticatorTransport for U2FManager { + fn register( + &mut self, + timeout: u64, + ctap_args: RegisterArgs, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::RegisterResult>>, + ) -> crate::Result<()> { + let args = match ctap_args { + RegisterArgs::CTAP1(args) => args, + RegisterArgs::CTAP2(_) => { + return Err(AuthenticatorError::VersionMismatch("U2FManager", 1)); + } + }; + if args.challenge.len() != PARAMETER_SIZE || args.application.len() != PARAMETER_SIZE { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + + for key_handle in &args.key_handles { + if key_handle.credential.len() >= 256 { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + } + + let action = QueueAction::RegisterCtap1 { + timeout, + ctap_args: args, + status, + callback, + }; + Ok(self.tx.send(action)?) + } + + fn sign( + &mut self, + timeout: u64, + ctap_args: SignArgs, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::SignResult>>, + ) -> crate::Result<()> { + let args = match ctap_args { + SignArgs::CTAP1(args) => args, + SignArgs::CTAP2(_) => { + return Err(AuthenticatorError::VersionMismatch("U2FManager", 1)); + } + }; + + if args.challenge.len() != PARAMETER_SIZE { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + + if args.app_ids.is_empty() { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + + for app_id in &args.app_ids { + if app_id.len() != PARAMETER_SIZE { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + } + + for key_handle in &args.key_handles { + if key_handle.credential.len() >= 256 { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + } + + let action = QueueAction::SignCtap1 { + flags: args.flags, + timeout, + challenge: args.challenge, + app_ids: args.app_ids, + key_handles: args.key_handles, + status, + callback, + }; + Ok(self.tx.send(action)?) + } + + fn cancel(&mut self) -> crate::Result<()> { + Ok(self.tx.send(QueueAction::Cancel)?) + } + + fn reset( + &mut self, + timeout: u64, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::ResetResult>>, + ) -> crate::Result<()> { + Ok(self.tx.send(QueueAction::Reset { + timeout, + status, + callback, + })?) + } + + fn set_pin( + &mut self, + timeout: u64, + new_pin: Pin, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::ResetResult>>, + ) -> crate::Result<()> { + Ok(self.tx.send(QueueAction::SetPin { + timeout, + new_pin, + status, + callback, + })?) + } +} + +impl Drop for U2FManager { + fn drop(&mut self) { + self.queue.cancel(); + } +} + +pub struct Manager { + queue: RunLoop, + tx: Sender<QueueAction>, +} + +impl Manager { + pub fn new() -> io::Result<Self> { + let (tx, rx) = channel(); + + // Start a new work queue thread. + let queue = RunLoop::new(move |alive| { + let mut sm = StateMachineCtap2::new(); + + while alive() { + match rx.recv_timeout(Duration::from_millis(50)) { + Ok(QueueAction::RegisterCtap2 { + timeout, + make_credentials, + status, + callback, + }) => { + // This must not block, otherwise we can't cancel. + sm.register(timeout, make_credentials, status, callback); + } + + Ok(QueueAction::SignCtap2 { + timeout, + get_assertion, + status, + callback, + }) => { + // This must not block, otherwise we can't cancel. + sm.sign(timeout, get_assertion, status, callback); + } + + Ok(QueueAction::Cancel) => { + // Cancelling must block so that we don't start a new + // polling thread before the old one has shut down. + sm.cancel(); + } + + Ok(QueueAction::Reset { + timeout, + status, + callback, + }) => { + // Reset the token: Delete all keypairs, reset PIN + sm.reset(timeout, status, callback); + } + + Ok(QueueAction::SetPin { + timeout, + new_pin, + status, + callback, + }) => { + // This must not block, otherwise we can't cancel. + sm.set_pin(timeout, new_pin, status, callback); + } + + Ok(QueueAction::RegisterCtap1 { + timeout: _, + ctap_args: _, + status: _, + callback: _, + }) => { + // TODO(MS): Remove QueueAction::RegisterCtap1 once U2FManager is deleted. + // The repackaging from CTAP1 to CTAP2 happens in self.register() + unimplemented!(); + } + + Ok(QueueAction::SignCtap1 { + timeout: _, + callback: _, + flags: _, + challenge: _, + app_ids: _, + key_handles: _, + status: _, + }) => { + // TODO(MS): Remove QueueAction::SignCtap1 once U2FManager is deleted. + // The repackaging from CTAP1 to CTAP2 happens in self.sign() + unimplemented!() + } + + Err(RecvTimeoutError::Disconnected) => { + break; + } + + _ => { /* continue */ } + } + } + + // Cancel any ongoing activity. + sm.cancel(); + })?; + + Ok(Self { queue, tx }) + } +} + +impl Drop for Manager { + fn drop(&mut self) { + self.queue.cancel(); + } +} + +impl AuthenticatorTransport for Manager { + fn register( + &mut self, + timeout: u64, + ctap_args: RegisterArgs, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::RegisterResult>>, + ) -> Result<(), AuthenticatorError> { + let make_credentials = match ctap_args { + RegisterArgs::CTAP2(args) => { + let client_data = CollectedClientData { + webauthn_type: WebauthnType::Create, + challenge: args.challenge.into(), + origin: args.origin, + cross_origin: false, + token_binding: None, + }; + + MakeCredentials::new( + client_data, + RelyingPartyWrapper::Data(args.relying_party), + Some(args.user), + args.pub_cred_params, + args.exclude_list, + args.options, + args.extensions, + args.pin, + // pin_auth will be filled in Statemachine, once we have a device + ) + } + RegisterArgs::CTAP1(args) => { + let client_data = CollectedClientData { + webauthn_type: WebauthnType::Create, + challenge: args.challenge.into(), + origin: String::new(), + cross_origin: false, + token_binding: None, + }; + + MakeCredentials::new( + client_data, + RelyingPartyWrapper::Hash(RpIdHash::from(&args.application)?), + None, + vec![PublicKeyCredentialParameters { + alg: COSEAlgorithm::ES256, + }], + args.key_handles + .iter() + .map(|k| k.into()) + .collect::<Vec<_>>(), + MakeCredentialsOptions { + resident_key: None, + user_verification: None, + }, + Default::default(), + None, + ) + } + }?; + + let action = QueueAction::RegisterCtap2 { + timeout, + make_credentials, + status, + callback, + }; + Ok(self.tx.send(action)?) + } + + fn sign( + &mut self, + timeout: u64, + ctap_args: SignArgs, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::SignResult>>, + ) -> crate::Result<()> { + match ctap_args { + SignArgs::CTAP1(args) => { + if args.challenge.len() != PARAMETER_SIZE { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + + if args.app_ids.is_empty() { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + + let client_data = CollectedClientData { + webauthn_type: WebauthnType::Get, + challenge: args.challenge.into(), + origin: String::new(), + cross_origin: false, + token_binding: None, + }; + let options = if args.flags == SignFlags::empty() { + GetAssertionOptions::default() + } else { + GetAssertionOptions { + user_verification: Some( + args.flags.contains(SignFlags::REQUIRE_USER_VERIFICATION), + ), + ..GetAssertionOptions::default() + } + }; + + for app_id in &args.app_ids { + if app_id.len() != PARAMETER_SIZE { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + for key_handle in &args.key_handles { + if key_handle.credential.len() >= 256 { + return Err(AuthenticatorError::InvalidRelyingPartyInput); + } + let rp = RelyingPartyWrapper::Hash(RpIdHash::from(&app_id)?); + + let allow_list = vec![key_handle.into()]; + + let get_assertion = GetAssertion::new( + client_data.clone(), + rp, + allow_list, + options, + Default::default(), + None, + )?; + + let action = QueueAction::SignCtap2 { + timeout, + get_assertion, + status: status.clone(), + callback: callback.clone(), + }; + self.tx.send(action)?; + } + } + } + + SignArgs::CTAP2(args) => { + let client_data = CollectedClientData { + webauthn_type: WebauthnType::Get, + challenge: args.challenge.into(), + origin: args.origin, + cross_origin: false, + token_binding: None, + }; + + let get_assertion = GetAssertion::new( + client_data.clone(), + RelyingPartyWrapper::Data(RelyingParty { + id: args.relying_party_id, + name: None, + icon: None, + }), + args.allow_list, + args.options, + args.extensions, + args.pin, + )?; + + let action = QueueAction::SignCtap2 { + timeout, + get_assertion, + status, + callback, + }; + self.tx.send(action)?; + } + }; + Ok(()) + } + + fn cancel(&mut self) -> Result<(), AuthenticatorError> { + Ok(self.tx.send(QueueAction::Cancel)?) + } + + fn reset( + &mut self, + timeout: u64, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::ResetResult>>, + ) -> Result<(), AuthenticatorError> { + Ok(self.tx.send(QueueAction::Reset { + timeout, + status, + callback, + })?) + } + + fn set_pin( + &mut self, + timeout: u64, + new_pin: Pin, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::ResetResult>>, + ) -> crate::Result<()> { + Ok(self.tx.send(QueueAction::SetPin { + timeout, + new_pin, + status, + callback, + })?) + } +} diff --git a/third_party/rust/authenticator/src/statecallback.rs b/third_party/rust/authenticator/src/statecallback.rs new file mode 100644 index 0000000000..ce1caf3e7c --- /dev/null +++ b/third_party/rust/authenticator/src/statecallback.rs @@ -0,0 +1,166 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::sync::{Arc, Condvar, Mutex}; + +pub struct StateCallback<T> { + callback: Arc<Mutex<Option<Box<dyn Fn(T) + Send>>>>, + observer: Arc<Mutex<Option<Box<dyn Fn() + Send>>>>, + condition: Arc<(Mutex<bool>, Condvar)>, +} + +impl<T> StateCallback<T> { + // This is used for the Condvar, which requires this kind of construction + #[allow(clippy::mutex_atomic)] + pub fn new(cb: Box<dyn Fn(T) + Send>) -> Self { + Self { + callback: Arc::new(Mutex::new(Some(cb))), + observer: Arc::new(Mutex::new(None)), + condition: Arc::new((Mutex::new(true), Condvar::new())), + } + } + + pub fn add_uncloneable_observer(&mut self, obs: Box<dyn Fn() + Send>) { + let mut opt = self.observer.lock().unwrap(); + if opt.is_some() { + error!("Replacing an already-set observer.") + } + opt.replace(obs); + } + + pub fn call(&self, rv: T) { + if let Some(cb) = self.callback.lock().unwrap().take() { + cb(rv); + + if let Some(obs) = self.observer.lock().unwrap().take() { + obs(); + } + } + + let (lock, cvar) = &*self.condition; + let mut pending = lock.lock().unwrap(); + *pending = false; + cvar.notify_all(); + } + + pub fn wait(&self) { + let (lock, cvar) = &*self.condition; + let _useless_guard = cvar + .wait_while(lock.lock().unwrap(), |pending| *pending) + .unwrap(); + } +} + +impl<T> Clone for StateCallback<T> { + fn clone(&self) -> Self { + Self { + callback: self.callback.clone(), + observer: Arc::new(Mutex::new(None)), + condition: self.condition.clone(), + } + } +} + +#[cfg(test)] +mod tests { + use super::StateCallback; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Barrier}; + use std::thread; + + #[test] + fn test_statecallback_is_single_use() { + let counter = Arc::new(AtomicUsize::new(0)); + let counter_clone = counter.clone(); + let sc = StateCallback::new(Box::new(move |_| { + counter_clone.fetch_add(1, Ordering::SeqCst); + })); + + assert_eq!(counter.load(Ordering::SeqCst), 0); + for _ in 0..10 { + sc.call(()); + assert_eq!(counter.load(Ordering::SeqCst), 1); + } + + for _ in 0..10 { + sc.clone().call(()); + assert_eq!(counter.load(Ordering::SeqCst), 1); + } + } + + #[test] + fn test_statecallback_observer_is_single_use() { + let counter = Arc::new(AtomicUsize::new(0)); + let counter_clone = counter.clone(); + let mut sc = StateCallback::<()>::new(Box::new(move |_| {})); + + sc.add_uncloneable_observer(Box::new(move || { + counter_clone.fetch_add(1, Ordering::SeqCst); + })); + + assert_eq!(counter.load(Ordering::SeqCst), 0); + for _ in 0..10 { + sc.call(()); + assert_eq!(counter.load(Ordering::SeqCst), 1); + } + + for _ in 0..10 { + sc.clone().call(()); + assert_eq!(counter.load(Ordering::SeqCst), 1); + } + } + + #[test] + fn test_statecallback_observer_only_runs_for_completing_callback() { + let cb_counter = Arc::new(AtomicUsize::new(0)); + let cb_counter_clone = cb_counter.clone(); + let sc = StateCallback::new(Box::new(move |_| { + cb_counter_clone.fetch_add(1, Ordering::SeqCst); + })); + + let obs_counter = Arc::new(AtomicUsize::new(0)); + + for _ in 0..10 { + let obs_counter_clone = obs_counter.clone(); + let mut c = sc.clone(); + c.add_uncloneable_observer(Box::new(move || { + obs_counter_clone.fetch_add(1, Ordering::SeqCst); + })); + + c.call(()); + + assert_eq!(cb_counter.load(Ordering::SeqCst), 1); + assert_eq!(obs_counter.load(Ordering::SeqCst), 1); + } + } + + #[test] + #[allow(clippy::redundant_clone)] + fn test_statecallback_observer_unclonable() { + let mut sc = StateCallback::<()>::new(Box::new(move |_| {})); + sc.add_uncloneable_observer(Box::new(move || {})); + + assert!(sc.observer.lock().unwrap().is_some()); + // This is deliberate, to force an extra clone + assert!(sc.clone().observer.lock().unwrap().is_none()); + } + + #[test] + fn test_statecallback_wait() { + let sc = StateCallback::<()>::new(Box::new(move |_| {})); + let barrier = Arc::new(Barrier::new(2)); + + { + let c = sc.clone(); + let b = barrier.clone(); + thread::spawn(move || { + b.wait(); + c.call(()); + }); + } + + barrier.wait(); + sc.wait(); + } +} diff --git a/third_party/rust/authenticator/src/statemachine.rs b/third_party/rust/authenticator/src/statemachine.rs new file mode 100644 index 0000000000..e6a27a7550 --- /dev/null +++ b/third_party/rust/authenticator/src/statemachine.rs @@ -0,0 +1,829 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::consts::PARAMETER_SIZE; +use crate::ctap2::commands::client_pin::{ChangeExistingPin, Pin, PinError, SetNewPin}; +use crate::ctap2::commands::get_assertion::{GetAssertion, GetAssertionResult}; +use crate::ctap2::commands::make_credentials::{MakeCredentials, MakeCredentialsResult}; +use crate::ctap2::commands::reset::Reset; +use crate::ctap2::commands::{ + repackage_pin_errors, CommandError, PinAuthCommand, Request, StatusCode, +}; +use crate::errors::{self, AuthenticatorError, UnsupportedOption}; +use crate::statecallback::StateCallback; +use crate::transport::device_selector::{ + BlinkResult, Device, DeviceBuildParameters, DeviceCommand, DeviceSelectorEvent, +}; +use crate::transport::platform::transaction::Transaction; +use crate::transport::{errors::HIDError, hid::HIDDevice, FidoDevice, Nonce}; +use crate::u2fprotocol::{u2f_init_device, u2f_is_keyhandle_valid, u2f_register, u2f_sign}; +use crate::u2ftypes::U2FDevice; +use crate::{send_status, RegisterResult, SignResult, StatusUpdate}; +use std::sync::mpsc::{channel, Sender}; +use std::thread; +use std::time::Duration; + +fn is_valid_transport(transports: crate::AuthenticatorTransports) -> bool { + transports.is_empty() || transports.contains(crate::AuthenticatorTransports::USB) +} + +fn find_valid_key_handles<'a, F>( + app_ids: &'a [crate::AppId], + key_handles: &'a [crate::KeyHandle], + mut is_valid: F, +) -> (&'a crate::AppId, Vec<&'a crate::KeyHandle>) +where + F: FnMut(&Vec<u8>, &crate::KeyHandle) -> bool, +{ + // Try all given app_ids in order. + for app_id in app_ids { + // Find all valid key handles for the current app_id. + let valid_handles = key_handles + .iter() + .filter(|key_handle| is_valid(app_id, key_handle)) + .collect::<Vec<_>>(); + + // If there's at least one, stop. + if !valid_handles.is_empty() { + return (app_id, valid_handles); + } + } + + (&app_ids[0], vec![]) +} + +#[derive(Default)] +pub struct StateMachine { + transaction: Option<Transaction>, +} + +impl StateMachine { + pub fn new() -> Self { + Default::default() + } + + pub fn register( + &mut self, + flags: crate::RegisterFlags, + timeout: u64, + challenge: Vec<u8>, + application: crate::AppId, + key_handles: Vec<crate::KeyHandle>, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::RegisterResult>>, + ) { + // Abort any prior register/sign calls. + self.cancel(); + + let cbc = callback.clone(); + + let transaction = Transaction::new( + timeout, + cbc.clone(), + status, + move |info, _, status, alive| { + // Create a new device. + let dev = &mut match Device::new(info) { + Ok(dev) => dev, + _ => return, + }; + + // Try initializing it. + if !dev.is_u2f() || !u2f_init_device(dev) { + return; + } + + // We currently support none of the authenticator selection + // criteria because we can't ask tokens whether they do support + // those features. If flags are set, ignore all tokens for now. + // + // Technically, this is a ConstraintError because we shouldn't talk + // to this authenticator in the first place. But the result is the + // same anyway. + if !flags.is_empty() { + return; + } + + send_status( + &status, + crate::StatusUpdate::DeviceAvailable { + dev_info: dev.get_device_info(), + }, + ); + + // Iterate the exclude list and see if there are any matches. + // If so, we'll keep polling the device anyway to test for user + // consent, to be consistent with CTAP2 device behavior. + let excluded = key_handles.iter().any(|key_handle| { + is_valid_transport(key_handle.transports) + && u2f_is_keyhandle_valid( + dev, + &challenge, + &application, + &key_handle.credential, + ) + .unwrap_or(false) /* no match on failure */ + }); + + while alive() { + if excluded { + let blank = vec![0u8; PARAMETER_SIZE]; + if u2f_register(dev, &blank, &blank).is_ok() { + callback.call(Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::InvalidState, + ))); + break; + } + } else if let Ok(bytes) = u2f_register(dev, &challenge, &application) { + let dev_info = dev.get_device_info(); + send_status( + &status, + crate::StatusUpdate::Success { + dev_info: dev.get_device_info(), + }, + ); + callback.call(Ok(RegisterResult::CTAP1(bytes, dev_info))); + break; + } + + // Sleep a bit before trying again. + thread::sleep(Duration::from_millis(100)); + } + + send_status( + &status, + crate::StatusUpdate::DeviceUnavailable { + dev_info: dev.get_device_info(), + }, + ); + }, + ); + + self.transaction = Some(try_or!(transaction, |e| cbc.call(Err(e)))); + } + + pub fn sign( + &mut self, + flags: crate::SignFlags, + timeout: u64, + challenge: Vec<u8>, + app_ids: Vec<crate::AppId>, + key_handles: Vec<crate::KeyHandle>, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::SignResult>>, + ) { + // Abort any prior register/sign calls. + self.cancel(); + + let cbc = callback.clone(); + + let transaction = Transaction::new( + timeout, + cbc.clone(), + status, + move |info, _, status, alive| { + // Create a new device. + let dev = &mut match Device::new(info) { + Ok(dev) => dev, + _ => return, + }; + + // Try initializing it. + if !dev.is_u2f() || !u2f_init_device(dev) { + return; + } + + // We currently don't support user verification because we can't + // ask tokens whether they do support that. If the flag is set, + // ignore all tokens for now. + // + // Technically, this is a ConstraintError because we shouldn't talk + // to this authenticator in the first place. But the result is the + // same anyway. + if !flags.is_empty() { + return; + } + + // For each appId, try all key handles. If there's at least one + // valid key handle for an appId, we'll use that appId below. + let (app_id, valid_handles) = + find_valid_key_handles(&app_ids, &key_handles, |app_id, key_handle| { + u2f_is_keyhandle_valid(dev, &challenge, app_id, &key_handle.credential) + .unwrap_or(false) /* no match on failure */ + }); + + // Aggregate distinct transports from all given credentials. + let transports = key_handles + .iter() + .fold(crate::AuthenticatorTransports::empty(), |t, k| { + t | k.transports + }); + + // We currently only support USB. If the RP specifies transports + // and doesn't include USB it's probably lying. + if !is_valid_transport(transports) { + return; + } + + send_status( + &status, + crate::StatusUpdate::DeviceAvailable { + dev_info: dev.get_device_info(), + }, + ); + + 'outer: while alive() { + // If the device matches none of the given key handles + // then just make it blink with bogus data. + if valid_handles.is_empty() { + let blank = vec![0u8; PARAMETER_SIZE]; + if u2f_register(dev, &blank, &blank).is_ok() { + callback.call(Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::InvalidState, + ))); + break; + } + } else { + // Otherwise, try to sign. + for key_handle in &valid_handles { + if let Ok(bytes) = + u2f_sign(dev, &challenge, app_id, &key_handle.credential) + { + let dev_info = dev.get_device_info(); + send_status( + &status, + crate::StatusUpdate::Success { + dev_info: dev.get_device_info(), + }, + ); + callback.call(Ok(SignResult::CTAP1( + app_id.clone(), + key_handle.credential.clone(), + bytes, + dev_info, + ))); + break 'outer; + } + } + } + + // Sleep a bit before trying again. + thread::sleep(Duration::from_millis(100)); + } + + send_status( + &status, + crate::StatusUpdate::DeviceUnavailable { + dev_info: dev.get_device_info(), + }, + ); + }, + ); + + self.transaction = Some(try_or!(transaction, |e| cbc.call(Err(e)))); + } + + // This blocks. + pub fn cancel(&mut self) { + if let Some(mut transaction) = self.transaction.take() { + transaction.cancel(); + } + } +} + +#[derive(Default)] +// TODO(MS): To be renamed to `StateMachine` once U2FManager and the original StateMachine can be removed. +pub struct StateMachineCtap2 { + transaction: Option<Transaction>, +} + +impl StateMachineCtap2 { + pub fn new() -> Self { + Default::default() + } + + fn init_and_select( + info: DeviceBuildParameters, + selector: &Sender<DeviceSelectorEvent>, + ctap2_only: bool, + ) -> Option<Device> { + // Create a new device. + let mut dev = match Device::new(info) { + Ok(dev) => dev, + Err((e, id)) => { + info!("error happened with device: {}", e); + selector.send(DeviceSelectorEvent::NotAToken(id)).ok()?; + return None; + } + }; + + // Try initializing it. + if let Err(e) = dev.init(Nonce::CreateRandom) { + warn!("error while initializing device: {}", e); + selector.send(DeviceSelectorEvent::NotAToken(dev.id())).ok(); + return None; + } + + if ctap2_only && dev.get_authenticator_info().is_none() { + info!("Device does not support CTAP2"); + selector.send(DeviceSelectorEvent::NotAToken(dev.id())).ok(); + return None; + } + + let write_only_clone = match dev.clone_device_as_write_only() { + Ok(x) => x, + Err(_) => { + // There is probably something seriously wrong here, if this happens. + // So `NotAToken()` is probably too weak a response here. + warn!("error while cloning device: {:?}", dev.id()); + selector + .send(DeviceSelectorEvent::NotAToken(dev.id())) + .ok()?; + return None; + } + }; + let (tx, rx) = channel(); + selector + .send(DeviceSelectorEvent::ImAToken((write_only_clone, tx))) + .ok()?; + + // Blocking recv. DeviceSelector will tell us what to do + loop { + match rx.recv() { + Ok(DeviceCommand::Blink) => match dev.block_and_blink() { + BlinkResult::DeviceSelected => { + // User selected us. Let DeviceSelector know, so it can cancel all other + // outstanding open blink-requests. + selector + .send(DeviceSelectorEvent::SelectedToken(dev.id())) + .ok()?; + break; + } + BlinkResult::Cancelled => { + info!("Device {:?} was not selected", dev.id()); + return None; + } + }, + Ok(DeviceCommand::Removed) => { + info!("Device {:?} was removed", dev.id()); + return None; + } + Ok(DeviceCommand::Continue) => { + break; + } + Err(_) => { + warn!("Error when trying to receive messages from DeviceSelector! Exiting."); + return None; + } + } + } + Some(dev) + } + + fn ask_user_for_pin<U>( + error: PinError, + status: &Sender<StatusUpdate>, + callback: &StateCallback<crate::Result<U>>, + ) -> Result<Pin, ()> { + info!("PIN Error that requires user interaction detected. Sending it back and waiting for a reply"); + let (tx, rx) = channel(); + send_status(status, crate::StatusUpdate::PinError(error.clone(), tx)); + match rx.recv() { + Ok(pin) => Ok(pin), + Err(_) => { + // recv() can only fail, if the other side is dropping the Sender. We are using this as a trick + // to let the callback decide if this PinError is recoverable (e.g. with User input) or not (e.g. + // locked token). If it is deemed unrecoverable, we error out the 'normal' way with the same error. + error!("Callback dropped the channel, so we forward the error to the results-callback: {:?}", error); + callback.call(Err(AuthenticatorError::PinError(error))); + return Err(()); + } + } + } + + fn determine_pin_auth<T: PinAuthCommand, U>( + cmd: &mut T, + dev: &mut Device, + status: &Sender<StatusUpdate>, + callback: &StateCallback<crate::Result<U>>, + ) -> Result<(), ()> { + loop { + match cmd.determine_pin_auth(dev) { + Ok(_) => { + break; + } + Err(AuthenticatorError::PinError(e)) => { + let pin = Self::ask_user_for_pin(e, status, callback)?; + cmd.set_pin(Some(pin)); + continue; + } + Err(e) => { + error!("Error when determining pinAuth: {:?}", e); + callback.call(Err(e)); + return Err(()); + } + }; + } + + // CTAP 2.0 spec is a bit vague here, but CTAP 2.1 is very specific, that the request + // should either include pinAuth OR uv=true, but not both at the same time. + // Do not set user_verification, if pinAuth is provided + if cmd.pin_auth().is_some() { + cmd.unset_uv_option(); + } + + Ok(()) + } + + pub fn register( + &mut self, + timeout: u64, + params: MakeCredentials, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::RegisterResult>>, + ) { + // Abort any prior register/sign calls. + self.cancel(); + let cbc = callback.clone(); + let transaction = Transaction::new( + timeout, + cbc.clone(), + status, + move |info, selector, status, _alive| { + let mut dev = match Self::init_and_select(info, &selector, false) { + None => { + return; + } + Some(dev) => dev, + }; + + info!("Device {:?} continues with the register process", dev.id()); + // TODO(baloo): not sure about this, have to ask + // We currently support none of the authenticator selection + // criteria because we can't ask tokens whether they do support + // those features. If flags are set, ignore all tokens for now. + // + // Technically, this is a ConstraintError because we shouldn't talk + // to this authenticator in the first place. But the result is the + // same anyway. + //if !flags.is_empty() { + // return; + //} + + // TODO(baloo): not sure about this, have to ask + // Iterate the exclude list and see if there are any matches. + // If so, we'll keep polling the device anyway to test for user + // consent, to be consistent with CTAP2 device behavior. + //let excluded = key_handles.iter().any(|key_handle| { + // is_valid_transport(key_handle.transports) + // && u2f_is_keyhandle_valid(dev, &challenge, &application, &key_handle.credential) + // .unwrap_or(false) /* no match on failure */ + //}); + + // TODO(MS): This is wasteful, but the current setup with read only-functions doesn't allow me + // to modify "params" directly. + let mut makecred = params.clone(); + if params.is_ctap2_request() { + // First check if extensions have been requested that are not supported by the device + if let Some(true) = params.extensions.hmac_secret { + if let Some(auth) = dev.get_authenticator_info() { + if !auth.supports_hmac_secret() { + callback.call(Err(AuthenticatorError::UnsupportedOption( + UnsupportedOption::HmacSecret, + ))); + return; + } + } + } + + // Second, ask for PIN and get the shared secret + if Self::determine_pin_auth(&mut makecred, &mut dev, &status, &callback) + .is_err() + { + return; + } + } + debug!("------------------------------------------------------------------"); + debug!("{:?}", makecred); + debug!("------------------------------------------------------------------"); + let resp = dev.send_msg(&makecred); + if resp.is_ok() { + send_status( + &status, + crate::StatusUpdate::Success { + dev_info: dev.get_device_info(), + }, + ); + // The DeviceSelector could already be dead, but it might also wait + // for us to respond, in order to cancel all other tokens in case + // we skipped the "blinking"-action and went straight for the actual + // request. + let _ = selector.send(DeviceSelectorEvent::SelectedToken(dev.id())); + } + match resp { + Ok(MakeCredentialsResult::CTAP2(attestation, client_data)) => { + callback.call(Ok(RegisterResult::CTAP2(attestation, client_data))) + } + Ok(MakeCredentialsResult::CTAP1(data)) => { + callback.call(Ok(RegisterResult::CTAP1(data, dev.get_device_info()))) + } + + Err(HIDError::DeviceNotSupported) | Err(HIDError::UnsupportedCommand) => {} + Err(HIDError::Command(CommandError::StatusCode( + StatusCode::ChannelBusy, + _, + ))) => {} + Err(e) => { + warn!("error happened: {}", e); + callback.call(Err(AuthenticatorError::HIDError(e))); + } + } + }, + ); + + self.transaction = Some(try_or!(transaction, |e| cbc.call(Err(e)))); + } + + pub fn sign( + &mut self, + timeout: u64, + params: GetAssertion, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::SignResult>>, + ) { + // Abort any prior register/sign calls. + self.cancel(); + let cbc = callback.clone(); + + let transaction = Transaction::new( + timeout, + callback.clone(), + status, + move |info, selector, status, _alive| { + let mut dev = match Self::init_and_select(info, &selector, false) { + None => { + return; + } + Some(dev) => dev, + }; + + info!("Device {:?} continues with the signing process", dev.id()); + // TODO(MS): This is wasteful, but the current setup with read only-functions doesn't allow me + // to modify "params" directly. + let mut getassertion = params.clone(); + if params.is_ctap2_request() { + // First check if extensions have been requested that are not supported by the device + if params.extensions.hmac_secret.is_some() { + if let Some(auth) = dev.get_authenticator_info() { + if !auth.supports_hmac_secret() { + callback.call(Err(AuthenticatorError::UnsupportedOption( + UnsupportedOption::HmacSecret, + ))); + return; + } + } + } + + // Second, ask for PIN and get the shared secret + if Self::determine_pin_auth(&mut getassertion, &mut dev, &status, &callback) + .is_err() + { + return; + } + + // Third, use the shared secret in the extensions, if requested + if let Some(extension) = getassertion.extensions.hmac_secret.as_mut() { + if let Some(secret) = dev.get_shared_secret() { + match extension.calculate(secret) { + Ok(x) => x, + Err(e) => { + callback.call(Err(e)); + return; + } + } + } + } + } + + debug!("------------------------------------------------------------------"); + debug!("{:?}", getassertion); + debug!("------------------------------------------------------------------"); + + let resp = dev.send_msg(&getassertion); + if resp.is_ok() { + send_status( + &status, + crate::StatusUpdate::Success { + dev_info: dev.get_device_info(), + }, + ); + // The DeviceSelector could already be dead, but it might also wait + // for us to respond, in order to cancel all other tokens in case + // we skipped the "blinking"-action and went straight for the actual + // request. + let _ = selector.send(DeviceSelectorEvent::SelectedToken(dev.id())); + } + match resp { + Ok(GetAssertionResult::CTAP1(resp)) => { + let app_id = getassertion.rp.hash().as_ref().to_vec(); + let key_handle = getassertion.allow_list[0].id.clone(); + + callback.call(Ok(SignResult::CTAP1( + app_id, + key_handle, + resp, + dev.get_device_info(), + ))) + } + Ok(GetAssertionResult::CTAP2(assertion, client_data)) => { + callback.call(Ok(SignResult::CTAP2(assertion, client_data))) + } + // TODO(baloo): if key_handle is invalid for this device, it + // should reply something like: + // CTAP2_ERR_INVALID_CREDENTIAL + // have to check + Err(HIDError::DeviceNotSupported) | Err(HIDError::UnsupportedCommand) => {} + Err(HIDError::Command(CommandError::StatusCode( + StatusCode::ChannelBusy, + _, + ))) => {} + Err(e) => { + warn!("error happened: {}", e); + callback.call(Err(AuthenticatorError::HIDError(e))); + } + } + }, + ); + + self.transaction = Some(try_or!(transaction, move |e| cbc.call(Err(e)))); + } + + // This blocks. + pub fn cancel(&mut self) { + if let Some(mut transaction) = self.transaction.take() { + info!("Statemachine was cancelled. Cancelling transaction now."); + transaction.cancel(); + } + } + + pub fn reset( + &mut self, + timeout: u64, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::ResetResult>>, + ) { + // Abort any prior register/sign calls. + self.cancel(); + let cbc = callback.clone(); + + let transaction = Transaction::new( + timeout, + callback.clone(), + status, + move |info, selector, status, _alive| { + let reset = Reset {}; + let mut dev = match Self::init_and_select(info, &selector, true) { + None => { + return; + } + Some(dev) => dev, + }; + + info!("Device {:?} continues with the reset process", dev.id()); + debug!("------------------------------------------------------------------"); + debug!("{:?}", reset); + debug!("------------------------------------------------------------------"); + + let resp = dev.send_cbor(&reset); + if resp.is_ok() { + send_status( + &status, + crate::StatusUpdate::Success { + dev_info: dev.get_device_info(), + }, + ); + // The DeviceSelector could already be dead, but it might also wait + // for us to respond, in order to cancel all other tokens in case + // we skipped the "blinking"-action and went straight for the actual + // request. + let _ = selector.send(DeviceSelectorEvent::SelectedToken(dev.id())); + } + + match resp { + Ok(()) => callback.call(Ok(())), + Err(HIDError::DeviceNotSupported) | Err(HIDError::UnsupportedCommand) => {} + Err(HIDError::Command(CommandError::StatusCode( + StatusCode::ChannelBusy, + _, + ))) => {} + Err(e) => { + warn!("error happened: {}", e); + callback.call(Err(AuthenticatorError::HIDError(e))); + } + } + }, + ); + + self.transaction = Some(try_or!(transaction, move |e| cbc.call(Err(e)))); + } + + pub fn set_pin( + &mut self, + timeout: u64, + new_pin: Pin, + status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::ResetResult>>, + ) { + // Abort any prior register/sign calls. + self.cancel(); + + let cbc = callback.clone(); + + let transaction = Transaction::new( + timeout, + callback.clone(), + status, + move |info, selector, status, _alive| { + let mut dev = match Self::init_and_select(info, &selector, true) { + None => { + return; + } + Some(dev) => dev, + }; + + let (mut shared_secret, authinfo) = match dev.establish_shared_secret() { + Ok(s) => s, + Err(e) => { + callback.call(Err(AuthenticatorError::HIDError(e))); + return; + } + }; + + // With CTAP2.1 we will have an adjustable required length for PINs + if new_pin.as_bytes().len() < 4 { + callback.call(Err(AuthenticatorError::PinError(PinError::PinIsTooShort))); + return; + } + + if new_pin.as_bytes().len() > 64 { + callback.call(Err(AuthenticatorError::PinError(PinError::PinIsTooLong( + new_pin.as_bytes().len(), + )))); + return; + } + + // Check if a client-pin is already set, or if a new one should be created + let res = if authinfo.options.client_pin.unwrap_or_default() { + let mut res; + let mut error = PinError::PinRequired; + loop { + let current_pin = match Self::ask_user_for_pin(error, &status, &callback) { + Ok(pin) => pin, + _ => { + return; + } + }; + + res = ChangeExistingPin::new( + &authinfo, + &shared_secret, + ¤t_pin, + &new_pin, + ) + .map_err(HIDError::Command) + .and_then(|msg| dev.send_cbor(&msg)) + .map_err(AuthenticatorError::HIDError) + .map_err(|e| repackage_pin_errors(&mut dev, e)); + + if let Err(AuthenticatorError::PinError(e)) = res { + error = e; + // We need to re-establish the shared secret for the next round. + match dev.establish_shared_secret() { + Ok((s, _)) => { + shared_secret = s; + } + Err(e) => { + callback.call(Err(AuthenticatorError::HIDError(e))); + return; + } + }; + + continue; + } else { + break; + } + } + res + } else { + SetNewPin::new(&authinfo, &shared_secret, &new_pin) + .map_err(HIDError::Command) + .and_then(|msg| dev.send_cbor(&msg)) + .map_err(AuthenticatorError::HIDError) + }; + callback.call(res); + }, + ); + self.transaction = Some(try_or!(transaction, move |e| cbc.call(Err(e)))); + } +} diff --git a/third_party/rust/authenticator/src/status_update.rs b/third_party/rust/authenticator/src/status_update.rs new file mode 100644 index 0000000000..528ca3afb3 --- /dev/null +++ b/third_party/rust/authenticator/src/status_update.rs @@ -0,0 +1,101 @@ +use super::{u2ftypes, Pin, PinError}; +use serde::ser::{Serialize, SerializeStruct}; +use std::sync::mpsc::Sender; + +#[derive(Debug)] +pub enum StatusUpdate { + /// Device found + DeviceAvailable { dev_info: u2ftypes::U2FDeviceInfo }, + /// Device got removed + DeviceUnavailable { dev_info: u2ftypes::U2FDeviceInfo }, + /// We successfully finished the register or sign request + Success { dev_info: u2ftypes::U2FDeviceInfo }, + /// Sent if a PIN is needed (or was wrong), or some other kind of PIN-related + /// error occurred. The Sender is for sending back a PIN (if needed). + PinError(PinError, Sender<Pin>), + /// Sent, if multiple devices are found and the user has to select one + SelectDeviceNotice, + /// Sent, once a device was selected (either automatically or by user-interaction) + /// and the register or signing process continues with this device + DeviceSelected(u2ftypes::U2FDeviceInfo), +} + +impl Serialize for StatusUpdate { + fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + let mut map = serializer.serialize_struct("StatusUpdate", 1)?; + match &*self { + StatusUpdate::DeviceAvailable { dev_info } => { + map.serialize_field("DeviceAvailable", &dev_info)? + } + StatusUpdate::DeviceUnavailable { dev_info } => { + map.serialize_field("DeviceUnavailable", &dev_info)? + } + StatusUpdate::Success { dev_info } => map.serialize_field("Success", &dev_info)?, + StatusUpdate::PinError(e, _) => map.serialize_field("PinError", &e)?, + StatusUpdate::SelectDeviceNotice => map.serialize_field("SelectDeviceNotice", &())?, + StatusUpdate::DeviceSelected(dev_info) => { + map.serialize_field("DeviceSelected", &dev_info)? + } + } + map.end() + } +} + +pub(crate) fn send_status(status: &Sender<StatusUpdate>, msg: StatusUpdate) { + match status.send(msg) { + Ok(_) => {} + Err(e) => error!("Couldn't send status: {:?}", e), + }; +} + +#[cfg(test)] +pub mod tests { + use crate::consts::U2F_AUTHENTICATE; + + use super::*; + use crate::consts::Capability; + use serde_json::to_string; + use std::sync::mpsc::channel; + + #[test] + fn serialize_select() { + let st = StatusUpdate::SelectDeviceNotice; + let json = to_string(&st).expect("Failed to serialize"); + assert_eq!(&json, r#"{"SelectDeviceNotice":null}"#); + } + + #[test] + fn serialize_invalid_pin() { + let (tx, _rx) = channel(); + let st = StatusUpdate::PinError(PinError::InvalidPin(Some(3)), tx.clone()); + let json = to_string(&st).expect("Failed to serialize"); + assert_eq!(&json, r#"{"PinError":{"InvalidPin":3}}"#); + + let st = StatusUpdate::PinError(PinError::InvalidPin(None), tx); + let json = to_string(&st).expect("Failed to serialize"); + assert_eq!(&json, r#"{"PinError":{"InvalidPin":null}}"#); + } + + #[test] + fn serialize_success() { + let cap = Capability::WINK | Capability::CBOR; + let dev = u2ftypes::U2FDeviceInfo { + vendor_name: String::from("ABC").into_bytes(), + device_name: String::from("DEF").into_bytes(), + version_interface: 2, + version_major: 5, + version_minor: 4, + version_build: 3, + cap_flags: cap, + }; + let st = StatusUpdate::Success { dev_info: dev }; + let json = to_string(&st).expect("Failed to serialize"); + assert_eq!( + &json, + r#"{"Success":{"vendor_name":[65,66,67],"device_name":[68,69,70],"version_interface":2,"version_major":5,"version_minor":4,"version_build":3,"cap_flags":{"bits":5}}}"# + ); + } +} diff --git a/third_party/rust/authenticator/src/transport/device_selector.rs b/third_party/rust/authenticator/src/transport/device_selector.rs new file mode 100644 index 0000000000..05eea25a5b --- /dev/null +++ b/third_party/rust/authenticator/src/transport/device_selector.rs @@ -0,0 +1,520 @@ +use crate::send_status; +use crate::transport::hid::HIDDevice; +pub use crate::transport::platform::device::Device; +use crate::u2ftypes::U2FDevice; +use runloop::RunLoop; +use std::collections::{HashMap, HashSet}; +use std::sync::mpsc::{channel, RecvTimeoutError, Sender}; +use std::time::Duration; + +pub type DeviceID = <Device as HIDDevice>::Id; +pub type DeviceBuildParameters = <Device as HIDDevice>::BuildParameters; + +trait DeviceSelectorEventMarker {} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum BlinkResult { + DeviceSelected, + Cancelled, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum DeviceCommand { + Blink, + Continue, + Removed, +} + +#[derive(Debug)] +pub enum DeviceSelectorEvent { + Cancel, + Timeout, + DevicesAdded(Vec<DeviceID>), + DeviceRemoved(DeviceID), + NotAToken(DeviceID), + ImAToken((Device, Sender<DeviceCommand>)), + SelectedToken(DeviceID), +} + +pub struct DeviceSelector { + /// How to send a message to the event loop + sender: Sender<DeviceSelectorEvent>, + /// Thread of the event loop + runloop: RunLoop, +} + +impl DeviceSelector { + pub fn run(status: Sender<crate::StatusUpdate>) -> Self { + let (selector_send, selector_rec) = channel(); + // let new_device_callback = Arc::new(new_device_cb); + let runloop = RunLoop::new(move |alive| { + let mut blinking = false; + // Device was added, but we wait for its response, if it is a token or not + // We save both a write-only copy of the device (for cancellation) and it's thread + let mut waiting_for_response = HashSet::new(); + // All devices that responded with "ImAToken" + let mut tokens = HashMap::new(); + while alive() { + let d = Duration::from_secs(100); + let res = match selector_rec.recv_timeout(d) { + Err(RecvTimeoutError::Disconnected) => { + break; + } + Err(RecvTimeoutError::Timeout) => DeviceSelectorEvent::Timeout, + Ok(res) => res, + }; + + match res { + DeviceSelectorEvent::Timeout | DeviceSelectorEvent::Cancel => { + /* TODO */ + Self::cancel_all(tokens, None); + break; + } + DeviceSelectorEvent::SelectedToken(id) => { + if let Some(dev) = tokens.keys().find(|d| d.id() == id) { + send_status( + &status, + crate::StatusUpdate::DeviceSelected(dev.get_device_info()), + ); + } + Self::cancel_all(tokens, Some(&id)); + break; // We are done here. The selected device continues without us. + } + DeviceSelectorEvent::DevicesAdded(ids) => { + for id in ids { + debug!("Device added event: {:?}", id); + waiting_for_response.insert(id); + } + continue; + } + DeviceSelectorEvent::DeviceRemoved(id) => { + debug!("Device removed event: {:?}", id); + if !waiting_for_response.remove(&id) { + // Note: We _could_ check here if we had multiple tokens and are already blinking + // and the removal of this one leads to only one token left. So we could in theory + // stop blinking and select it right away. At the moment, I think this is a + // too surprising behavior and therefore, we let the remaining device keep on blinking + // since the user could add yet another device, instead of using the remaining one. + tokens.iter().for_each(|(dev, send)| { + if dev.id() == id { + let _ = send.send(DeviceCommand::Removed); + send_status( + &status, + crate::StatusUpdate::DeviceUnavailable { + dev_info: dev.get_device_info(), + }, + ); + } + }); + tokens.retain(|dev, _| dev.id() != id); + if tokens.is_empty() { + blinking = false; + continue; + } + } + // We are already blinking, so no need to run the code below this match + // that figures out if we should blink or not. In fact, currently, we do + // NOT want to run this code again, because if you have 2 blinking tokens + // and one got removed, we WANT the remaining one to continue blinking. + // This is a design choice, because I currently think it is the "less surprising" + // option to the user. + if blinking { + continue; + } + } + DeviceSelectorEvent::NotAToken(id) => { + debug!("Device not a token event: {:?}", id); + waiting_for_response.remove(&id); + } + DeviceSelectorEvent::ImAToken((dev, tx)) => { + send_status( + &status, + crate::StatusUpdate::DeviceAvailable { + dev_info: dev.get_device_info(), + }, + ); + let id = dev.id(); + let _ = waiting_for_response.remove(&id); + tokens.insert(dev, tx.clone()); + if blinking { + // We are already blinking, so this new device should blink too. + if tx.send(DeviceCommand::Blink).is_err() { + // Device thread died in the meantime (which shouldn't happen) + tokens.retain(|dev, _| dev.id() != id); + } + continue; + } + } + } + + // All known devices told us, whether they are tokens or not and we have at least one token + if waiting_for_response.is_empty() && !tokens.is_empty() { + if tokens.len() == 1 { + let (dev, tx) = tokens.drain().next().unwrap(); // We just checked that it can't be empty + if tx.send(DeviceCommand::Continue).is_err() { + // Device thread died in the meantime (which shouldn't happen). + // Tokens is empty, so we just start over again + continue; + } + send_status( + &status, + crate::StatusUpdate::DeviceSelected(dev.get_device_info()), + ); + Self::cancel_all(tokens, Some(&dev.id())); + break; // We are done here + } else { + blinking = true; + + tokens.iter().for_each(|(_dev, tx)| { + // A send operation can only fail if the receiving end of a channel is disconnected, implying that the data could never be received. + // We ignore errors here for now, but should probably remove the device in such a case (even though it theoretically can't happen) + let _ = tx.send(DeviceCommand::Blink); + }); + send_status(&status, crate::StatusUpdate::SelectDeviceNotice); + } + } + } + }); + Self { + runloop: runloop.unwrap(), // TODO + sender: selector_send, + } + } + + pub fn clone_sender(&self) -> Sender<DeviceSelectorEvent> { + self.sender.clone() + } + + fn cancel_all(tokens: HashMap<Device, Sender<DeviceCommand>>, exclude: Option<&DeviceID>) { + tokens + .into_keys() + .filter(|x| exclude.map_or(true, |y| y != &x.id())) + .for_each(|mut dev| dev.cancel().unwrap()); // TODO + } + + pub fn stop(&mut self) { + // We ignore a possible error here, since we don't really care + let _ = self.sender.send(DeviceSelectorEvent::Cancel); + self.runloop.cancel(); + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::{ + consts::Capability, + ctap2::commands::get_info::{AuthenticatorInfo, AuthenticatorOptions}, + u2ftypes::U2FDeviceInfo, + StatusUpdate, + }; + use std::sync::mpsc::Receiver; + + enum ExpectedUpdate { + DeviceAvailable, + DeviceUnavailable, + SelectDeviceNotice, + DeviceSelected, + } + + fn gen_info(id: String) -> U2FDeviceInfo { + U2FDeviceInfo { + vendor_name: String::from("ExampleVendor").into_bytes(), + device_name: id.into_bytes(), + version_interface: 1, + version_major: 3, + version_minor: 2, + version_build: 1, + cap_flags: Capability::WINK | Capability::CBOR | Capability::NMSG, + } + } + + fn make_device_simple_u2f(dev: &mut Device) { + dev.set_device_info(gen_info(dev.id())); + dev.create_channel(); + } + + fn make_device_with_pin(dev: &mut Device) { + dev.set_device_info(gen_info(dev.id())); + dev.create_channel(); + let info = AuthenticatorInfo { + options: AuthenticatorOptions { + client_pin: Some(true), + ..Default::default() + }, + ..Default::default() + }; + dev.set_authenticator_info(info.clone()); + } + + fn send_i_am_token(dev: &Device, selector: &DeviceSelector) { + selector + .sender + .send(DeviceSelectorEvent::ImAToken(( + dev.clone_device_as_write_only().unwrap(), + dev.sender.clone().unwrap(), + ))) + .unwrap(); + } + + fn send_no_token(dev: &Device, selector: &DeviceSelector) { + selector + .sender + .send(DeviceSelectorEvent::NotAToken(dev.id())) + .unwrap() + } + + fn remove_device(dev: &Device, selector: &DeviceSelector) { + selector + .sender + .send(DeviceSelectorEvent::DeviceRemoved(dev.id())) + .unwrap(); + assert_eq!( + dev.receiver.as_ref().unwrap().recv().unwrap(), + DeviceCommand::Removed + ); + } + + fn recv_status(dev: &Device, status_rx: &Receiver<StatusUpdate>, expected: ExpectedUpdate) { + let res = status_rx.recv().unwrap(); + // Marking it with _, as cargo warns about "unused exp", as it doesn't view the assert below as a usage + let _exp = match expected { + ExpectedUpdate::DeviceAvailable => StatusUpdate::DeviceUnavailable { + dev_info: gen_info(dev.id()), + }, + ExpectedUpdate::DeviceUnavailable => StatusUpdate::DeviceUnavailable { + dev_info: gen_info(dev.id()), + }, + ExpectedUpdate::DeviceSelected => StatusUpdate::DeviceSelected(gen_info(dev.id())), + ExpectedUpdate::SelectDeviceNotice => StatusUpdate::SelectDeviceNotice, + }; + assert!(matches!(res, _exp)); + } + + fn add_devices<'a, T>(iter: T, selector: &DeviceSelector) + where + T: Iterator<Item = &'a Device>, + { + selector + .sender + .send(DeviceSelectorEvent::DevicesAdded( + iter.map(|f| f.id()).collect(), + )) + .unwrap(); + } + + #[test] + fn test_device_selector_one_token_no_late_adds() { + let mut devices = vec![ + Device::new("device selector 1").unwrap(), + Device::new("device selector 2").unwrap(), + Device::new("device selector 3").unwrap(), + Device::new("device selector 4").unwrap(), + ]; + + // Make those actual tokens. The rest is interpreted as non-u2f-devices + make_device_with_pin(&mut devices[2]); + let (status_tx, status_rx) = channel(); + let selector = DeviceSelector::run(status_tx); + + // Adding all + add_devices(devices.iter(), &selector); + devices.iter_mut().for_each(|d| { + if !d.is_u2f() { + send_no_token(&d, &selector); + } + }); + + send_i_am_token(&devices[2], &selector); + + recv_status(&devices[2], &status_rx, ExpectedUpdate::DeviceAvailable); + assert_eq!( + devices[2].receiver.as_ref().unwrap().recv().unwrap(), + DeviceCommand::Continue + ); + recv_status(&devices[2], &status_rx, ExpectedUpdate::DeviceSelected); + } + + #[test] + fn test_device_selector_all_pins_with_late_add() { + let mut devices = vec![ + Device::new("device selector 1").unwrap(), + Device::new("device selector 2").unwrap(), + Device::new("device selector 3").unwrap(), + Device::new("device selector 4").unwrap(), + Device::new("device selector 5").unwrap(), + Device::new("device selector 6").unwrap(), + ]; + + // Make those actual tokens. The rest is interpreted as non-u2f-devices + make_device_with_pin(&mut devices[2]); + make_device_with_pin(&mut devices[4]); + make_device_with_pin(&mut devices[5]); + + let (status_tx, status_rx) = channel(); + let selector = DeviceSelector::run(status_tx); + + // Adding all, except the last one (we simulate that this one is not yet plugged in) + add_devices(devices.iter().take(5), &selector); + + // Interleave tokens and non-tokens + send_i_am_token(&devices[2], &selector); + recv_status(&devices[2], &status_rx, ExpectedUpdate::DeviceAvailable); + + devices.iter_mut().for_each(|d| { + if !d.is_u2f() { + send_no_token(d, &selector); + } + }); + + send_i_am_token(&devices[4], &selector); + recv_status(&devices[4], &status_rx, ExpectedUpdate::DeviceAvailable); + + // We added 2 devices that are tokens. They should get the blink-command now + assert_eq!( + devices[2].receiver.as_ref().unwrap().recv().unwrap(), + DeviceCommand::Blink + ); + assert_eq!( + devices[4].receiver.as_ref().unwrap().recv().unwrap(), + DeviceCommand::Blink + ); + recv_status(&devices[2], &status_rx, ExpectedUpdate::SelectDeviceNotice); + + // Plug in late device + send_i_am_token(&devices[5], &selector); + recv_status(&devices[5], &status_rx, ExpectedUpdate::DeviceAvailable); + assert_eq!( + devices[5].receiver.as_ref().unwrap().recv().unwrap(), + DeviceCommand::Blink + ); + } + + #[test] + fn test_device_selector_no_pins_late_mixed_adds() { + // Multiple tokes, none of them support a PIN + let mut devices = vec![ + Device::new("device selector 1").unwrap(), + Device::new("device selector 2").unwrap(), + Device::new("device selector 3").unwrap(), + Device::new("device selector 4").unwrap(), + Device::new("device selector 5").unwrap(), + Device::new("device selector 6").unwrap(), + Device::new("device selector 7").unwrap(), + ]; + + // Make those actual tokens. The rest is interpreted as non-u2f-devices + make_device_simple_u2f(&mut devices[2]); + make_device_simple_u2f(&mut devices[4]); + make_device_simple_u2f(&mut devices[5]); + + let (status_tx, status_rx) = channel(); + let selector = DeviceSelector::run(status_tx); + + // Adding all, except the last one (we simulate that this one is not yet plugged in) + add_devices(devices.iter().take(5), &selector); + + // Interleave tokens and non-tokens + send_i_am_token(&devices[2], &selector); + recv_status(&devices[2], &status_rx, ExpectedUpdate::DeviceAvailable); + + devices.iter_mut().for_each(|d| { + if !d.is_u2f() { + send_no_token(d, &selector); + } + }); + + send_i_am_token(&devices[4], &selector); + recv_status(&devices[4], &status_rx, ExpectedUpdate::DeviceAvailable); + + // We added 2 devices that are tokens. They should get the blink-command now + assert_eq!( + devices[2].receiver.as_ref().unwrap().recv().unwrap(), + DeviceCommand::Blink + ); + assert_eq!( + devices[4].receiver.as_ref().unwrap().recv().unwrap(), + DeviceCommand::Blink + ); + recv_status(&devices[2], &status_rx, ExpectedUpdate::SelectDeviceNotice); + + // Plug in late device + send_i_am_token(&devices[5], &selector); + recv_status(&devices[5], &status_rx, ExpectedUpdate::DeviceAvailable); + assert_eq!( + devices[5].receiver.as_ref().unwrap().recv().unwrap(), + DeviceCommand::Blink + ); + // Remove device again + remove_device(&devices[5], &selector); + recv_status(&devices[5], &status_rx, ExpectedUpdate::DeviceUnavailable); + + // Now we add a token that has a PIN, it should not get "Continue" but "Blink" + make_device_with_pin(&mut devices[6]); + send_i_am_token(&devices[6], &selector); + recv_status(&devices[6], &status_rx, ExpectedUpdate::DeviceAvailable); + assert_eq!( + devices[6].receiver.as_ref().unwrap().recv().unwrap(), + DeviceCommand::Blink + ); + } + + #[test] + fn test_device_selector_mixed_pins_remove_all() { + // Multiple tokes, none of them support a PIN, so we should get Continue-commands + // for all of them + let mut devices = vec![ + Device::new("device selector 1").unwrap(), + Device::new("device selector 2").unwrap(), + Device::new("device selector 3").unwrap(), + Device::new("device selector 4").unwrap(), + Device::new("device selector 5").unwrap(), + Device::new("device selector 6").unwrap(), + ]; + + // Make those actual tokens. The rest is interpreted as non-u2f-devices + make_device_with_pin(&mut devices[2]); + make_device_with_pin(&mut devices[4]); + make_device_with_pin(&mut devices[5]); + + let (status_tx, status_rx) = channel(); + let selector = DeviceSelector::run(status_tx); + + // Adding all, except the last one (we simulate that this one is not yet plugged in) + add_devices(devices.iter(), &selector); + + devices.iter_mut().for_each(|d| { + if d.is_u2f() { + send_i_am_token(d, &selector); + } else { + send_no_token(d, &selector); + } + }); + + for idx in [2, 4, 5] { + recv_status(&devices[idx], &status_rx, ExpectedUpdate::DeviceAvailable); + assert_eq!( + devices[idx].receiver.as_ref().unwrap().recv().unwrap(), + DeviceCommand::Blink + ); + } + recv_status(&devices[2], &status_rx, ExpectedUpdate::SelectDeviceNotice); + + // Remove all tokens + for idx in [2, 4, 5] { + remove_device(&devices[idx], &selector); + recv_status(&devices[idx], &status_rx, ExpectedUpdate::DeviceUnavailable); + } + + // Adding one again + send_i_am_token(&devices[4], &selector); + recv_status(&devices[4], &status_rx, ExpectedUpdate::DeviceAvailable); + + // This should now get a "Continue" instead of "Blinking", because it's the only device + assert_eq!( + devices[4].receiver.as_ref().unwrap().recv().unwrap(), + DeviceCommand::Continue + ); + recv_status(&devices[4], &status_rx, ExpectedUpdate::DeviceSelected); + } +} diff --git a/third_party/rust/authenticator/src/transport/errors.rs b/third_party/rust/authenticator/src/transport/errors.rs new file mode 100644 index 0000000000..463b7cb5cb --- /dev/null +++ b/third_party/rust/authenticator/src/transport/errors.rs @@ -0,0 +1,98 @@ +use crate::consts::{SW_CONDITIONS_NOT_SATISFIED, SW_NO_ERROR, SW_WRONG_DATA, SW_WRONG_LENGTH}; +use crate::ctap2::commands::CommandError; +use std::fmt; +use std::io; +use std::path; + +#[allow(unused)] +#[derive(Debug, PartialEq, Eq)] +pub enum ApduErrorStatus { + ConditionsNotSatisfied, + WrongData, + WrongLength, + Unknown([u8; 2]), +} + +impl ApduErrorStatus { + pub fn from(status: [u8; 2]) -> Result<(), ApduErrorStatus> { + match status { + s if s == SW_NO_ERROR => Ok(()), + s if s == SW_CONDITIONS_NOT_SATISFIED => Err(ApduErrorStatus::ConditionsNotSatisfied), + s if s == SW_WRONG_DATA => Err(ApduErrorStatus::WrongData), + s if s == SW_WRONG_LENGTH => Err(ApduErrorStatus::WrongLength), + other => Err(ApduErrorStatus::Unknown(other)), + } + } +} + +impl fmt::Display for ApduErrorStatus { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + ApduErrorStatus::ConditionsNotSatisfied => write!(f, "Apdu: condition not satisfied"), + ApduErrorStatus::WrongData => write!(f, "Apdu: wrong data"), + ApduErrorStatus::WrongLength => write!(f, "Apdu: wrong length"), + ApduErrorStatus::Unknown(ref u) => write!(f, "Apdu: unknown error: {:?}", u), + } + } +} + +#[allow(unused)] +#[derive(Debug)] +pub enum HIDError { + /// Transport replied with a status not expected + DeviceError, + UnexpectedInitReplyLen, + NonceMismatch, + DeviceNotInitialized, + DeviceNotSupported, + UnsupportedCommand, + UnexpectedVersion, + IO(Option<path::PathBuf>, io::Error), + UnexpectedCmd(u8), + Command(CommandError), + ApduStatus(ApduErrorStatus), +} + +impl From<io::Error> for HIDError { + fn from(e: io::Error) -> HIDError { + HIDError::IO(None, e) + } +} + +impl From<CommandError> for HIDError { + fn from(e: CommandError) -> HIDError { + HIDError::Command(e) + } +} + +impl From<ApduErrorStatus> for HIDError { + fn from(e: ApduErrorStatus) -> HIDError { + HIDError::ApduStatus(e) + } +} + +impl fmt::Display for HIDError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + HIDError::UnexpectedInitReplyLen => { + write!(f, "Error: Unexpected reply len when initilizaling") + } + HIDError::NonceMismatch => write!(f, "Error: Nonce mismatch"), + HIDError::DeviceError => write!(f, "Error: device returned error"), + HIDError::DeviceNotInitialized => write!(f, "Error: using not initiliazed device"), + HIDError::DeviceNotSupported => { + write!(f, "Error: requested operation is not available on device") + } + HIDError::UnexpectedVersion => write!(f, "Error: Unexpected protocol version"), + HIDError::UnsupportedCommand => { + write!(f, "Error: command is not supported on this device") + } + HIDError::IO(ref p, ref e) => write!(f, "Error: Ioerror({:?}): {}", p, e), + HIDError::Command(ref e) => write!(f, "Error: Error issuing command: {}", e), + HIDError::UnexpectedCmd(s) => write!(f, "Error: Unexpected status: {}", s), + HIDError::ApduStatus(ref status) => { + write!(f, "Error: Unexpected apdu status: {:?}", status) + } + } + } +} diff --git a/third_party/rust/authenticator/src/transport/freebsd/device.rs b/third_party/rust/authenticator/src/transport/freebsd/device.rs new file mode 100644 index 0000000000..a73b48bab1 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/freebsd/device.rs @@ -0,0 +1,228 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate libc; + +use crate::consts::{CID_BROADCAST, MAX_HID_RPT_SIZE}; +use crate::transport::hid::HIDDevice; +use crate::transport::platform::uhid; +use crate::transport::{AuthenticatorInfo, ECDHSecret, FidoDevice, HIDError}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use crate::util::from_unix_result; +use crate::util::io_err; +use std::ffi::{CString, OsString}; +use std::hash::{Hash, Hasher}; +use std::io::{self, Read, Write}; +use std::mem; +use std::os::unix::prelude::*; + +#[derive(Debug)] +pub struct Device { + path: OsString, + fd: libc::c_int, + cid: [u8; 4], + dev_info: Option<U2FDeviceInfo>, + secret: Option<ECDHSecret>, + authenticator_info: Option<AuthenticatorInfo>, +} + +impl Device { + fn ping(&mut self) -> io::Result<()> { + for i in 0..10 { + let mut buf = vec![0u8; 1 + MAX_HID_RPT_SIZE]; + + buf[0] = 0; // report number + buf[1] = 0xff; // CID_BROADCAST + buf[2] = 0xff; + buf[3] = 0xff; + buf[4] = 0xff; + buf[5] = 0x81; // ping + buf[6] = 0; + buf[7] = 1; // one byte + + self.write(&buf[..])?; + + // Wait for response + let mut pfd: libc::pollfd = unsafe { mem::zeroed() }; + pfd.fd = self.fd; + pfd.events = libc::POLLIN; + let nfds = unsafe { libc::poll(&mut pfd, 1, 100) }; + if nfds == -1 { + return Err(io::Error::last_os_error()); + } + if nfds == 0 { + debug!("device timeout {}", i); + continue; + } + + // Read response + self.read(&mut buf[..])?; + + return Ok(()); + } + + Err(io_err("no response from device")) + } +} + +impl Drop for Device { + fn drop(&mut self) { + // Close the fd, ignore any errors. + let _ = unsafe { libc::close(self.fd) }; + } +} + +impl PartialEq for Device { + fn eq(&self, other: &Device) -> bool { + self.path == other.path + } +} + +impl Eq for Device {} + +impl Hash for Device { + fn hash<H: Hasher>(&self, state: &mut H) { + self.path.hash(state); + } +} + +impl Read for Device { + fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { + let bufp = buf.as_mut_ptr() as *mut libc::c_void; + let rv = unsafe { libc::read(self.fd, bufp, buf.len()) }; + from_unix_result(rv as usize) + } +} + +impl Write for Device { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + let report_id = buf[0] as i64; + // Skip report number when not using numbered reports. + let start = if report_id == 0x0 { 1 } else { 0 }; + let data = &buf[start..]; + + let data_ptr = data.as_ptr() as *const libc::c_void; + let rv = unsafe { libc::write(self.fd, data_ptr, data.len()) }; + from_unix_result(rv as usize + 1) + } + + // USB HID writes don't buffer, so this will be a nop. + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl U2FDevice for Device { + fn get_cid(&self) -> &[u8; 4] { + &self.cid + } + + fn set_cid(&mut self, cid: [u8; 4]) { + self.cid = cid; + } + + fn in_rpt_size(&self) -> usize { + MAX_HID_RPT_SIZE + } + + fn out_rpt_size(&self) -> usize { + MAX_HID_RPT_SIZE + } + + fn get_property(&self, _prop_name: &str) -> io::Result<String> { + Err(io::Error::new(io::ErrorKind::Other, "Not implemented")) + } + + fn get_device_info(&self) -> U2FDeviceInfo { + // unwrap is okay, as dev_info must have already been set, else + // a programmer error + self.dev_info.clone().unwrap() + } + + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { + self.dev_info = Some(dev_info); + } +} + +impl HIDDevice for Device { + type BuildParameters = OsString; + type Id = OsString; + + fn new(path: OsString) -> Result<Self, (HIDError, Self::Id)> { + let cstr = + CString::new(path.as_bytes()).map_err(|_| (HIDError::DeviceError, path.clone()))?; + let fd = unsafe { libc::open(cstr.as_ptr(), libc::O_RDWR) }; + let fd = from_unix_result(fd).map_err(|e| (e.into(), path.clone()))?; + let mut res = Self { + path, + fd, + cid: CID_BROADCAST, + dev_info: None, + secret: None, + authenticator_info: None, + }; + if res.is_u2f() { + info!("new device {:?}", res.path); + Ok(res) + } else { + Err((HIDError::DeviceNotSupported, res.path.clone())) + } + } + + fn initialized(&self) -> bool { + // During successful init, the broadcast channel id gets repplaced by an actual one + self.cid != CID_BROADCAST + } + + fn id(&self) -> Self::Id { + self.path.clone() + } + + fn is_u2f(&mut self) -> bool { + if !uhid::is_u2f_device(self.fd) { + return false; + } + if let Err(_) = self.ping() { + return false; + } + true + } + + fn get_shared_secret(&self) -> Option<&ECDHSecret> { + self.secret.as_ref() + } + + fn set_shared_secret(&mut self, secret: ECDHSecret) { + self.secret = Some(secret); + } + + fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> { + self.authenticator_info.as_ref() + } + + fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) { + self.authenticator_info = Some(authenticator_info); + } + + /// This is used for cancellation of blocking read()-requests. + /// With this, we can clone the Device, pass it to another thread and call "cancel()" on that. + fn clone_device_as_write_only(&self) -> Result<Self, HIDError> { + // Try to open the device. + // This can't really error out as we already did this conversion + let cstr = CString::new(self.path.as_bytes()).map_err(|_| (HIDError::DeviceError))?; + let fd = unsafe { libc::open(cstr.as_ptr(), libc::O_WRONLY) }; + let fd = + from_unix_result(fd).map_err(|e| (HIDError::IO(Some(self.path.clone().into()), e)))?; + Ok(Self { + path: self.path.clone(), + fd, + cid: self.cid, + dev_info: self.dev_info.clone(), + secret: self.secret.clone(), + authenticator_info: self.authenticator_info.clone(), + }) + } +} + +impl FidoDevice for Device {} diff --git a/third_party/rust/authenticator/src/transport/freebsd/mod.rs b/third_party/rust/authenticator/src/transport/freebsd/mod.rs new file mode 100644 index 0000000000..7ed5727157 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/freebsd/mod.rs @@ -0,0 +1,9 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +pub mod device; +pub mod transaction; + +mod monitor; +mod uhid; diff --git a/third_party/rust/authenticator/src/transport/freebsd/monitor.rs b/third_party/rust/authenticator/src/transport/freebsd/monitor.rs new file mode 100644 index 0000000000..02b2e08110 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/freebsd/monitor.rs @@ -0,0 +1,165 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::transport::device_selector::DeviceSelectorEvent; +use devd_rs; +use runloop::RunLoop; +use std::collections::HashMap; +use std::error::Error; +use std::ffi::OsString; +use std::sync::{mpsc::Sender, Arc}; +use std::thread; +use std::time::Duration; +use std::{fs, io}; + +const POLL_TIMEOUT: usize = 100; + +pub enum Event { + Add(OsString), + Remove(OsString), +} + +impl Event { + fn from_devd(event: devd_rs::Event) -> Option<Self> { + match event { + devd_rs::Event::Attach { + ref dev, + parent: _, + location: _, + } if dev.starts_with("uhid") => Some(Event::Add(("/dev/".to_owned() + dev).into())), + devd_rs::Event::Detach { + ref dev, + parent: _, + location: _, + } if dev.starts_with("uhid") => Some(Event::Remove(("/dev/".to_owned() + dev).into())), + _ => None, + } + } +} + +fn convert_error(e: devd_rs::Error) -> io::Error { + e.into() +} + +pub struct Monitor<F> +where + F: Fn(OsString, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool) + + Sync, +{ + runloops: HashMap<OsString, RunLoop>, + new_device_cb: Arc<F>, + selector_sender: Sender<DeviceSelectorEvent>, + status_sender: Sender<crate::StatusUpdate>, +} + +impl<F> Monitor<F> +where + F: Fn(OsString, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool) + + Send + + Sync + + 'static, +{ + pub fn new( + new_device_cb: F, + selector_sender: Sender<DeviceSelectorEvent>, + status_sender: Sender<crate::StatusUpdate>, + ) -> Self { + Self { + runloops: HashMap::new(), + new_device_cb: Arc::new(new_device_cb), + selector_sender, + status_sender, + } + } + + pub fn run(&mut self, alive: &dyn Fn() -> bool) -> Result<(), Box<dyn Error>> { + let mut ctx = devd_rs::Context::new().map_err(convert_error)?; + + let mut initial_devs = Vec::new(); + // Iterate all existing devices. + for dev in fs::read_dir("/dev")? { + if let Ok(dev) = dev { + let filename_ = dev.file_name(); + let filename = filename_.to_str().unwrap_or(""); + if filename.starts_with("uhid") { + let path = OsString::from("/dev/".to_owned() + filename); + initial_devs.push(path.clone()); + self.add_device(path); + } + } + } + let _ = self + .selector_sender + .send(DeviceSelectorEvent::DevicesAdded(initial_devs)); + + // Loop until we're stopped by the controlling thread, or fail. + while alive() { + // Wait for new events, break on failure. + match ctx.wait_for_event(POLL_TIMEOUT) { + Err(devd_rs::Error::Timeout) => (), + Err(e) => return Err(convert_error(e).into()), + Ok(event) => { + if let Some(event) = Event::from_devd(event) { + self.process_event(event); + } + } + } + } + + // Remove all tracked devices. + self.remove_all_devices(); + + Ok(()) + } + + fn process_event(&mut self, event: Event) { + match event { + Event::Add(path) => { + let _ = self + .selector_sender + .send(DeviceSelectorEvent::DevicesAdded(vec![path.clone()])); + self.add_device(path); + } + Event::Remove(path) => { + self.remove_device(path); + } + } + } + + fn add_device(&mut self, path: OsString) { + let f = self.new_device_cb.clone(); + let selector_sender = self.selector_sender.clone(); + let status_sender = self.status_sender.clone(); + let key = path.clone(); + debug!("Adding device {}", key.to_string_lossy()); + + let runloop = RunLoop::new(move |alive| { + if alive() { + f(path, selector_sender, status_sender, alive); + } + }); + + if let Ok(runloop) = runloop { + self.runloops.insert(key, runloop); + } + } + + fn remove_device(&mut self, path: OsString) { + let _ = self + .selector_sender + .send(DeviceSelectorEvent::DeviceRemoved(path.clone())); + + debug!("Removing device {}", path.to_string_lossy()); + if let Some(runloop) = self.runloops.remove(&path) { + runloop.cancel(); + } + } + + fn remove_all_devices(&mut self) { + while !self.runloops.is_empty() { + let path = self.runloops.keys().next().unwrap().clone(); + self.remove_device(path); + } + } +} diff --git a/third_party/rust/authenticator/src/transport/freebsd/transaction.rs b/third_party/rust/authenticator/src/transport/freebsd/transaction.rs new file mode 100644 index 0000000000..35baa8f3c4 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/freebsd/transaction.rs @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::errors; +use crate::statecallback::StateCallback; +use crate::transport::device_selector::{ + DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent, +}; +use crate::transport::platform::monitor::Monitor; +use runloop::RunLoop; +use std::sync::mpsc::Sender; + +pub struct Transaction { + // Handle to the thread loop. + thread: RunLoop, + device_selector: DeviceSelector, +} + +impl Transaction { + pub fn new<F, T>( + timeout: u64, + callback: StateCallback<crate::Result<T>>, + status: Sender<crate::StatusUpdate>, + new_device_cb: F, + ) -> crate::Result<Self> + where + F: Fn( + DeviceBuildParameters, + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Sync + + Send + + 'static, + T: 'static, + { + let status_sender = status.clone(); + let device_selector = DeviceSelector::run(status); + let selector_sender = device_selector.clone_sender(); + let thread = RunLoop::new_with_timeout( + move |alive| { + // Create a new device monitor. + let mut monitor = Monitor::new(new_device_cb, selector_sender, status_sender); + + // Start polling for new devices. + try_or!(monitor.run(alive), |_| callback + .call(Err(errors::AuthenticatorError::Platform))); + + // Send an error, if the callback wasn't called already. + callback.call(Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::NotAllowed, + ))); + }, + timeout, + ) + .map_err(|_| errors::AuthenticatorError::Platform)?; + + Ok(Self { + thread, + device_selector, + }) + } + + pub fn cancel(&mut self) { + info!("Transaction was cancelled."); + self.device_selector.stop(); + self.thread.cancel(); + } +} diff --git a/third_party/rust/authenticator/src/transport/freebsd/uhid.rs b/third_party/rust/authenticator/src/transport/freebsd/uhid.rs new file mode 100644 index 0000000000..0fc2131cb1 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/freebsd/uhid.rs @@ -0,0 +1,92 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate libc; + +use std::io; +use std::os::unix::io::RawFd; +use std::ptr; + +use crate::transport::hidproto::*; +use crate::util::from_unix_result; + +#[allow(non_camel_case_types)] +#[repr(C)] +#[derive(Debug)] +pub struct GenDescriptor { + ugd_data: *mut u8, + ugd_lang_id: u16, + ugd_maxlen: u16, + ugd_actlen: u16, + ugd_offset: u16, + ugd_config_index: u8, + ugd_string_index: u8, + ugd_iface_index: u8, + ugd_altif_index: u8, + ugd_endpt_index: u8, + ugd_report_index: u8, + reserved: [u8; 16], +} + +impl Default for GenDescriptor { + fn default() -> GenDescriptor { + GenDescriptor { + ugd_data: ptr::null_mut(), + ugd_lang_id: 0, + ugd_maxlen: 65535, + ugd_actlen: 0, + ugd_offset: 0, + ugd_config_index: 0, + ugd_string_index: 0, + ugd_iface_index: 0, + ugd_altif_index: 0, + ugd_endpt_index: 0, + ugd_report_index: 0, + reserved: [0; 16], + } + } +} + +const IOWR: u32 = 0x40000000 | 0x80000000; + +const IOCPARM_SHIFT: u32 = 13; +const IOCPARM_MASK: u32 = (1 << IOCPARM_SHIFT) - 1; + +const TYPESHIFT: u32 = 8; +const SIZESHIFT: u32 = 16; + +macro_rules! ioctl { + ($dir:expr, $name:ident, $ioty:expr, $nr:expr, $size:expr; $ty:ty) => { + pub unsafe fn $name(fd: libc::c_int, val: *mut $ty) -> io::Result<libc::c_int> { + let ioc = ($dir as u32) + | (($size as u32 & IOCPARM_MASK) << SIZESHIFT) + | (($ioty as u32) << TYPESHIFT) + | ($nr as u32); + from_unix_result(libc::ioctl(fd, ioc as libc::c_ulong, val)) + } + }; +} + +// https://github.com/freebsd/freebsd/blob/master/sys/dev/usb/usb_ioctl.h +ioctl!(IOWR, usb_get_report_desc, b'U', 21, 32; /*struct*/ GenDescriptor); + +fn read_report_descriptor(fd: RawFd) -> io::Result<ReportDescriptor> { + let mut desc = GenDescriptor::default(); + let _ = unsafe { usb_get_report_desc(fd, &mut desc)? }; + desc.ugd_maxlen = desc.ugd_actlen; + let mut value = Vec::with_capacity(desc.ugd_actlen as usize); + unsafe { + value.set_len(desc.ugd_actlen as usize); + } + desc.ugd_data = value.as_mut_ptr(); + let _ = unsafe { usb_get_report_desc(fd, &mut desc)? }; + Ok(ReportDescriptor { value }) +} + +pub fn is_u2f_device(fd: RawFd) -> bool { + match read_report_descriptor(fd) { + Ok(desc) => has_fido_usage(desc), + Err(_) => false, // Upon failure, just say it's not a U2F device. + } +} diff --git a/third_party/rust/authenticator/src/transport/hid.rs b/third_party/rust/authenticator/src/transport/hid.rs new file mode 100644 index 0000000000..90f40526ae --- /dev/null +++ b/third_party/rust/authenticator/src/transport/hid.rs @@ -0,0 +1,136 @@ +use crate::consts::{HIDCmd, CID_BROADCAST}; +use crate::crypto::ECDHSecret; +use crate::ctap2::commands::get_info::AuthenticatorInfo; +use crate::transport::{errors::HIDError, Nonce}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo, U2FHIDCont, U2FHIDInit, U2FHIDInitResp}; +use rand::{thread_rng, RngCore}; +use std::cmp::Eq; +use std::fmt; +use std::hash::Hash; +use std::io; + +pub trait HIDDevice +where + Self: io::Read, + Self: io::Write, + Self: U2FDevice, + Self: Sized, + Self: fmt::Debug, +{ + type BuildParameters: Sized; + type Id: fmt::Debug + PartialEq + Eq + Hash + Sized; + + // Open device, verify that it is indeed a CTAP device and potentially read initial values + fn new(parameters: Self::BuildParameters) -> Result<Self, (HIDError, Self::Id)>; + fn id(&self) -> Self::Id; + fn initialized(&self) -> bool; + // Check if the device is actually a token + fn is_u2f(&mut self) -> bool; + fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo>; + fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo); + fn set_shared_secret(&mut self, secret: ECDHSecret); + fn get_shared_secret(&self) -> Option<&ECDHSecret>; + fn clone_device_as_write_only(&self) -> Result<Self, HIDError>; + + // Initialize on a protocol-level + fn initialize(&mut self, noncecmd: Nonce) -> Result<(), HIDError> { + if self.initialized() { + return Ok(()); + } + + let nonce = match noncecmd { + Nonce::Use(x) => x, + Nonce::CreateRandom => { + let mut nonce = [0u8; 8]; + thread_rng().fill_bytes(&mut nonce); + nonce + } + }; + + // Send Init to broadcast address to create a new channel + self.set_cid(CID_BROADCAST); + let (cmd, raw) = self.sendrecv(HIDCmd::Init, &nonce)?; + if cmd != HIDCmd::Init { + return Err(HIDError::DeviceError); + } + + let rsp = U2FHIDInitResp::read(&raw, &nonce)?; + // Get the new Channel ID + self.set_cid(rsp.cid); + + let vendor = self + .get_property("Manufacturer") + .unwrap_or_else(|_| String::from("Unknown Vendor")); + let product = self + .get_property("Product") + .unwrap_or_else(|_| String::from("Unknown Device")); + + self.set_device_info(U2FDeviceInfo { + vendor_name: vendor.as_bytes().to_vec(), + device_name: product.as_bytes().to_vec(), + version_interface: rsp.version_interface, + version_major: rsp.version_major, + version_minor: rsp.version_minor, + version_build: rsp.version_build, + cap_flags: rsp.cap_flags, + }); + + // A CTAPHID host SHALL accept a response size that is longer than the + // anticipated size to allow for future extensions of the protocol, yet + // maintaining backwards compatibility. Future versions will maintain + // the response structure of the current version, but additional fields + // may be added. + + Ok(()) + } + + fn sendrecv(&mut self, cmd: HIDCmd, send: &[u8]) -> io::Result<(HIDCmd, Vec<u8>)> { + let cmd: u8 = cmd.into(); + self.u2f_write(cmd, send)?; + loop { + let (cmd, data) = self.u2f_read()?; + if cmd != HIDCmd::Keepalive { + break Ok((cmd, data)); + } + } + } + + fn u2f_write(&mut self, cmd: u8, send: &[u8]) -> io::Result<()> { + let mut count = U2FHIDInit::write(self, cmd, send)?; + + // Send continuation packets. + let mut sequence = 0u8; + while count < send.len() { + count += U2FHIDCont::write(self, sequence, &send[count..])?; + sequence += 1; + } + + Ok(()) + } + + fn u2f_read(&mut self) -> io::Result<(HIDCmd, Vec<u8>)> { + // Now we read. This happens in 2 chunks: The initial packet, which has + // the size we expect overall, then continuation packets, which will + // fill in data until we have everything. + let (cmd, data) = { + let (cmd, mut data) = U2FHIDInit::read(self)?; + + trace!("init frame data read: {:04X?}", &data); + let mut sequence = 0u8; + while data.len() < data.capacity() { + let max = data.capacity() - data.len(); + data.extend_from_slice(&U2FHIDCont::read(self, sequence, max)?); + sequence += 1; + } + (cmd, data) + }; + trace!("u2f_read({:?}) cmd={:?}: {:04X?}", self.id(), cmd, &data); + Ok((cmd, data)) + } + + fn cancel(&mut self) -> Result<(), HIDError> { + let cancel: u8 = HIDCmd::Cancel.into(); + self.u2f_write(cancel, &[])?; + Ok(()) + } +} diff --git a/third_party/rust/authenticator/src/transport/hidproto.rs b/third_party/rust/authenticator/src/transport/hidproto.rs new file mode 100644 index 0000000000..5679e6e5d7 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/hidproto.rs @@ -0,0 +1,253 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Shared code for platforms that use raw HID access (Linux, FreeBSD, etc.) + +#![cfg_attr( + feature = "cargo-clippy", + allow(clippy::cast_lossless, clippy::needless_lifetimes) +)] + +use std::io; +use std::mem; + +use crate::consts::{FIDO_USAGE_PAGE, FIDO_USAGE_U2FHID, INIT_HEADER_SIZE, MAX_HID_RPT_SIZE}; + +// The 4 MSBs (the tag) are set when it's a long item. +const HID_MASK_LONG_ITEM_TAG: u8 = 0b1111_0000; +// The 2 LSBs denote the size of a short item. +const HID_MASK_SHORT_ITEM_SIZE: u8 = 0b0000_0011; +// The 6 MSBs denote the tag (4) and type (2). +const HID_MASK_ITEM_TAGTYPE: u8 = 0b1111_1100; +// tag=0000, type=10 (local) +const HID_ITEM_TAGTYPE_USAGE: u8 = 0b0000_1000; +// tag=0000, type=01 (global) +const HID_ITEM_TAGTYPE_USAGE_PAGE: u8 = 0b0000_0100; +// tag=1000, type=00 (main) +const HID_ITEM_TAGTYPE_INPUT: u8 = 0b1000_0000; +// tag=1001, type=00 (main) +const HID_ITEM_TAGTYPE_OUTPUT: u8 = 0b1001_0000; +// tag=1001, type=01 (global) +const HID_ITEM_TAGTYPE_REPORT_COUNT: u8 = 0b1001_0100; + +pub struct ReportDescriptor { + pub value: Vec<u8>, +} + +impl ReportDescriptor { + fn iter(self) -> ReportDescriptorIterator { + ReportDescriptorIterator::new(self) + } +} + +#[derive(Debug)] +pub enum Data { + UsagePage { data: u32 }, + Usage { data: u32 }, + Input, + Output, + ReportCount { data: u32 }, +} + +pub struct ReportDescriptorIterator { + desc: ReportDescriptor, + pos: usize, +} + +impl ReportDescriptorIterator { + fn new(desc: ReportDescriptor) -> Self { + Self { desc, pos: 0 } + } + + fn next_item(&mut self) -> Option<Data> { + let item = get_hid_item(&self.desc.value[self.pos..]); + if item.is_none() { + self.pos = self.desc.value.len(); // Close, invalid data. + return None; + } + + let (tag_type, key_len, data) = item.unwrap(); + + // Advance if we have a valid item. + self.pos += key_len + data.len(); + + // We only check short items. + if key_len > 1 { + return None; // Check next item. + } + + // Short items have max. length of 4 bytes. + assert!(data.len() <= mem::size_of::<u32>()); + + // Convert data bytes to a uint. + let data = read_uint_le(data); + match tag_type { + HID_ITEM_TAGTYPE_USAGE_PAGE => Some(Data::UsagePage { data }), + HID_ITEM_TAGTYPE_USAGE => Some(Data::Usage { data }), + HID_ITEM_TAGTYPE_INPUT => Some(Data::Input), + HID_ITEM_TAGTYPE_OUTPUT => Some(Data::Output), + HID_ITEM_TAGTYPE_REPORT_COUNT => Some(Data::ReportCount { data }), + _ => None, + } + } +} + +impl Iterator for ReportDescriptorIterator { + type Item = Data; + + fn next(&mut self) -> Option<Self::Item> { + if self.pos >= self.desc.value.len() { + return None; + } + + self.next_item().or_else(|| self.next()) + } +} + +fn get_hid_item<'a>(buf: &'a [u8]) -> Option<(u8, usize, &'a [u8])> { + if (buf[0] & HID_MASK_LONG_ITEM_TAG) == HID_MASK_LONG_ITEM_TAG { + get_hid_long_item(buf) + } else { + get_hid_short_item(buf) + } +} + +fn get_hid_long_item<'a>(buf: &'a [u8]) -> Option<(u8, usize, &'a [u8])> { + // A valid long item has at least three bytes. + if buf.len() < 3 { + return None; + } + + let len = buf[1] as usize; + + // Ensure that there are enough bytes left in the buffer. + if len > buf.len() - 3 { + return None; + } + + Some((buf[2], 3 /* key length */, &buf[3..])) +} + +fn get_hid_short_item<'a>(buf: &'a [u8]) -> Option<(u8, usize, &'a [u8])> { + // This is a short item. The bottom two bits of the key + // contain the length of the data section (value) for this key. + let len = match buf[0] & HID_MASK_SHORT_ITEM_SIZE { + s @ 0..=2 => s as usize, + _ => 4, /* _ == 3 */ + }; + + // Ensure that there are enough bytes left in the buffer. + if len > buf.len() - 1 { + return None; + } + + Some(( + buf[0] & HID_MASK_ITEM_TAGTYPE, + 1, /* key length */ + &buf[1..=len], + )) +} + +fn read_uint_le(buf: &[u8]) -> u32 { + assert!(buf.len() <= 4); + // Parse the number in little endian byte order. + buf.iter() + .rev() + .fold(0, |num, b| (num << 8) | (u32::from(*b))) +} + +pub fn has_fido_usage(desc: ReportDescriptor) -> bool { + let mut usage_page = None; + let mut usage = None; + + for data in desc.iter() { + match data { + Data::UsagePage { data } => usage_page = Some(data), + Data::Usage { data } => usage = Some(data), + _ => {} + } + + // Check the values we found. + if let (Some(usage_page), Some(usage)) = (usage_page, usage) { + return usage_page == u32::from(FIDO_USAGE_PAGE) + && usage == u32::from(FIDO_USAGE_U2FHID); + } + } + + false +} + +pub fn read_hid_rpt_sizes(desc: ReportDescriptor) -> io::Result<(usize, usize)> { + let mut in_rpt_count = None; + let mut out_rpt_count = None; + let mut last_rpt_count = None; + + for data in desc.iter() { + match data { + Data::ReportCount { data } => { + if last_rpt_count != None { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Duplicate HID_ReportCount", + )); + } + last_rpt_count = Some(data as usize); + } + Data::Input => { + if last_rpt_count.is_none() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "HID_Input should be preceded by HID_ReportCount", + )); + } + if in_rpt_count.is_some() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Duplicate HID_ReportCount", + )); + } + in_rpt_count = last_rpt_count; + last_rpt_count = None + } + Data::Output => { + if last_rpt_count.is_none() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "HID_Output should be preceded by HID_ReportCount", + )); + } + if out_rpt_count.is_some() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Duplicate HID_ReportCount", + )); + } + out_rpt_count = last_rpt_count; + last_rpt_count = None; + } + _ => {} + } + } + + match (in_rpt_count, out_rpt_count) { + (Some(in_count), Some(out_count)) => { + if in_count > INIT_HEADER_SIZE + && in_count <= MAX_HID_RPT_SIZE + && out_count > INIT_HEADER_SIZE + && out_count <= MAX_HID_RPT_SIZE + { + Ok((in_count, out_count)) + } else { + Err(io::Error::new( + io::ErrorKind::InvalidData, + "Report size is too small or too large", + )) + } + } + _ => Err(io::Error::new( + io::ErrorKind::InvalidData, + "Failed to extract report sizes from report descriptor", + )), + } +} diff --git a/third_party/rust/authenticator/src/transport/linux/device.rs b/third_party/rust/authenticator/src/transport/linux/device.rs new file mode 100644 index 0000000000..3806a01888 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/device.rs @@ -0,0 +1,182 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate libc; +use crate::consts::CID_BROADCAST; +use crate::transport::hid::HIDDevice; +use crate::transport::platform::{hidraw, monitor}; +use crate::transport::{AuthenticatorInfo, ECDHSecret, FidoDevice, HIDError}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use crate::util::from_unix_result; +use std::fs::OpenOptions; +use std::hash::{Hash, Hasher}; +use std::io; +use std::io::{Read, Write}; +use std::os::unix::io::AsRawFd; +use std::path::PathBuf; + +#[derive(Debug)] +pub struct Device { + path: PathBuf, + fd: std::fs::File, + in_rpt_size: usize, + out_rpt_size: usize, + cid: [u8; 4], + dev_info: Option<U2FDeviceInfo>, + secret: Option<ECDHSecret>, + authenticator_info: Option<AuthenticatorInfo>, +} + +impl PartialEq for Device { + fn eq(&self, other: &Device) -> bool { + // The path should be the only identifying member for a device + // If the path is the same, its the same device + self.path == other.path + } +} + +impl Eq for Device {} + +impl Hash for Device { + fn hash<H: Hasher>(&self, state: &mut H) { + // The path should be the only identifying member for a device + // If the path is the same, its the same device + self.path.hash(state); + } +} + +impl Read for Device { + fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { + let bufp = buf.as_mut_ptr() as *mut libc::c_void; + let rv = unsafe { libc::read(self.fd.as_raw_fd(), bufp, buf.len()) }; + from_unix_result(rv as usize) + } +} + +impl Write for Device { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + let bufp = buf.as_ptr() as *const libc::c_void; + let rv = unsafe { libc::write(self.fd.as_raw_fd(), bufp, buf.len()) }; + from_unix_result(rv as usize) + } + + // USB HID writes don't buffer, so this will be a nop. + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl U2FDevice for Device { + fn get_cid(&self) -> &[u8; 4] { + &self.cid + } + + fn set_cid(&mut self, cid: [u8; 4]) { + self.cid = cid; + } + + fn in_rpt_size(&self) -> usize { + self.in_rpt_size + } + + fn out_rpt_size(&self) -> usize { + self.out_rpt_size + } + + fn get_property(&self, prop_name: &str) -> io::Result<String> { + monitor::get_property_linux(&self.path, prop_name) + } + + fn get_device_info(&self) -> U2FDeviceInfo { + // unwrap is okay, as dev_info must have already been set, else + // a programmer error + self.dev_info.clone().unwrap() + } + + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { + self.dev_info = Some(dev_info); + } +} + +impl HIDDevice for Device { + type BuildParameters = PathBuf; + type Id = PathBuf; + + fn new(path: PathBuf) -> Result<Self, (HIDError, Self::Id)> { + debug!("Opening device {:?}", path); + let fd = OpenOptions::new() + .read(true) + .write(true) + .open(&path) + .map_err(|e| (HIDError::IO(Some(path.clone()), e), path.clone()))?; + let (in_rpt_size, out_rpt_size) = hidraw::read_hid_rpt_sizes_or_defaults(fd.as_raw_fd()); + let mut res = Self { + path, + fd, + in_rpt_size, + out_rpt_size, + cid: CID_BROADCAST, + dev_info: None, + secret: None, + authenticator_info: None, + }; + if res.is_u2f() { + info!("new device {:?}", res.path); + Ok(res) + } else { + Err((HIDError::DeviceNotSupported, res.path.clone())) + } + } + + fn initialized(&self) -> bool { + // During successful init, the broadcast channel id gets repplaced by an actual one + self.cid != CID_BROADCAST + } + + fn id(&self) -> Self::Id { + self.path.clone() + } + + fn is_u2f(&mut self) -> bool { + hidraw::is_u2f_device(self.fd.as_raw_fd()) + } + + fn get_shared_secret(&self) -> Option<&ECDHSecret> { + self.secret.as_ref() + } + + fn set_shared_secret(&mut self, secret: ECDHSecret) { + self.secret = Some(secret); + } + + fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> { + self.authenticator_info.as_ref() + } + + fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) { + self.authenticator_info = Some(authenticator_info); + } + + /// This is used for cancellation of blocking read()-requests. + /// With this, we can clone the Device, pass it to another thread and call "cancel()" on that. + fn clone_device_as_write_only(&self) -> Result<Self, HIDError> { + let fd = OpenOptions::new() + .write(true) + .open(&self.path) + .map_err(|e| (HIDError::IO(Some(self.path.clone()), e)))?; + + Ok(Self { + path: self.path.clone(), + fd, + in_rpt_size: self.in_rpt_size, + out_rpt_size: self.out_rpt_size, + cid: self.cid, + dev_info: self.dev_info.clone(), + secret: self.secret.clone(), + authenticator_info: self.authenticator_info.clone(), + }) + } +} + +impl FidoDevice for Device {} diff --git a/third_party/rust/authenticator/src/transport/linux/hidraw.rs b/third_party/rust/authenticator/src/transport/linux/hidraw.rs new file mode 100644 index 0000000000..16d687f358 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/hidraw.rs @@ -0,0 +1,80 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#![cfg_attr(feature = "cargo-clippy", allow(clippy::cast_lossless))] + +extern crate libc; + +use std::io; +use std::os::unix::io::RawFd; + +use super::hidwrapper::{_HIDIOCGRDESC, _HIDIOCGRDESCSIZE}; +use crate::consts::MAX_HID_RPT_SIZE; +use crate::transport::hidproto::*; +use crate::util::{from_unix_result, io_err}; + +#[allow(non_camel_case_types)] +#[repr(C)] +pub struct LinuxReportDescriptor { + size: ::libc::c_int, + value: [u8; 4096], +} + +const HID_MAX_DESCRIPTOR_SIZE: usize = 4096; + +#[cfg(not(target_env = "musl"))] +type IocType = libc::c_ulong; +#[cfg(target_env = "musl")] +type IocType = libc::c_int; + +pub unsafe fn hidiocgrdescsize( + fd: libc::c_int, + val: *mut ::libc::c_int, +) -> io::Result<libc::c_int> { + from_unix_result(libc::ioctl(fd, _HIDIOCGRDESCSIZE as IocType, val)) +} + +pub unsafe fn hidiocgrdesc( + fd: libc::c_int, + val: *mut LinuxReportDescriptor, +) -> io::Result<libc::c_int> { + from_unix_result(libc::ioctl(fd, _HIDIOCGRDESC as IocType, val)) +} + +pub fn is_u2f_device(fd: RawFd) -> bool { + match read_report_descriptor(fd) { + Ok(desc) => has_fido_usage(desc), + Err(_) => false, // Upon failure, just say it's not a U2F device. + } +} + +pub fn read_hid_rpt_sizes_or_defaults(fd: RawFd) -> (usize, usize) { + let default_rpt_sizes = (MAX_HID_RPT_SIZE, MAX_HID_RPT_SIZE); + let desc = read_report_descriptor(fd); + if let Ok(desc) = desc { + if let Ok(rpt_sizes) = read_hid_rpt_sizes(desc) { + rpt_sizes + } else { + default_rpt_sizes + } + } else { + default_rpt_sizes + } +} + +fn read_report_descriptor(fd: RawFd) -> io::Result<ReportDescriptor> { + let mut desc = LinuxReportDescriptor { + size: 0, + value: [0; HID_MAX_DESCRIPTOR_SIZE], + }; + + let _ = unsafe { hidiocgrdescsize(fd, &mut desc.size)? }; + if desc.size == 0 || desc.size as usize > desc.value.len() { + return Err(io_err("unexpected hidiocgrdescsize() result")); + } + + let _ = unsafe { hidiocgrdesc(fd, &mut desc)? }; + let mut value = Vec::from(&desc.value[..]); + value.truncate(desc.size as usize); + Ok(ReportDescriptor { value }) +} diff --git a/third_party/rust/authenticator/src/transport/linux/hidwrapper.h b/third_party/rust/authenticator/src/transport/linux/hidwrapper.h new file mode 100644 index 0000000000..ce77e0f1ca --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/hidwrapper.h @@ -0,0 +1,12 @@ +#include<sys/ioctl.h> +#include<linux/hidraw.h> + +/* we define these constants to work around the fact that bindgen + can't deal with the _IOR macro function. We let cpp deal with it + for us. */ + +const __u32 _HIDIOCGRDESCSIZE = HIDIOCGRDESCSIZE; +#undef HIDIOCGRDESCSIZE + +const __u32 _HIDIOCGRDESC = HIDIOCGRDESC; +#undef HIDIOCGRDESC diff --git a/third_party/rust/authenticator/src/transport/linux/hidwrapper.rs b/third_party/rust/authenticator/src/transport/linux/hidwrapper.rs new file mode 100644 index 0000000000..82aabc6301 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/hidwrapper.rs @@ -0,0 +1,51 @@ +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +// sadly we need this file so we can avoid the suprious warnings that +// would come with bindgen, as well as to avoid cluttering the mod.rs +// with spurious architecture specific modules. + +#[cfg(target_arch = "x86")] +include!("ioctl_x86.rs"); + +#[cfg(target_arch = "x86_64")] +include!("ioctl_x86_64.rs"); + +#[cfg(all(target_arch = "mips", target_endian = "little"))] +include!("ioctl_mipsle.rs"); + +#[cfg(all(target_arch = "mips", target_endian = "big"))] +include!("ioctl_mipsbe.rs"); + +#[cfg(all(target_arch = "mips64", target_endian = "little"))] +include!("ioctl_mips64le.rs"); + +#[cfg(all(target_arch = "powerpc", target_endian = "little"))] +include!("ioctl_powerpcle.rs"); + +#[cfg(all(target_arch = "powerpc", target_endian = "big"))] +include!("ioctl_powerpcbe.rs"); + +#[cfg(all(target_arch = "powerpc64", target_endian = "little"))] +include!("ioctl_powerpc64le.rs"); + +#[cfg(all(target_arch = "powerpc64", target_endian = "big"))] +include!("ioctl_powerpc64be.rs"); + +#[cfg(all(target_arch = "arm", target_endian = "little"))] +include!("ioctl_armle.rs"); + +#[cfg(all(target_arch = "arm", target_endian = "big"))] +include!("ioctl_armbe.rs"); + +#[cfg(all(target_arch = "aarch64", target_endian = "little"))] +include!("ioctl_aarch64le.rs"); + +#[cfg(all(target_arch = "aarch64", target_endian = "big"))] +include!("ioctl_aarch64be.rs"); + +#[cfg(all(target_arch = "s390x", target_endian = "big"))] +include!("ioctl_s390xbe.rs"); + +#[cfg(all(target_arch = "riscv64", target_endian = "little"))] +include!("ioctl_riscv64.rs"); diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_aarch64le.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_aarch64le.rs new file mode 100644 index 0000000000..a784e9bf46 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/ioctl_aarch64le.rs @@ -0,0 +1,5 @@ +/* automatically generated by rust-bindgen */ + +pub type __u32 = ::std::os::raw::c_uint; +pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225; +pub const _HIDIOCGRDESC: __u32 = 2416199682; diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_armle.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_armle.rs new file mode 100644 index 0000000000..a784e9bf46 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/ioctl_armle.rs @@ -0,0 +1,5 @@ +/* automatically generated by rust-bindgen */ + +pub type __u32 = ::std::os::raw::c_uint; +pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225; +pub const _HIDIOCGRDESC: __u32 = 2416199682; diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_mips64le.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_mips64le.rs new file mode 100644 index 0000000000..1ca187fa1f --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/ioctl_mips64le.rs @@ -0,0 +1,5 @@ +/* automatically generated by rust-bindgen */ + +pub type __u32 = ::std::os::raw::c_uint; +pub const _HIDIOCGRDESCSIZE: __u32 = 1074022401; +pub const _HIDIOCGRDESC: __u32 = 1342457858; diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_mipsbe.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_mipsbe.rs new file mode 100644 index 0000000000..1ca187fa1f --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/ioctl_mipsbe.rs @@ -0,0 +1,5 @@ +/* automatically generated by rust-bindgen */ + +pub type __u32 = ::std::os::raw::c_uint; +pub const _HIDIOCGRDESCSIZE: __u32 = 1074022401; +pub const _HIDIOCGRDESC: __u32 = 1342457858; diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_mipsle.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_mipsle.rs new file mode 100644 index 0000000000..1ca187fa1f --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/ioctl_mipsle.rs @@ -0,0 +1,5 @@ +/* automatically generated by rust-bindgen */ + +pub type __u32 = ::std::os::raw::c_uint; +pub const _HIDIOCGRDESCSIZE: __u32 = 1074022401; +pub const _HIDIOCGRDESC: __u32 = 1342457858; diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64be.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64be.rs new file mode 100644 index 0000000000..1ca187fa1f --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64be.rs @@ -0,0 +1,5 @@ +/* automatically generated by rust-bindgen */ + +pub type __u32 = ::std::os::raw::c_uint; +pub const _HIDIOCGRDESCSIZE: __u32 = 1074022401; +pub const _HIDIOCGRDESC: __u32 = 1342457858; diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64le.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64le.rs new file mode 100644 index 0000000000..1ca187fa1f --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64le.rs @@ -0,0 +1,5 @@ +/* automatically generated by rust-bindgen */ + +pub type __u32 = ::std::os::raw::c_uint; +pub const _HIDIOCGRDESCSIZE: __u32 = 1074022401; +pub const _HIDIOCGRDESC: __u32 = 1342457858; diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_powerpcbe.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_powerpcbe.rs new file mode 100644 index 0000000000..1ca187fa1f --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/ioctl_powerpcbe.rs @@ -0,0 +1,5 @@ +/* automatically generated by rust-bindgen */ + +pub type __u32 = ::std::os::raw::c_uint; +pub const _HIDIOCGRDESCSIZE: __u32 = 1074022401; +pub const _HIDIOCGRDESC: __u32 = 1342457858; diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_riscv64.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_riscv64.rs new file mode 100644 index 0000000000..a784e9bf46 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/ioctl_riscv64.rs @@ -0,0 +1,5 @@ +/* automatically generated by rust-bindgen */ + +pub type __u32 = ::std::os::raw::c_uint; +pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225; +pub const _HIDIOCGRDESC: __u32 = 2416199682; diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_s390xbe.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_s390xbe.rs new file mode 100644 index 0000000000..a784e9bf46 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/ioctl_s390xbe.rs @@ -0,0 +1,5 @@ +/* automatically generated by rust-bindgen */ + +pub type __u32 = ::std::os::raw::c_uint; +pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225; +pub const _HIDIOCGRDESC: __u32 = 2416199682; diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_x86.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_x86.rs new file mode 100644 index 0000000000..a784e9bf46 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/ioctl_x86.rs @@ -0,0 +1,5 @@ +/* automatically generated by rust-bindgen */ + +pub type __u32 = ::std::os::raw::c_uint; +pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225; +pub const _HIDIOCGRDESC: __u32 = 2416199682; diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_x86_64.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_x86_64.rs new file mode 100644 index 0000000000..a784e9bf46 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/ioctl_x86_64.rs @@ -0,0 +1,5 @@ +/* automatically generated by rust-bindgen */ + +pub type __u32 = ::std::os::raw::c_uint; +pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225; +pub const _HIDIOCGRDESC: __u32 = 2416199682; diff --git a/third_party/rust/authenticator/src/transport/linux/mod.rs b/third_party/rust/authenticator/src/transport/linux/mod.rs new file mode 100644 index 0000000000..c4d490ecee --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/mod.rs @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#![allow(clippy::unreadable_literal)] + +pub mod device; +pub mod transaction; + +mod hidraw; +mod hidwrapper; +mod monitor; diff --git a/third_party/rust/authenticator/src/transport/linux/monitor.rs b/third_party/rust/authenticator/src/transport/linux/monitor.rs new file mode 100644 index 0000000000..003a034c87 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/monitor.rs @@ -0,0 +1,194 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::transport::device_selector::DeviceSelectorEvent; +use libc::{c_int, c_short, c_ulong}; +use libudev::EventType; +use runloop::RunLoop; +use std::collections::HashMap; +use std::error::Error; +use std::io; +use std::os::unix::io::AsRawFd; +use std::path::PathBuf; +use std::sync::{mpsc::Sender, Arc}; + +const UDEV_SUBSYSTEM: &str = "hidraw"; +const POLLIN: c_short = 0x0001; +const POLL_TIMEOUT: c_int = 100; + +fn poll(fds: &mut Vec<::libc::pollfd>) -> io::Result<()> { + let nfds = fds.len() as c_ulong; + + let rv = unsafe { ::libc::poll((&mut fds[..]).as_mut_ptr(), nfds, POLL_TIMEOUT) }; + + if rv < 0 { + Err(io::Error::from_raw_os_error(rv)) + } else { + Ok(()) + } +} + +pub struct Monitor<F> +where + F: Fn(PathBuf, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool) + + Sync, +{ + runloops: HashMap<PathBuf, RunLoop>, + new_device_cb: Arc<F>, + selector_sender: Sender<DeviceSelectorEvent>, + status_sender: Sender<crate::StatusUpdate>, +} + +impl<F> Monitor<F> +where + F: Fn(PathBuf, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool) + + Send + + Sync + + 'static, +{ + pub fn new( + new_device_cb: F, + selector_sender: Sender<DeviceSelectorEvent>, + status_sender: Sender<crate::StatusUpdate>, + ) -> Self { + Self { + runloops: HashMap::new(), + new_device_cb: Arc::new(new_device_cb), + selector_sender, + status_sender, + } + } + + pub fn run(&mut self, alive: &dyn Fn() -> bool) -> Result<(), Box<dyn Error>> { + let ctx = libudev::Context::new()?; + + let mut enumerator = libudev::Enumerator::new(&ctx)?; + enumerator.match_subsystem(UDEV_SUBSYSTEM)?; + + // Iterate all existing devices. + let paths: Vec<PathBuf> = enumerator + .scan_devices()? + .filter_map(|dev| dev.devnode().map(|p| p.to_owned())) + .collect(); + + // Add them all in one go to avoid race conditions in DeviceSelector + // (8 devices should be added, but the first returns already before all + // others are known to DeviceSelector) + self.selector_sender + .send(DeviceSelectorEvent::DevicesAdded(paths.clone()))?; + for path in paths { + self.add_device(path); + } + + let mut monitor = libudev::Monitor::new(&ctx)?; + monitor.match_subsystem(UDEV_SUBSYSTEM)?; + + // Start listening for new devices. + let mut socket = monitor.listen()?; + let mut fds = vec![::libc::pollfd { + fd: socket.as_raw_fd(), + events: POLLIN, + revents: 0, + }]; + + while alive() { + // Wait for new events, break on failure. + poll(&mut fds)?; + + if let Some(event) = socket.receive_event() { + self.process_event(&event); + } + } + + // Remove all tracked devices. + self.remove_all_devices(); + + Ok(()) + } + + fn process_event(&mut self, event: &libudev::Event) { + let path = event.device().devnode().map(|dn| dn.to_owned()); + + match (event.event_type(), path) { + (EventType::Add, Some(path)) => { + let _ = self + .selector_sender + .send(DeviceSelectorEvent::DevicesAdded(vec![path.clone()])); + self.add_device(path); + } + (EventType::Remove, Some(path)) => { + self.remove_device(&path); + } + _ => { /* ignore other types and failures */ } + } + } + + fn add_device(&mut self, path: PathBuf) { + let f = self.new_device_cb.clone(); + let key = path.clone(); + let selector_sender = self.selector_sender.clone(); + let status_sender = self.status_sender.clone(); + debug!("Adding device {}", path.to_string_lossy()); + + let runloop = RunLoop::new(move |alive| { + if alive() { + f(path, selector_sender, status_sender, alive); + } + }); + + if let Ok(runloop) = runloop { + self.runloops.insert(key, runloop); + } + } + + fn remove_device(&mut self, path: &PathBuf) { + let _ = self + .selector_sender + .send(DeviceSelectorEvent::DeviceRemoved(path.clone())); + + debug!("Removing device {}", path.to_string_lossy()); + if let Some(runloop) = self.runloops.remove(path) { + runloop.cancel(); + } + } + + fn remove_all_devices(&mut self) { + while !self.runloops.is_empty() { + let path = self.runloops.keys().next().unwrap().clone(); + self.remove_device(&path); + } + } +} + +pub fn get_property_linux(path: &PathBuf, prop_name: &str) -> io::Result<String> { + let ctx = libudev::Context::new()?; + + let mut enumerator = libudev::Enumerator::new(&ctx)?; + enumerator.match_subsystem(UDEV_SUBSYSTEM)?; + + // Iterate all existing devices, since we don't have a syspath + // and libudev-rs doesn't implement opening by devnode. + for dev in enumerator.scan_devices()? { + if dev.devnode().is_some() && dev.devnode().unwrap() == path { + debug!( + "get_property_linux Querying property {} from {}", + prop_name, + dev.syspath().display() + ); + + let value = dev + .attribute_value(prop_name) + .ok_or(io::ErrorKind::Other)? + .to_string_lossy(); + + debug!("get_property_linux Fetched Result, {}={}", prop_name, value); + return Ok(value.to_string()); + } + } + + Err(io::Error::new( + io::ErrorKind::Other, + "Unable to find device", + )) +} diff --git a/third_party/rust/authenticator/src/transport/linux/transaction.rs b/third_party/rust/authenticator/src/transport/linux/transaction.rs new file mode 100644 index 0000000000..35baa8f3c4 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/transaction.rs @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::errors; +use crate::statecallback::StateCallback; +use crate::transport::device_selector::{ + DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent, +}; +use crate::transport::platform::monitor::Monitor; +use runloop::RunLoop; +use std::sync::mpsc::Sender; + +pub struct Transaction { + // Handle to the thread loop. + thread: RunLoop, + device_selector: DeviceSelector, +} + +impl Transaction { + pub fn new<F, T>( + timeout: u64, + callback: StateCallback<crate::Result<T>>, + status: Sender<crate::StatusUpdate>, + new_device_cb: F, + ) -> crate::Result<Self> + where + F: Fn( + DeviceBuildParameters, + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Sync + + Send + + 'static, + T: 'static, + { + let status_sender = status.clone(); + let device_selector = DeviceSelector::run(status); + let selector_sender = device_selector.clone_sender(); + let thread = RunLoop::new_with_timeout( + move |alive| { + // Create a new device monitor. + let mut monitor = Monitor::new(new_device_cb, selector_sender, status_sender); + + // Start polling for new devices. + try_or!(monitor.run(alive), |_| callback + .call(Err(errors::AuthenticatorError::Platform))); + + // Send an error, if the callback wasn't called already. + callback.call(Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::NotAllowed, + ))); + }, + timeout, + ) + .map_err(|_| errors::AuthenticatorError::Platform)?; + + Ok(Self { + thread, + device_selector, + }) + } + + pub fn cancel(&mut self) { + info!("Transaction was cancelled."); + self.device_selector.stop(); + self.thread.cancel(); + } +} diff --git a/third_party/rust/authenticator/src/transport/macos/device.rs b/third_party/rust/authenticator/src/transport/macos/device.rs new file mode 100644 index 0000000000..757e93810f --- /dev/null +++ b/third_party/rust/authenticator/src/transport/macos/device.rs @@ -0,0 +1,227 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate log; + +use crate::consts::{CID_BROADCAST, MAX_HID_RPT_SIZE}; +use crate::transport::hid::HIDDevice; +use crate::transport::platform::iokit::*; +use crate::transport::{AuthenticatorInfo, ECDHSecret, FidoDevice, HIDError}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use core_foundation::base::*; +use core_foundation::string::*; +use std::convert::TryInto; +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::io; +use std::io::{Read, Write}; +use std::sync::mpsc::{Receiver, RecvTimeoutError}; +use std::time::Duration; + +const READ_TIMEOUT: u64 = 15; + +pub struct Device { + device_ref: IOHIDDeviceRef, + cid: [u8; 4], + report_rx: Option<Receiver<Vec<u8>>>, + dev_info: Option<U2FDeviceInfo>, + secret: Option<ECDHSecret>, + authenticator_info: Option<AuthenticatorInfo>, +} + +impl Device { + unsafe fn get_property_macos(&self, prop_name: &str) -> io::Result<String> { + let prop_ref = IOHIDDeviceGetProperty( + self.device_ref, + CFString::new(prop_name).as_concrete_TypeRef(), + ); + if prop_ref.is_null() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "IOHIDDeviceGetProperty received nullptr for property {}", + prop_name + ), + )); + } + + if CFGetTypeID(prop_ref) != CFStringGetTypeID() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "IOHIDDeviceGetProperty returned non-string type for property {}", + prop_name + ), + )); + } + + Ok(CFString::from_void(prop_ref).to_string()) + } +} + +impl fmt::Debug for Device { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Device").field("cid", &self.cid).finish() + } +} + +impl PartialEq for Device { + fn eq(&self, other_device: &Device) -> bool { + self.device_ref == other_device.device_ref + } +} + +impl Eq for Device {} + +impl Hash for Device { + fn hash<H: Hasher>(&self, state: &mut H) { + // The path should be the only identifying member for a device + // If the path is the same, its the same device + self.device_ref.hash(state); + } +} + +impl Read for Device { + fn read(&mut self, mut bytes: &mut [u8]) -> io::Result<usize> { + if let Some(rx) = &self.report_rx { + let timeout = Duration::from_secs(READ_TIMEOUT); + let data = match rx.recv_timeout(timeout) { + Ok(v) => v, + Err(e) if e == RecvTimeoutError::Timeout => { + return Err(io::Error::new(io::ErrorKind::TimedOut, e)); + } + Err(e) => { + return Err(io::Error::new(io::ErrorKind::UnexpectedEof, e)); + } + }; + bytes.write(&data) + } else { + Err(io::Error::from(io::ErrorKind::Unsupported)) + } + } +} + +impl Write for Device { + fn write(&mut self, bytes: &[u8]) -> io::Result<usize> { + assert_eq!(bytes.len(), self.out_rpt_size() + 1); + + let report_id = i64::from(bytes[0]); + // Skip report number when not using numbered reports. + let start = if report_id == 0x0 { 1 } else { 0 }; + let data = &bytes[start..]; + + let result = unsafe { + IOHIDDeviceSetReport( + self.device_ref, + kIOHIDReportTypeOutput, + report_id.try_into().unwrap(), + data.as_ptr(), + data.len() as CFIndex, + ) + }; + if result != 0 { + warn!("set_report sending failure = {0:X}", result); + return Err(io::Error::from_raw_os_error(result)); + } + trace!("set_report sending success = {0:X}", result); + + Ok(bytes.len()) + } + + // USB HID writes don't buffer, so this will be a nop. + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl U2FDevice for Device { + fn get_cid(&self) -> &[u8; 4] { + &self.cid + } + + fn set_cid(&mut self, cid: [u8; 4]) { + self.cid = cid; + } + + fn in_rpt_size(&self) -> usize { + MAX_HID_RPT_SIZE + } + + fn out_rpt_size(&self) -> usize { + MAX_HID_RPT_SIZE + } + + fn get_property(&self, prop_name: &str) -> io::Result<String> { + unsafe { self.get_property_macos(prop_name) } + } + + fn get_device_info(&self) -> U2FDeviceInfo { + // unwrap is okay, as dev_info must have already been set, else + // a programmer error + self.dev_info.clone().unwrap() + } + + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { + self.dev_info = Some(dev_info); + } +} + +impl HIDDevice for Device { + type BuildParameters = (IOHIDDeviceRef, Receiver<Vec<u8>>); + type Id = IOHIDDeviceRef; + + fn new(dev_ids: Self::BuildParameters) -> Result<Self, (HIDError, Self::Id)> { + let (device_ref, report_rx) = dev_ids; + Ok(Self { + device_ref, + cid: CID_BROADCAST, + report_rx: Some(report_rx), + dev_info: None, + secret: None, + authenticator_info: None, + }) + } + + fn initialized(&self) -> bool { + self.cid != CID_BROADCAST + } + + fn id(&self) -> Self::Id { + self.device_ref + } + + fn is_u2f(&mut self) -> bool { + true + } + fn get_shared_secret(&self) -> Option<&ECDHSecret> { + self.secret.as_ref() + } + + fn set_shared_secret(&mut self, secret: ECDHSecret) { + self.secret = Some(secret); + } + + fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> { + self.authenticator_info.as_ref() + } + + fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) { + self.authenticator_info = Some(authenticator_info); + } + + /// This is used for cancellation of blocking read()-requests. + /// With this, we can clone the Device, pass it to another thread and call "cancel()" on that. + fn clone_device_as_write_only(&self) -> Result<Self, HIDError> { + Ok(Self { + device_ref: self.device_ref, + cid: self.cid, + report_rx: None, + dev_info: self.dev_info.clone(), + secret: self.secret.clone(), + authenticator_info: self.authenticator_info.clone(), + }) + } +} + +impl FidoDevice for Device {} diff --git a/third_party/rust/authenticator/src/transport/macos/iokit.rs b/third_party/rust/authenticator/src/transport/macos/iokit.rs new file mode 100644 index 0000000000..656cdb045d --- /dev/null +++ b/third_party/rust/authenticator/src/transport/macos/iokit.rs @@ -0,0 +1,292 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals)] + +extern crate libc; + +use crate::consts::{FIDO_USAGE_PAGE, FIDO_USAGE_U2FHID}; +use core_foundation::array::*; +use core_foundation::base::*; +use core_foundation::dictionary::*; +use core_foundation::number::*; +use core_foundation::runloop::*; +use core_foundation::string::*; +use std::ops::Deref; +use std::os::raw::c_void; + +type IOOptionBits = u32; + +pub type IOReturn = libc::c_int; + +pub type IOHIDManagerRef = *mut __IOHIDManager; +pub type IOHIDManagerOptions = IOOptionBits; + +pub type IOHIDDeviceCallback = extern "C" fn( + context: *mut c_void, + result: IOReturn, + sender: *mut c_void, + device: IOHIDDeviceRef, +); + +pub type IOHIDReportType = IOOptionBits; +pub type IOHIDReportCallback = extern "C" fn( + context: *mut c_void, + result: IOReturn, + sender: IOHIDDeviceRef, + report_type: IOHIDReportType, + report_id: u32, + report: *mut u8, + report_len: CFIndex, +); + +pub const kIOHIDManagerOptionNone: IOHIDManagerOptions = 0; + +pub const kIOHIDReportTypeOutput: IOHIDReportType = 1; + +#[repr(C)] +pub struct __IOHIDManager { + __private: c_void, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +pub struct IOHIDDeviceRef(*const c_void); + +unsafe impl Send for IOHIDDeviceRef {} +unsafe impl Sync for IOHIDDeviceRef {} + +pub struct SendableRunLoop(CFRunLoopRef); + +impl SendableRunLoop { + pub fn new(runloop: CFRunLoopRef) -> Self { + // Keep the CFRunLoop alive for as long as we are. + unsafe { CFRetain(runloop as *mut c_void) }; + + SendableRunLoop(runloop) + } +} + +unsafe impl Send for SendableRunLoop {} + +impl Deref for SendableRunLoop { + type Target = CFRunLoopRef; + + fn deref(&self) -> &CFRunLoopRef { + &self.0 + } +} + +impl Drop for SendableRunLoop { + fn drop(&mut self) { + unsafe { CFRelease(self.0 as *mut c_void) }; + } +} + +#[repr(C)] +pub struct CFRunLoopObserverContext { + pub version: CFIndex, + pub info: *mut c_void, + pub retain: Option<extern "C" fn(info: *const c_void) -> *const c_void>, + pub release: Option<extern "C" fn(info: *const c_void)>, + pub copyDescription: Option<extern "C" fn(info: *const c_void) -> CFStringRef>, +} + +impl CFRunLoopObserverContext { + pub fn new(context: *mut c_void) -> Self { + Self { + version: 0 as CFIndex, + info: context, + retain: None, + release: None, + copyDescription: None, + } + } +} + +pub struct CFRunLoopEntryObserver { + observer: CFRunLoopObserverRef, + // Keep alive until the observer goes away. + context_ptr: *mut CFRunLoopObserverContext, +} + +impl CFRunLoopEntryObserver { + pub fn new(callback: CFRunLoopObserverCallBack, context: *mut c_void) -> Self { + let context = CFRunLoopObserverContext::new(context); + let context_ptr = Box::into_raw(Box::new(context)); + + let observer = unsafe { + CFRunLoopObserverCreate( + kCFAllocatorDefault, + kCFRunLoopEntry, + false as Boolean, + 0, + callback, + context_ptr, + ) + }; + + Self { + observer, + context_ptr, + } + } + + pub fn add_to_current_runloop(&self) { + unsafe { + CFRunLoopAddObserver(CFRunLoopGetCurrent(), self.observer, kCFRunLoopDefaultMode) + }; + } +} + +impl Drop for CFRunLoopEntryObserver { + fn drop(&mut self) { + unsafe { + CFRelease(self.observer as *mut c_void); + + // Drop the CFRunLoopObserverContext. + let _ = Box::from_raw(self.context_ptr); + }; + } +} + +pub struct IOHIDDeviceMatcher { + pub dict: CFDictionary<CFString, CFNumber>, +} + +impl IOHIDDeviceMatcher { + pub fn new() -> Self { + let dict = CFDictionary::<CFString, CFNumber>::from_CFType_pairs(&[ + ( + CFString::from_static_string("DeviceUsage"), + CFNumber::from(i32::from(FIDO_USAGE_U2FHID)), + ), + ( + CFString::from_static_string("DeviceUsagePage"), + CFNumber::from(i32::from(FIDO_USAGE_PAGE)), + ), + ]); + Self { dict } + } +} + +#[link(name = "IOKit", kind = "framework")] +extern "C" { + // CFRunLoop + pub fn CFRunLoopObserverCreate( + allocator: CFAllocatorRef, + activities: CFOptionFlags, + repeats: Boolean, + order: CFIndex, + callout: CFRunLoopObserverCallBack, + context: *mut CFRunLoopObserverContext, + ) -> CFRunLoopObserverRef; + + // IOHIDManager + pub fn IOHIDManagerCreate( + allocator: CFAllocatorRef, + options: IOHIDManagerOptions, + ) -> IOHIDManagerRef; + pub fn IOHIDManagerSetDeviceMatching(manager: IOHIDManagerRef, matching: CFDictionaryRef); + pub fn IOHIDManagerRegisterDeviceMatchingCallback( + manager: IOHIDManagerRef, + callback: IOHIDDeviceCallback, + context: *mut c_void, + ); + pub fn IOHIDManagerRegisterDeviceRemovalCallback( + manager: IOHIDManagerRef, + callback: IOHIDDeviceCallback, + context: *mut c_void, + ); + pub fn IOHIDManagerRegisterInputReportCallback( + manager: IOHIDManagerRef, + callback: IOHIDReportCallback, + context: *mut c_void, + ); + pub fn IOHIDManagerOpen(manager: IOHIDManagerRef, options: IOHIDManagerOptions) -> IOReturn; + pub fn IOHIDManagerClose(manager: IOHIDManagerRef, options: IOHIDManagerOptions) -> IOReturn; + pub fn IOHIDManagerScheduleWithRunLoop( + manager: IOHIDManagerRef, + runLoop: CFRunLoopRef, + runLoopMode: CFStringRef, + ); + + // IOHIDDevice + pub fn IOHIDDeviceSetReport( + device: IOHIDDeviceRef, + reportType: IOHIDReportType, + reportID: CFIndex, + report: *const u8, + reportLength: CFIndex, + ) -> IOReturn; + pub fn IOHIDDeviceGetProperty(device: IOHIDDeviceRef, key: CFStringRef) -> CFTypeRef; +} + +//////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////// + +#[cfg(test)] +mod tests { + use super::*; + use std::os::raw::c_void; + use std::ptr; + use std::sync::mpsc::{channel, Sender}; + use std::thread; + + extern "C" fn observe(_: CFRunLoopObserverRef, _: CFRunLoopActivity, context: *mut c_void) { + let tx: &Sender<SendableRunLoop> = unsafe { &*(context as *mut _) }; + + // Send the current runloop to the receiver to unblock it. + let _ = tx.send(SendableRunLoop::new(unsafe { CFRunLoopGetCurrent() })); + } + + #[test] + fn test_sendable_runloop() { + let (tx, rx) = channel(); + + let thread = thread::spawn(move || { + // Send the runloop to the owning thread. + let context = &tx as *const _ as *mut c_void; + let obs = CFRunLoopEntryObserver::new(observe, context); + obs.add_to_current_runloop(); + + unsafe { + // We need some source for the runloop to run. + let manager = IOHIDManagerCreate(kCFAllocatorDefault, 0); + assert!(!manager.is_null()); + + IOHIDManagerScheduleWithRunLoop( + manager, + CFRunLoopGetCurrent(), + kCFRunLoopDefaultMode, + ); + IOHIDManagerSetDeviceMatching(manager, ptr::null_mut()); + + let rv = IOHIDManagerOpen(manager, 0); + assert_eq!(rv, 0); + + // This will run until `CFRunLoopStop()` is called. + CFRunLoopRun(); + + let rv = IOHIDManagerClose(manager, 0); + assert_eq!(rv, 0); + + CFRelease(manager as *mut c_void); + } + }); + + // Block until we enter the CFRunLoop. + let runloop: SendableRunLoop = rx.recv().expect("failed to receive runloop"); + + // Stop the runloop. + unsafe { CFRunLoopStop(*runloop) }; + + // Stop the thread. + thread.join().expect("failed to join the thread"); + + // Try to stop the runloop again (without crashing). + unsafe { CFRunLoopStop(*runloop) }; + } +} diff --git a/third_party/rust/authenticator/src/transport/macos/mod.rs b/third_party/rust/authenticator/src/transport/macos/mod.rs new file mode 100644 index 0000000000..44e85094d0 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/macos/mod.rs @@ -0,0 +1,9 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +pub mod device; +pub mod transaction; + +mod iokit; +mod monitor; diff --git a/third_party/rust/authenticator/src/transport/macos/monitor.rs b/third_party/rust/authenticator/src/transport/macos/monitor.rs new file mode 100644 index 0000000000..eafa206505 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/macos/monitor.rs @@ -0,0 +1,212 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate libc; +extern crate log; + +use crate::transport::device_selector::DeviceSelectorEvent; +use crate::transport::platform::iokit::*; +use crate::util::io_err; +use core_foundation::base::*; +use core_foundation::runloop::*; +use runloop::RunLoop; +use std::collections::HashMap; +use std::os::raw::c_void; +use std::sync::mpsc::{channel, Receiver, Sender}; +use std::{io, slice}; + +struct DeviceData { + tx: Sender<Vec<u8>>, + runloop: RunLoop, +} + +pub struct Monitor<F> +where + F: Fn( + (IOHIDDeviceRef, Receiver<Vec<u8>>), + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Send + + Sync + + 'static, +{ + manager: IOHIDManagerRef, + // Keep alive until the monitor goes away. + _matcher: IOHIDDeviceMatcher, + map: HashMap<IOHIDDeviceRef, DeviceData>, + new_device_cb: F, + selector_sender: Sender<DeviceSelectorEvent>, + status_sender: Sender<crate::StatusUpdate>, +} + +impl<F> Monitor<F> +where + F: Fn( + (IOHIDDeviceRef, Receiver<Vec<u8>>), + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Send + + Sync + + 'static, +{ + pub fn new( + new_device_cb: F, + selector_sender: Sender<DeviceSelectorEvent>, + status_sender: Sender<crate::StatusUpdate>, + ) -> Self { + let manager = unsafe { IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDManagerOptionNone) }; + + // Match FIDO devices only. + let _matcher = IOHIDDeviceMatcher::new(); + unsafe { IOHIDManagerSetDeviceMatching(manager, _matcher.dict.as_concrete_TypeRef()) }; + + Self { + manager, + _matcher, + new_device_cb, + map: HashMap::new(), + selector_sender, + status_sender, + } + } + + pub fn start(&mut self) -> io::Result<()> { + let context = self as *mut Self as *mut c_void; + + unsafe { + IOHIDManagerRegisterDeviceMatchingCallback( + self.manager, + Monitor::<F>::on_device_matching, + context, + ); + IOHIDManagerRegisterDeviceRemovalCallback( + self.manager, + Monitor::<F>::on_device_removal, + context, + ); + IOHIDManagerRegisterInputReportCallback( + self.manager, + Monitor::<F>::on_input_report, + context, + ); + + IOHIDManagerScheduleWithRunLoop( + self.manager, + CFRunLoopGetCurrent(), + kCFRunLoopDefaultMode, + ); + + let rv = IOHIDManagerOpen(self.manager, kIOHIDManagerOptionNone); + if rv == 0 { + Ok(()) + } else { + Err(io_err(&format!("Couldn't open HID Manager, rv={}", rv))) + } + } + } + + pub fn stop(&mut self) { + // Remove all devices. + while !self.map.is_empty() { + let device_ref = *self.map.keys().next().unwrap(); + self.remove_device(device_ref); + } + + // Close the manager and its devices. + unsafe { IOHIDManagerClose(self.manager, kIOHIDManagerOptionNone) }; + } + + fn remove_device(&mut self, device_ref: IOHIDDeviceRef) { + if let Some(DeviceData { tx, runloop }) = self.map.remove(&device_ref) { + let _ = self + .selector_sender + .send(DeviceSelectorEvent::DeviceRemoved(device_ref)); + // Dropping `tx` will make Device::read() fail eventually. + drop(tx); + + // Wait until the runloop stopped. + runloop.cancel(); + } + } + + extern "C" fn on_input_report( + context: *mut c_void, + _: IOReturn, + device_ref: IOHIDDeviceRef, + _: IOHIDReportType, + _: u32, + report: *mut u8, + report_len: CFIndex, + ) { + let this = unsafe { &mut *(context as *mut Self) }; + let mut send_failed = false; + + // Ignore the report if we can't find a device for it. + if let Some(&DeviceData { ref tx, .. }) = this.map.get(&device_ref) { + let data = unsafe { slice::from_raw_parts(report, report_len as usize).to_vec() }; + send_failed = tx.send(data).is_err(); + } + + // Remove the device if sending fails. + if send_failed { + this.remove_device(device_ref); + } + } + + extern "C" fn on_device_matching( + context: *mut c_void, + _: IOReturn, + _: *mut c_void, + device_ref: IOHIDDeviceRef, + ) { + let this = unsafe { &mut *(context as *mut Self) }; + let _ = this + .selector_sender + .send(DeviceSelectorEvent::DevicesAdded(vec![device_ref])); + let selector_sender = this.selector_sender.clone(); + let status_sender = this.status_sender.clone(); + let (tx, rx) = channel(); + let f = &this.new_device_cb; + + // Create a new per-device runloop. + let runloop = RunLoop::new(move |alive| { + // Ensure that the runloop is still alive. + if alive() { + f((device_ref, rx), selector_sender, status_sender, alive); + } + }); + + if let Ok(runloop) = runloop { + this.map.insert(device_ref, DeviceData { tx, runloop }); + } + } + + extern "C" fn on_device_removal( + context: *mut c_void, + _: IOReturn, + _: *mut c_void, + device_ref: IOHIDDeviceRef, + ) { + let this = unsafe { &mut *(context as *mut Self) }; + this.remove_device(device_ref); + } +} + +impl<F> Drop for Monitor<F> +where + F: Fn( + (IOHIDDeviceRef, Receiver<Vec<u8>>), + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Send + + Sync + + 'static, +{ + fn drop(&mut self) { + unsafe { CFRelease(self.manager as *mut c_void) }; + } +} diff --git a/third_party/rust/authenticator/src/transport/macos/transaction.rs b/third_party/rust/authenticator/src/transport/macos/transaction.rs new file mode 100644 index 0000000000..7387a0ac8d --- /dev/null +++ b/third_party/rust/authenticator/src/transport/macos/transaction.rs @@ -0,0 +1,108 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate libc; + +use crate::errors; +use crate::statecallback::StateCallback; +use crate::transport::device_selector::{ + DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent, +}; +use crate::transport::platform::iokit::{CFRunLoopEntryObserver, SendableRunLoop}; +use crate::transport::platform::monitor::Monitor; +use core_foundation::runloop::*; +use std::os::raw::c_void; +use std::sync::mpsc::{channel, Sender}; +use std::thread; + +// A transaction will run the given closure in a new thread, thereby using a +// separate per-thread state machine for each HID. It will either complete or +// fail through user action, timeout, or be cancelled when overridden by a new +// transaction. +pub struct Transaction { + runloop: Option<SendableRunLoop>, + thread: Option<thread::JoinHandle<()>>, + device_selector: DeviceSelector, +} + +impl Transaction { + pub fn new<F, T>( + timeout: u64, + callback: StateCallback<crate::Result<T>>, + status: Sender<crate::StatusUpdate>, + new_device_cb: F, + ) -> crate::Result<Self> + where + F: Fn( + DeviceBuildParameters, + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Sync + + Send + + 'static, + T: 'static, + { + let (tx, rx) = channel(); + let timeout = (timeout as f64) / 1000.0; + let status_sender = status.clone(); + let device_selector = DeviceSelector::run(status); + let selector_sender = device_selector.clone_sender(); + let builder = thread::Builder::new(); + let thread = builder + .spawn(move || { + // Add a runloop observer that will be notified when we enter the + // runloop and tx.send() the current runloop to the owning thread. + // We need to ensure the runloop was entered before unblocking + // Transaction::new(), so we can always properly cancel. + let context = &tx as *const _ as *mut c_void; + let obs = CFRunLoopEntryObserver::new(Transaction::observe, context); + obs.add_to_current_runloop(); + + // Create a new HID device monitor and start polling. + let mut monitor = Monitor::new(new_device_cb, selector_sender, status_sender); + try_or!(monitor.start(), |_| callback + .call(Err(errors::AuthenticatorError::Platform))); + + // This will block until completion, abortion, or timeout. + unsafe { CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, 0) }; + + // Close the monitor and its devices. + monitor.stop(); + + // Send an error, if the callback wasn't called already. + callback.call(Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::NotAllowed, + ))); + }) + .map_err(|_| errors::AuthenticatorError::Platform)?; + + // Block until we enter the CFRunLoop. + let runloop = rx + .recv() + .map_err(|_| errors::AuthenticatorError::Platform)?; + + Ok(Self { + runloop: Some(runloop), + thread: Some(thread), + device_selector, + }) + } + + extern "C" fn observe(_: CFRunLoopObserverRef, _: CFRunLoopActivity, context: *mut c_void) { + let tx: &Sender<SendableRunLoop> = unsafe { &*(context as *mut _) }; + + // Send the current runloop to the receiver to unblock it. + let _ = tx.send(SendableRunLoop::new(unsafe { CFRunLoopGetCurrent() })); + } + + pub fn cancel(&mut self) { + // This must never be None. This won't block. + unsafe { CFRunLoopStop(*self.runloop.take().unwrap()) }; + + self.device_selector.stop(); + // This must never be None. Ignore return value. + let _ = self.thread.take().unwrap().join(); + } +} diff --git a/third_party/rust/authenticator/src/transport/mock/device.rs b/third_party/rust/authenticator/src/transport/mock/device.rs new file mode 100644 index 0000000000..e76a27db28 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/mock/device.rs @@ -0,0 +1,194 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +use crate::consts::CID_BROADCAST; +use crate::crypto::ECDHSecret; +use crate::ctap2::commands::get_info::AuthenticatorInfo; +use crate::transport::device_selector::DeviceCommand; +use crate::transport::{hid::HIDDevice, FidoDevice, HIDError}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use std::hash::{Hash, Hasher}; +use std::io::{self, Read, Write}; +use std::sync::mpsc::{channel, Receiver, Sender}; + +pub(crate) const IN_HID_RPT_SIZE: usize = 64; +const OUT_HID_RPT_SIZE: usize = 64; + +#[derive(Debug)] +pub struct Device { + pub id: String, + pub cid: [u8; 4], + pub reads: Vec<[u8; IN_HID_RPT_SIZE]>, + pub writes: Vec<[u8; OUT_HID_RPT_SIZE + 1]>, + pub dev_info: Option<U2FDeviceInfo>, + pub authenticator_info: Option<AuthenticatorInfo>, + pub sender: Option<Sender<DeviceCommand>>, + pub receiver: Option<Receiver<DeviceCommand>>, +} + +impl Device { + pub fn add_write(&mut self, packet: &[u8], fill_value: u8) { + // Add one to deal with record index check + let mut write = [fill_value; OUT_HID_RPT_SIZE + 1]; + // Make sure we start with a 0, for HID record index + write[0] = 0; + // Clone packet data in at 1, since front is padded with HID record index + write[1..=packet.len()].clone_from_slice(packet); + self.writes.push(write); + } + + pub fn add_read(&mut self, packet: &[u8], fill_value: u8) { + let mut read = [fill_value; IN_HID_RPT_SIZE]; + read[..packet.len()].clone_from_slice(packet); + self.reads.push(read); + } + + pub fn create_channel(&mut self) { + let (tx, rx) = channel(); + self.sender = Some(tx); + self.receiver = Some(rx); + } +} + +impl Write for Device { + fn write(&mut self, bytes: &[u8]) -> io::Result<usize> { + // Pop a vector from the expected writes, check for quality + // against bytes array. + assert!( + !self.writes.is_empty(), + "Ran out of expected write values! Wanted to write {:?}", + bytes + ); + let check = self.writes.remove(0); + assert_eq!(check.len(), bytes.len()); + assert_eq!(&check, bytes); + Ok(bytes.len()) + } + + // nop + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl Read for Device { + fn read(&mut self, bytes: &mut [u8]) -> io::Result<usize> { + assert!(!self.reads.is_empty(), "Ran out of read values!"); + let check = self.reads.remove(0); + assert_eq!(check.len(), bytes.len()); + bytes.clone_from_slice(&check); + Ok(check.len()) + } +} + +impl Drop for Device { + fn drop(&mut self) { + if !std::thread::panicking() { + assert!(self.reads.is_empty()); + assert!(self.writes.is_empty()); + } + } +} + +impl PartialEq for Device { + fn eq(&self, other: &Device) -> bool { + self.id == other.id + } +} + +impl Eq for Device {} + +impl Hash for Device { + fn hash<H: Hasher>(&self, state: &mut H) { + self.id.hash(state); + } +} + +impl U2FDevice for Device { + fn get_cid<'a>(&'a self) -> &'a [u8; 4] { + &self.cid + } + + fn set_cid(&mut self, cid: [u8; 4]) { + self.cid = cid; + } + + fn in_rpt_size(&self) -> usize { + IN_HID_RPT_SIZE + } + + fn out_rpt_size(&self) -> usize { + OUT_HID_RPT_SIZE + } + + fn get_property(&self, prop_name: &str) -> io::Result<String> { + Ok(format!("{} not implemented", prop_name)) + } + fn get_device_info(&self) -> U2FDeviceInfo { + self.dev_info.clone().unwrap() + } + + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { + self.dev_info = Some(dev_info); + } +} + +impl HIDDevice for Device { + type Id = String; + type BuildParameters = &'static str; // None used + + fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> { + self.authenticator_info.as_ref() + } + + fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) { + self.authenticator_info = Some(authenticator_info); + } + + fn set_shared_secret(&mut self, _: ECDHSecret) { + // Nothing + } + fn get_shared_secret(&self) -> std::option::Option<&ECDHSecret> { + None + } + + fn new(id: Self::BuildParameters) -> Result<Self, (HIDError, Self::Id)> { + Ok(Device { + id: id.to_string(), + cid: CID_BROADCAST, + reads: vec![], + writes: vec![], + dev_info: None, + authenticator_info: None, + sender: None, + receiver: None, + }) + } + + fn initialized(&self) -> bool { + self.get_cid() != &CID_BROADCAST + } + + fn id(&self) -> Self::Id { + self.id.clone() + } + + fn clone_device_as_write_only(&self) -> Result<Self, HIDError> { + Ok(Device { + id: self.id.clone(), + cid: self.cid, + reads: self.reads.clone(), + writes: self.writes.clone(), + dev_info: self.dev_info.clone(), + authenticator_info: self.authenticator_info.clone(), + sender: self.sender.clone(), + receiver: None, + }) + } + + fn is_u2f(&mut self) -> bool { + self.sender.is_some() + } +} + +impl FidoDevice for Device {} diff --git a/third_party/rust/authenticator/src/transport/mock/mod.rs b/third_party/rust/authenticator/src/transport/mock/mod.rs new file mode 100644 index 0000000000..d0e200a7ef --- /dev/null +++ b/third_party/rust/authenticator/src/transport/mock/mod.rs @@ -0,0 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +pub mod device; +pub mod transaction; diff --git a/third_party/rust/authenticator/src/transport/mock/transaction.rs b/third_party/rust/authenticator/src/transport/mock/transaction.rs new file mode 100644 index 0000000000..e19b1cb56f --- /dev/null +++ b/third_party/rust/authenticator/src/transport/mock/transaction.rs @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::statecallback::StateCallback; +use crate::transport::device_selector::{DeviceBuildParameters, DeviceSelectorEvent}; +use std::sync::mpsc::Sender; + +pub struct Transaction {} + +impl Transaction { + pub fn new<F, T>( + _timeout: u64, + _callback: StateCallback<crate::Result<T>>, + _status: Sender<crate::StatusUpdate>, + _new_device_cb: F, + ) -> crate::Result<Self> + where + F: Fn( + DeviceBuildParameters, + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Sync + + Send + + 'static, + T: 'static, + { + Ok(Self {}) + } + + pub fn cancel(&mut self) { + info!("Transaction was cancelled."); + } +} diff --git a/third_party/rust/authenticator/src/transport/mod.rs b/third_party/rust/authenticator/src/transport/mod.rs new file mode 100644 index 0000000000..c68615eedc --- /dev/null +++ b/third_party/rust/authenticator/src/transport/mod.rs @@ -0,0 +1,263 @@ +use crate::consts::{Capability, HIDCmd}; +use crate::crypto::ECDHSecret; + +use crate::ctap2::commands::client_pin::{GetKeyAgreement, PinAuth}; +use crate::ctap2::commands::get_info::{AuthenticatorInfo, GetInfo}; +use crate::ctap2::commands::get_version::GetVersion; +use crate::ctap2::commands::make_credentials::dummy_make_credentials_cmd; +use crate::ctap2::commands::selection::Selection; +use crate::ctap2::commands::{ + CommandError, PinAuthCommand, Request, RequestCtap1, RequestCtap2, Retryable, StatusCode, +}; +use crate::transport::device_selector::BlinkResult; +use crate::transport::errors::{ApduErrorStatus, HIDError}; +use crate::transport::hid::HIDDevice; +use crate::util::io_err; +use std::thread; +use std::time::Duration; + +pub mod device_selector; +pub mod errors; +pub mod hid; + +#[cfg(all( + any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"), + not(test) +))] +pub mod hidproto; + +#[cfg(all(target_os = "linux", not(test)))] +#[path = "linux/mod.rs"] +pub mod platform; + +#[cfg(all(target_os = "freebsd", not(test)))] +#[path = "freebsd/mod.rs"] +pub mod platform; + +#[cfg(all(target_os = "netbsd", not(test)))] +#[path = "netbsd/mod.rs"] +pub mod platform; + +#[cfg(all(target_os = "openbsd", not(test)))] +#[path = "openbsd/mod.rs"] +pub mod platform; + +#[cfg(all(target_os = "macos", not(test)))] +#[path = "macos/mod.rs"] +pub mod platform; + +#[cfg(all(target_os = "windows", not(test)))] +#[path = "windows/mod.rs"] +pub mod platform; + +#[cfg(not(any( + target_os = "linux", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd", + target_os = "macos", + target_os = "windows", + test +)))] +#[path = "stub/mod.rs"] +pub mod platform; + +#[cfg(test)] +#[path = "mock/mod.rs"] +pub mod platform; + +#[derive(Debug)] +pub enum Nonce { + CreateRandom, + Use([u8; 8]), +} + +// TODO(MS): This is the lazy way: FidoDevice currently only extends HIDDevice by more functions, +// but the goal is to remove U2FDevice entirely and copy over the trait-definition here +pub trait FidoDevice: HIDDevice { + fn send_msg<'msg, Out, Req: Request<Out>>(&mut self, msg: &'msg Req) -> Result<Out, HIDError> { + if !self.initialized() { + return Err(HIDError::DeviceNotInitialized); + } + + if self.supports_ctap2() && msg.is_ctap2_request() { + self.send_cbor(msg) + } else { + self.send_ctap1(msg) + } + } + + fn send_cbor<'msg, Req: RequestCtap2>( + &mut self, + msg: &'msg Req, + ) -> Result<Req::Output, HIDError> { + debug!("sending {:?} to {:?}", msg, self); + + let mut data = msg.wire_format(self)?; + let mut buf: Vec<u8> = Vec::with_capacity(data.len() + 1); + // CTAP2 command + buf.push(Req::command() as u8); + // payload + buf.append(&mut data); + let buf = buf; + + let (cmd, resp) = self.sendrecv(HIDCmd::Cbor, &buf)?; + debug!( + "got from Device {:?} status={:?}: {:?}", + self.id(), + cmd, + resp + ); + if cmd == HIDCmd::Cbor { + Ok(msg.handle_response_ctap2(self, &resp)?) + } else { + Err(HIDError::UnexpectedCmd(cmd.into())) + } + } + + fn send_ctap1<'msg, Req: RequestCtap1>( + &mut self, + msg: &'msg Req, + ) -> Result<Req::Output, HIDError> { + debug!("sending {:?} to {:?}", msg, self); + let data = msg.ctap1_format(self)?; + + loop { + let (cmd, mut data) = self.sendrecv(HIDCmd::Msg, &data)?; + debug!( + "got from Device {:?} status={:?}: {:?}", + self.id(), + cmd, + data + ); + if cmd == HIDCmd::Msg { + if data.len() < 2 { + return Err(io_err("Unexpected Response: shorter than expected").into()); + } + let split_at = data.len() - 2; + let status = data.split_off(split_at); + // This will bubble up error if status != no error + let status = ApduErrorStatus::from([status[0], status[1]]); + + match msg.handle_response_ctap1(status, &data) { + Ok(out) => return Ok(out), + Err(Retryable::Retry) => { + // sleep 100ms then loop again + // TODO(baloo): meh, use tokio instead? + thread::sleep(Duration::from_millis(100)); + } + Err(Retryable::Error(e)) => return Err(e), + } + } else { + return Err(HIDError::UnexpectedCmd(cmd.into())); + } + } + } + + // This is ugly as we have 2 init-functions now, but the fastest way currently. + fn init(&mut self, nonce: Nonce) -> Result<(), HIDError> { + let resp = <Self as HIDDevice>::initialize(self, nonce)?; + // TODO(baloo): this logic should be moved to + // transport/mod.rs::Device trait + if self.supports_ctap2() { + let command = GetInfo::default(); + let info = self.send_cbor(&command)?; + debug!("{:?} infos: {:?}", self.id(), info); + + self.set_authenticator_info(info); + } + if self.supports_ctap1() { + let command = GetVersion::default(); + // We don't really use the result here + self.send_ctap1(&command)?; + } + Ok(resp) + } + + fn block_and_blink(&mut self) -> BlinkResult { + let resp; + let supports_select_cmd = self + .get_authenticator_info() + .map_or(false, |i| i.versions.contains(&String::from("FIDO_2_1"))); + if supports_select_cmd { + let msg = Selection {}; + resp = self.send_cbor(&msg); + } else { + // We need to fake a blink-request, because FIDO2.0 forgot to specify one + // See: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#using-pinToken-in-authenticatorMakeCredential + let mut msg = match dummy_make_credentials_cmd() { + Ok(m) => m, + Err(_) => { + return BlinkResult::Cancelled; + } + }; + // Using a zero-length pinAuth will trigger the device to blink + // For CTAP1, this gets ignored anyways and we do a 'normal' register + // command, which also just blinks. + msg.set_pin_auth(Some(PinAuth::empty_pin_auth()), None); + info!("Trying to blink: {:?}", &msg); + // We don't care about the Ok-value, just if it is Ok or not + resp = self.send_msg(&msg).map(|_| ()); + } + match resp { + // Spec only says PinInvalid or PinNotSet should be returned on the fake touch-request, + // but Yubikeys for example return PinAuthInvalid. A successful return is also possible + // for CTAP1-tokens so we catch those here as well. + Ok(_) + | Err(HIDError::Command(CommandError::StatusCode(StatusCode::PinInvalid, _))) + | Err(HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthInvalid, _))) + | Err(HIDError::Command(CommandError::StatusCode(StatusCode::PinNotSet, _))) => { + BlinkResult::DeviceSelected + } + // We cancelled the receive, because another device was selected. + Err(HIDError::Command(CommandError::StatusCode(StatusCode::KeepaliveCancel, _))) + | Err(HIDError::Command(CommandError::StatusCode(StatusCode::OperationDenied, _))) + | Err(HIDError::Command(CommandError::StatusCode(StatusCode::UserActionTimeout, _))) => { + // TODO: Repeat the request, if it is a UserActionTimeout? + debug!("Device {:?} got cancelled", &self); + BlinkResult::Cancelled + } + // Something unexpected happened, so we assume this device is not usable and interpreting + // this equivalent to being cancelled. + e => { + info!("Device {:?} received unexpected answer, so we assume an error occurred and we are NOT using this device (assuming the request was cancelled): {:?}", &self, e); + BlinkResult::Cancelled + } + } + } + fn supports_ctap1(&self) -> bool { + // CAPABILITY_NMSG: + // If set to 1, authenticator DOES NOT implement U2FHID_MSG function + !self.get_device_info().cap_flags.contains(Capability::NMSG) + } + + fn supports_ctap2(&self) -> bool { + self.get_device_info().cap_flags.contains(Capability::CBOR) + } + + fn establish_shared_secret(&mut self) -> Result<(ECDHSecret, AuthenticatorInfo), HIDError> { + if !self.supports_ctap2() { + return Err(HIDError::UnsupportedCommand); + } + + let info = if let Some(authenticator_info) = self.get_authenticator_info().cloned() { + authenticator_info + } else { + // We should already have it, since it is queried upon `init()`, but just to be safe + let info_command = GetInfo::default(); + let info = self.send_cbor(&info_command)?; + debug!("infos: {:?}", info); + + self.set_authenticator_info(info.clone()); + info + }; + + // Not reusing the shared secret here, if it exists, since we might start again + // with a different PIN (e.g. if the last one was wrong) + let pin_command = GetKeyAgreement::new(&info)?; + let device_key_agreement = self.send_cbor(&pin_command)?; + let shared_secret = device_key_agreement.shared_secret()?; + self.set_shared_secret(shared_secret.clone()); + Ok((shared_secret, info)) + } +} diff --git a/third_party/rust/authenticator/src/transport/netbsd/device.rs b/third_party/rust/authenticator/src/transport/netbsd/device.rs new file mode 100644 index 0000000000..cb1d6570b9 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/netbsd/device.rs @@ -0,0 +1,227 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate libc; +use crate::consts::{CID_BROADCAST, MAX_HID_RPT_SIZE}; +use crate::transport::hid::HIDDevice; +use crate::transport::platform::fd::Fd; +use crate::transport::platform::monitor::WrappedOpenDevice; +use crate::transport::platform::uhid; +use crate::transport::{AuthenticatorInfo, ECDHSecret, FidoDevice, HIDError}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use crate::util::io_err; +use std::ffi::OsString; +use std::hash::{Hash, Hasher}; +use std::io::{self, Read, Write}; +use std::mem; + +#[derive(Debug)] +pub struct Device { + path: OsString, + fd: Fd, + cid: [u8; 4], + dev_info: Option<U2FDeviceInfo>, + secret: Option<ECDHSecret>, + authenticator_info: Option<AuthenticatorInfo>, +} + +impl Device { + fn ping(&mut self) -> io::Result<()> { + for i in 0..10 { + let mut buf = vec![0u8; 1 + MAX_HID_RPT_SIZE]; + + buf[0] = 0; // report number + buf[1] = 0xff; // CID_BROADCAST + buf[2] = 0xff; + buf[3] = 0xff; + buf[4] = 0xff; + buf[5] = 0x81; // ping + buf[6] = 0; + buf[7] = 1; // one byte + + self.write(&buf[..])?; + + // Wait for response + let mut pfd: libc::pollfd = unsafe { mem::zeroed() }; + pfd.fd = self.fd.fileno; + pfd.events = libc::POLLIN; + let nfds = unsafe { libc::poll(&mut pfd, 1, 100) }; + if nfds == -1 { + return Err(io::Error::last_os_error()); + } + if nfds == 0 { + debug!("device timeout {}", i); + continue; + } + + // Read response + self.read(&mut buf[..])?; + + return Ok(()); + } + + Err(io_err("no response from device")) + } +} + +impl PartialEq for Device { + fn eq(&self, other: &Device) -> bool { + self.fd == other.fd + } +} + +impl Eq for Device {} + +impl Hash for Device { + fn hash<H: Hasher>(&self, state: &mut H) { + self.fd.hash(state); + } +} + +impl Read for Device { + fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { + let bufp = buf.as_mut_ptr() as *mut libc::c_void; + let nread = unsafe { libc::read(self.fd.fileno, bufp, buf.len()) }; + if nread == -1 { + return Err(io::Error::last_os_error()); + } + Ok(nread as usize) + } +} + +impl Write for Device { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + // Always skip the first byte (report number) + let data = &buf[1..]; + let data_ptr = data.as_ptr() as *const libc::c_void; + let nwrit = unsafe { libc::write(self.fd.fileno, data_ptr, data.len()) }; + if nwrit == -1 { + return Err(io::Error::last_os_error()); + } + // Pretend we wrote the report number byte + Ok(nwrit as usize + 1) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl U2FDevice for Device { + fn get_cid(&self) -> &[u8; 4] { + &self.cid + } + + fn set_cid(&mut self, cid: [u8; 4]) { + self.cid = cid; + } + + fn in_rpt_size(&self) -> usize { + MAX_HID_RPT_SIZE + } + + fn out_rpt_size(&self) -> usize { + MAX_HID_RPT_SIZE + } + + fn get_property(&self, _prop_name: &str) -> io::Result<String> { + Err(io::Error::new(io::ErrorKind::Other, "Not implemented")) + } + + fn get_device_info(&self) -> U2FDeviceInfo { + // unwrap is okay, as dev_info must have already been set, else + // a programmer error + self.dev_info.clone().unwrap() + } + + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { + self.dev_info = Some(dev_info); + } +} + +impl HIDDevice for Device { + type BuildParameters = WrappedOpenDevice; + type Id = OsString; + + fn new(fido: WrappedOpenDevice) -> Result<Self, (HIDError, Self::Id)> { + debug!("device found: {:?}", fido); + let mut res = Self { + path: fido.os_path, + fd: fido.fd, + cid: CID_BROADCAST, + dev_info: None, + secret: None, + authenticator_info: None, + }; + if res.is_u2f() { + info!("new device {:?}", res.path); + Ok(res) + } else { + Err((HIDError::DeviceNotSupported, res.path.clone())) + } + } + + fn initialized(&self) -> bool { + // During successful init, the broadcast channel id gets repplaced by an actual one + self.cid != CID_BROADCAST + } + + fn id(&self) -> Self::Id { + self.path.clone() + } + + fn is_u2f(&mut self) -> bool { + if !uhid::is_u2f_device(&self.fd) { + return false; + } + // This step is not strictly necessary -- NetBSD puts fido + // devices into raw mode automatically by default, but in + // principle that might change, and this serves as a test to + // verify that we're running on a kernel with support for raw + // mode at all so we don't get confused issuing writes that try + // to set the report descriptor rather than transfer data on + // the output interrupt pipe as we need. + match uhid::hid_set_raw(&self.fd, true) { + Ok(_) => (), + Err(_) => return false, + } + if let Err(_) = self.ping() { + return false; + } + true + } + + fn get_shared_secret(&self) -> Option<&ECDHSecret> { + self.secret.as_ref() + } + + fn set_shared_secret(&mut self, secret: ECDHSecret) { + self.secret = Some(secret); + } + + fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> { + self.authenticator_info.as_ref() + } + + fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) { + self.authenticator_info = Some(authenticator_info); + } + + /// This is used for cancellation of blocking read()-requests. + /// With this, we can clone the Device, pass it to another thread and call "cancel()" on that. + fn clone_device_as_write_only(&self) -> Result<Self, HIDError> { + // Try to open the device. + let fd = Fd::open(&self.path, libc::O_WRONLY)?; + Ok(Self { + path: self.path.clone(), + fd, + cid: self.cid, + dev_info: self.dev_info.clone(), + secret: self.secret.clone(), + authenticator_info: self.authenticator_info.clone(), + }) + } +} + +impl FidoDevice for Device {} diff --git a/third_party/rust/authenticator/src/transport/netbsd/fd.rs b/third_party/rust/authenticator/src/transport/netbsd/fd.rs new file mode 100644 index 0000000000..d45410843b --- /dev/null +++ b/third_party/rust/authenticator/src/transport/netbsd/fd.rs @@ -0,0 +1,62 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate libc; + +use std::ffi::CString; +use std::ffi::OsStr; +use std::hash::{Hash, Hasher}; +use std::io; +use std::mem; +use std::os::raw::c_int; +use std::os::unix::{ffi::OsStrExt, io::RawFd}; + +#[derive(Debug)] +pub struct Fd { + pub fileno: RawFd, +} + +impl Fd { + pub fn open(path: &OsStr, flags: c_int) -> io::Result<Fd> { + let cpath = CString::new(path.as_bytes())?; + let rv = unsafe { libc::open(cpath.as_ptr(), flags) }; + if rv == -1 { + return Err(io::Error::last_os_error()); + } + Ok(Fd { fileno: rv }) + } +} + +impl Drop for Fd { + fn drop(&mut self) { + unsafe { libc::close(self.fileno) }; + } +} + +impl PartialEq for Fd { + fn eq(&self, other: &Fd) -> bool { + let mut st: libc::stat = unsafe { mem::zeroed() }; + let mut sto: libc::stat = unsafe { mem::zeroed() }; + if unsafe { libc::fstat(self.fileno, &mut st) } == -1 { + return false; + } + if unsafe { libc::fstat(other.fileno, &mut sto) } == -1 { + return false; + } + (st.st_dev == sto.st_dev) & (st.st_ino == sto.st_ino) + } +} + +impl Eq for Fd {} + +impl Hash for Fd { + fn hash<H: Hasher>(&self, state: &mut H) { + let mut st: libc::stat = unsafe { mem::zeroed() }; + if unsafe { libc::fstat(self.fileno, &mut st) } == -1 { + return; + } + st.st_dev.hash(state); + st.st_ino.hash(state); + } +} diff --git a/third_party/rust/authenticator/src/transport/netbsd/mod.rs b/third_party/rust/authenticator/src/transport/netbsd/mod.rs new file mode 100644 index 0000000000..a0eabb6e06 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/netbsd/mod.rs @@ -0,0 +1,10 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +pub mod device; +pub mod transaction; + +mod fd; +mod monitor; +mod uhid; diff --git a/third_party/rust/authenticator/src/transport/netbsd/monitor.rs b/third_party/rust/authenticator/src/transport/netbsd/monitor.rs new file mode 100644 index 0000000000..1b15733b5f --- /dev/null +++ b/third_party/rust/authenticator/src/transport/netbsd/monitor.rs @@ -0,0 +1,132 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::transport::device_selector::DeviceSelectorEvent; +use crate::transport::platform::fd::Fd; +use runloop::RunLoop; +use std::collections::HashMap; +use std::error::Error; +use std::ffi::OsString; +use std::sync::{mpsc::Sender, Arc}; +use std::thread; +use std::time::Duration; + +// XXX Should use drvctl, but it doesn't do pubsub properly yet so +// DRVGETEVENT requires write access to /dev/drvctl. Instead, for now, +// just poll every 500ms. +const POLL_TIMEOUT: u64 = 500; + +#[derive(Debug)] +pub struct WrappedOpenDevice { + pub fd: Fd, + pub os_path: OsString, +} + +pub struct Monitor<F> +where + F: Fn( + WrappedOpenDevice, + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Sync, +{ + runloops: HashMap<OsString, RunLoop>, + new_device_cb: Arc<F>, + selector_sender: Sender<DeviceSelectorEvent>, + status_sender: Sender<crate::StatusUpdate>, +} + +impl<F> Monitor<F> +where + F: Fn( + WrappedOpenDevice, + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Send + + Sync + + 'static, +{ + pub fn new( + new_device_cb: F, + selector_sender: Sender<DeviceSelectorEvent>, + status_sender: Sender<crate::StatusUpdate>, + ) -> Self { + Self { + runloops: HashMap::new(), + new_device_cb: Arc::new(new_device_cb), + selector_sender, + status_sender, + } + } + + pub fn run(&mut self, alive: &dyn Fn() -> bool) -> Result<(), Box<dyn Error>> { + // Loop until we're stopped by the controlling thread, or fail. + while alive() { + for n in 0..100 { + let uhidpath = OsString::from(format!("/dev/uhid{}", n)); + match Fd::open(&uhidpath, libc::O_RDWR | libc::O_CLOEXEC) { + Ok(uhid) => { + // The device is available if it can be opened. + let _ = self + .selector_sender + .send(DeviceSelectorEvent::DevicesAdded(vec![uhidpath.clone()])); + self.add_device(WrappedOpenDevice { + fd: uhid, + os_path: uhidpath, + }); + } + Err(ref err) => match err.raw_os_error() { + Some(libc::EBUSY) => continue, + Some(libc::ENOENT) => break, + _ => self.remove_device(uhidpath), + }, + } + } + thread::sleep(Duration::from_millis(POLL_TIMEOUT)); + } + + // Remove all tracked devices. + self.remove_all_devices(); + + Ok(()) + } + + fn add_device(&mut self, fido: WrappedOpenDevice) { + let f = self.new_device_cb.clone(); + let selector_sender = self.selector_sender.clone(); + let status_sender = self.status_sender.clone(); + let key = fido.os_path.clone(); + debug!("Adding device {}", key.to_string_lossy()); + + let runloop = RunLoop::new(move |alive| { + if alive() { + f(fido, selector_sender, status_sender, alive); + } + }); + + if let Ok(runloop) = runloop { + self.runloops.insert(key, runloop); + } + } + + fn remove_device(&mut self, path: OsString) { + let _ = self + .selector_sender + .send(DeviceSelectorEvent::DeviceRemoved(path.clone())); + + debug!("Removing device {}", path.to_string_lossy()); + if let Some(runloop) = self.runloops.remove(&path) { + runloop.cancel(); + } + } + + fn remove_all_devices(&mut self) { + while !self.runloops.is_empty() { + let path = self.runloops.keys().next().unwrap().clone(); + self.remove_device(path); + } + } +} diff --git a/third_party/rust/authenticator/src/transport/netbsd/transaction.rs b/third_party/rust/authenticator/src/transport/netbsd/transaction.rs new file mode 100644 index 0000000000..35baa8f3c4 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/netbsd/transaction.rs @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::errors; +use crate::statecallback::StateCallback; +use crate::transport::device_selector::{ + DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent, +}; +use crate::transport::platform::monitor::Monitor; +use runloop::RunLoop; +use std::sync::mpsc::Sender; + +pub struct Transaction { + // Handle to the thread loop. + thread: RunLoop, + device_selector: DeviceSelector, +} + +impl Transaction { + pub fn new<F, T>( + timeout: u64, + callback: StateCallback<crate::Result<T>>, + status: Sender<crate::StatusUpdate>, + new_device_cb: F, + ) -> crate::Result<Self> + where + F: Fn( + DeviceBuildParameters, + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Sync + + Send + + 'static, + T: 'static, + { + let status_sender = status.clone(); + let device_selector = DeviceSelector::run(status); + let selector_sender = device_selector.clone_sender(); + let thread = RunLoop::new_with_timeout( + move |alive| { + // Create a new device monitor. + let mut monitor = Monitor::new(new_device_cb, selector_sender, status_sender); + + // Start polling for new devices. + try_or!(monitor.run(alive), |_| callback + .call(Err(errors::AuthenticatorError::Platform))); + + // Send an error, if the callback wasn't called already. + callback.call(Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::NotAllowed, + ))); + }, + timeout, + ) + .map_err(|_| errors::AuthenticatorError::Platform)?; + + Ok(Self { + thread, + device_selector, + }) + } + + pub fn cancel(&mut self) { + info!("Transaction was cancelled."); + self.device_selector.stop(); + self.thread.cancel(); + } +} diff --git a/third_party/rust/authenticator/src/transport/netbsd/uhid.rs b/third_party/rust/authenticator/src/transport/netbsd/uhid.rs new file mode 100644 index 0000000000..ea183db998 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/netbsd/uhid.rs @@ -0,0 +1,77 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate libc; + +use std::io; +use std::mem; +use std::os::raw::c_int; +use std::os::raw::c_uchar; + +use crate::transport::hidproto::has_fido_usage; +use crate::transport::hidproto::ReportDescriptor; +use crate::transport::platform::fd::Fd; +use crate::util::io_err; + +/* sys/ioccom.h */ + +const IOCPARM_MASK: u32 = 0x1fff; +const IOCPARM_SHIFT: u32 = 16; +const IOCGROUP_SHIFT: u32 = 8; + +//const IOC_VOID: u32 = 0x20000000; +const IOC_OUT: u32 = 0x40000000; +const IOC_IN: u32 = 0x80000000; +//const IOC_INOUT: u32 = IOC_IN|IOC_OUT; + +macro_rules! ioctl { + ($dir:expr, $name:ident, $group:expr, $nr:expr, $ty:ty) => { + unsafe fn $name(fd: libc::c_int, val: *mut $ty) -> io::Result<libc::c_int> { + let ioc = ($dir as u32) + | ((mem::size_of::<$ty>() as u32 & IOCPARM_MASK) << IOCPARM_SHIFT) + | (($group as u32) << IOCGROUP_SHIFT) + | ($nr as u32); + let rv = libc::ioctl(fd, ioc as libc::c_ulong, val); + if rv == -1 { + return Err(io::Error::last_os_error()); + } + Ok(rv) + } + }; +} + +#[allow(non_camel_case_types)] +#[repr(C)] +struct usb_ctl_report_desc { + ucrd_size: c_int, + ucrd_data: [c_uchar; 1024], +} + +ioctl!(IOC_OUT, usb_get_report_desc, b'U', 21, usb_ctl_report_desc); + +fn read_report_descriptor(fd: &Fd) -> io::Result<ReportDescriptor> { + let mut desc = unsafe { mem::zeroed() }; + unsafe { usb_get_report_desc(fd.fileno, &mut desc) }?; + if desc.ucrd_size < 0 { + return Err(io_err("negative report descriptor size")); + } + let size = desc.ucrd_size as usize; + let value = Vec::from(&desc.ucrd_data[..size]); + Ok(ReportDescriptor { value }) +} + +pub fn is_u2f_device(fd: &Fd) -> bool { + match read_report_descriptor(fd) { + Ok(desc) => has_fido_usage(desc), + Err(_) => false, + } +} + +ioctl!(IOC_IN, usb_hid_set_raw_ioctl, b'h', 2, c_int); + +pub fn hid_set_raw(fd: &Fd, raw: bool) -> io::Result<()> { + let mut raw_int: c_int = if raw { 1 } else { 0 }; + unsafe { usb_hid_set_raw_ioctl(fd.fileno, &mut raw_int) }?; + Ok(()) +} diff --git a/third_party/rust/authenticator/src/transport/openbsd/device.rs b/third_party/rust/authenticator/src/transport/openbsd/device.rs new file mode 100644 index 0000000000..aff6f9a498 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/openbsd/device.rs @@ -0,0 +1,226 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate libc; +use crate::consts::{CID_BROADCAST, MAX_HID_RPT_SIZE}; +use crate::transport::hid::HIDDevice; +use crate::transport::platform::monitor::WrappedOpenDevice; +use crate::transport::{AuthenticatorInfo, ECDHSecret, FidoDevice, HIDError}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use crate::util::{from_unix_result, io_err}; +use std::ffi::{CString, OsString}; +use std::hash::{Hash, Hasher}; +use std::io::{self, Read, Write}; +use std::mem; +use std::os::unix::ffi::OsStrExt; + +#[derive(Debug)] +pub struct Device { + path: OsString, + fd: libc::c_int, + in_rpt_size: usize, + out_rpt_size: usize, + cid: [u8; 4], + dev_info: Option<U2FDeviceInfo>, + secret: Option<ECDHSecret>, + authenticator_info: Option<AuthenticatorInfo>, +} + +impl Device { + fn ping(&mut self) -> io::Result<()> { + let capacity = 256; + + for _ in 0..10 { + let mut data = vec![0u8; capacity]; + + // Send 1 byte ping + // self.write_all requires Device to be mut. This can't be done at the moment, + // and this is a workaround anyways, so writing by hand instead. + self.write_all(&[0, 0xff, 0xff, 0xff, 0xff, 0x81, 0, 1])?; + + // Wait for response + let mut pfd: libc::pollfd = unsafe { mem::zeroed() }; + pfd.fd = self.fd; + pfd.events = libc::POLLIN; + if from_unix_result(unsafe { libc::poll(&mut pfd, 1, 100) })? == 0 { + debug!("device {:?} timeout", self.path); + continue; + } + + // Read response + self.read(&mut data[..])?; + + return Ok(()); + } + + Err(io_err("no response from device")) + } +} + +impl Drop for Device { + fn drop(&mut self) { + // Close the fd, ignore any errors. + let _ = unsafe { libc::close(self.fd) }; + debug!("device {:?} closed", self.path); + } +} + +impl PartialEq for Device { + fn eq(&self, other: &Device) -> bool { + self.path == other.path + } +} + +impl Eq for Device {} + +impl Hash for Device { + fn hash<H: Hasher>(&self, state: &mut H) { + // The path should be the only identifying member for a device + // If the path is the same, its the same device + self.path.hash(state); + } +} + +impl Read for Device { + fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { + let buf_ptr = buf.as_mut_ptr() as *mut libc::c_void; + let rv = unsafe { libc::read(self.fd, buf_ptr, buf.len()) }; + from_unix_result(rv as usize) + } +} + +impl Write for Device { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + // Always skip the first byte (report number) + let data = &buf[1..]; + let data_ptr = data.as_ptr() as *const libc::c_void; + let rv = unsafe { libc::write(self.fd, data_ptr, data.len()) }; + Ok(from_unix_result(rv as usize)? + 1) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl U2FDevice for Device { + fn get_cid(&self) -> &[u8; 4] { + &self.cid + } + + fn set_cid(&mut self, cid: [u8; 4]) { + self.cid = cid; + } + + fn in_rpt_size(&self) -> usize { + self.in_rpt_size + } + + fn out_rpt_size(&self) -> usize { + self.out_rpt_size + } + + fn get_property(&self, _prop_name: &str) -> io::Result<String> { + Err(io::Error::new(io::ErrorKind::Other, "Not implemented")) + } + + fn get_device_info(&self) -> U2FDeviceInfo { + // unwrap is okay, as dev_info must have already been set, else + // a programmer error + self.dev_info.clone().unwrap() + } + + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { + self.dev_info = Some(dev_info); + } +} + +impl HIDDevice for Device { + type BuildParameters = WrappedOpenDevice; + type Id = OsString; + + fn new(fido: WrappedOpenDevice) -> Result<Self, (HIDError, Self::Id)> { + debug!("device found: {:?}", fido); + let mut res = Self { + path: fido.os_path, + fd: fido.fd, + in_rpt_size: MAX_HID_RPT_SIZE, + out_rpt_size: MAX_HID_RPT_SIZE, + cid: CID_BROADCAST, + dev_info: None, + secret: None, + authenticator_info: None, + }; + if res.is_u2f() { + info!("new device {:?}", res.path); + Ok(res) + } else { + Err((HIDError::DeviceNotSupported, res.path.clone())) + } + } + + fn initialized(&self) -> bool { + // During successful init, the broadcast channel id gets repplaced by an actual one + self.cid != CID_BROADCAST + } + + fn id(&self) -> Self::Id { + self.path.clone() + } + + fn is_u2f(&mut self) -> bool { + debug!("device {:?} is U2F/FIDO", self.path); + + // From OpenBSD's libfido2 in 6.6-current: + // "OpenBSD (as of 201910) has a bug that causes it to lose + // track of the DATA0/DATA1 sequence toggle across uhid device + // open and close. This is a terrible hack to work around it." + match self.ping() { + Ok(_) => true, + Err(err) => { + debug!("device {:?} is not responding: {}", self.path, err); + false + } + } + } + + fn get_shared_secret(&self) -> Option<&ECDHSecret> { + self.secret.as_ref() + } + + fn set_shared_secret(&mut self, secret: ECDHSecret) { + self.secret = Some(secret); + } + + fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> { + self.authenticator_info.as_ref() + } + + fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) { + self.authenticator_info = Some(authenticator_info); + } + + /// This is used for cancellation of blocking read()-requests. + /// With this, we can clone the Device, pass it to another thread and call "cancel()" on that. + fn clone_device_as_write_only(&self) -> Result<Self, HIDError> { + // Try to open the device. + // This can't really error out as we already did this conversion + let cstr = CString::new(self.path.as_bytes()).map_err(|_| (HIDError::DeviceError))?; + let fd = unsafe { libc::open(cstr.as_ptr(), libc::O_WRONLY) }; + let fd = + from_unix_result(fd).map_err(|e| (HIDError::IO(Some(self.path.clone().into()), e)))?; + Ok(Self { + path: self.path.clone(), + fd, + in_rpt_size: self.in_rpt_size, + out_rpt_size: self.out_rpt_size, + cid: self.cid, + dev_info: self.dev_info.clone(), + secret: self.secret.clone(), + authenticator_info: self.authenticator_info.clone(), + }) + } +} + +impl FidoDevice for Device {} diff --git a/third_party/rust/authenticator/src/transport/openbsd/mod.rs b/third_party/rust/authenticator/src/transport/openbsd/mod.rs new file mode 100644 index 0000000000..fa02132e67 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/openbsd/mod.rs @@ -0,0 +1,8 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +pub mod device; +pub mod transaction; + +mod monitor; diff --git a/third_party/rust/authenticator/src/transport/openbsd/monitor.rs b/third_party/rust/authenticator/src/transport/openbsd/monitor.rs new file mode 100644 index 0000000000..0ea5f3d0b8 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/openbsd/monitor.rs @@ -0,0 +1,138 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::transport::device_selector::DeviceSelectorEvent; +use crate::util::from_unix_result; +use runloop::RunLoop; +use std::collections::HashMap; +use std::error::Error; +use std::ffi::{CString, OsString}; +use std::os::unix::ffi::OsStrExt; +use std::os::unix::io::RawFd; +use std::path::PathBuf; +use std::sync::{mpsc::Sender, Arc}; +use std::thread; +use std::time::Duration; + +const POLL_TIMEOUT: u64 = 500; + +#[derive(Debug)] +pub struct WrappedOpenDevice { + pub fd: RawFd, + pub os_path: OsString, +} + +pub struct Monitor<F> +where + F: Fn( + WrappedOpenDevice, + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Sync, +{ + runloops: HashMap<OsString, RunLoop>, + new_device_cb: Arc<F>, + selector_sender: Sender<DeviceSelectorEvent>, + status_sender: Sender<crate::StatusUpdate>, +} + +impl<F> Monitor<F> +where + F: Fn( + WrappedOpenDevice, + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Send + + Sync + + 'static, +{ + pub fn new( + new_device_cb: F, + selector_sender: Sender<DeviceSelectorEvent>, + status_sender: Sender<crate::StatusUpdate>, + ) -> Self { + Self { + runloops: HashMap::new(), + new_device_cb: Arc::new(new_device_cb), + selector_sender, + status_sender, + } + } + + pub fn run(&mut self, alive: &dyn Fn() -> bool) -> Result<(), Box<dyn Error>> { + // Loop until we're stopped by the controlling thread, or fail. + while alive() { + // Iterate the first 10 fido(4) devices. + for path in (0..10) + .map(|unit| PathBuf::from(&format!("/dev/fido/{}", unit))) + .filter(|path| path.exists()) + { + let os_path = path.as_os_str().to_os_string(); + let cstr = CString::new(os_path.as_bytes())?; + + // Try to open the device. + let fd = unsafe { libc::open(cstr.as_ptr(), libc::O_RDWR) }; + match from_unix_result(fd) { + Ok(fd) => { + // The device is available if it can be opened. + let _ = self + .selector_sender + .send(DeviceSelectorEvent::DevicesAdded(vec![os_path.clone()])); + self.add_device(WrappedOpenDevice { fd, os_path }); + } + Err(ref err) if err.raw_os_error() == Some(libc::EBUSY) => { + // The device is available but currently in use. + } + _ => { + // libc::ENODEV or any other error. + self.remove_device(os_path); + } + } + } + + thread::sleep(Duration::from_millis(POLL_TIMEOUT)); + } + + // Remove all tracked devices. + self.remove_all_devices(); + + Ok(()) + } + + fn add_device(&mut self, fido: WrappedOpenDevice) { + if !self.runloops.contains_key(&fido.os_path) { + let f = self.new_device_cb.clone(); + let key = fido.os_path.clone(); + let selector_sender = self.selector_sender.clone(); + let status_sender = self.status_sender.clone(); + let runloop = RunLoop::new(move |alive| { + if alive() { + f(fido, selector_sender, status_sender, alive); + } + }); + + if let Ok(runloop) = runloop { + self.runloops.insert(key, runloop); + } + } + } + + fn remove_device(&mut self, path: OsString) { + let _ = self + .selector_sender + .send(DeviceSelectorEvent::DeviceRemoved(path.clone())); + if let Some(runloop) = self.runloops.remove(&path) { + runloop.cancel(); + } + } + + fn remove_all_devices(&mut self) { + while !self.runloops.is_empty() { + let path = self.runloops.keys().next().unwrap().clone(); + self.remove_device(path); + } + } +} diff --git a/third_party/rust/authenticator/src/transport/openbsd/transaction.rs b/third_party/rust/authenticator/src/transport/openbsd/transaction.rs new file mode 100644 index 0000000000..35baa8f3c4 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/openbsd/transaction.rs @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::errors; +use crate::statecallback::StateCallback; +use crate::transport::device_selector::{ + DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent, +}; +use crate::transport::platform::monitor::Monitor; +use runloop::RunLoop; +use std::sync::mpsc::Sender; + +pub struct Transaction { + // Handle to the thread loop. + thread: RunLoop, + device_selector: DeviceSelector, +} + +impl Transaction { + pub fn new<F, T>( + timeout: u64, + callback: StateCallback<crate::Result<T>>, + status: Sender<crate::StatusUpdate>, + new_device_cb: F, + ) -> crate::Result<Self> + where + F: Fn( + DeviceBuildParameters, + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Sync + + Send + + 'static, + T: 'static, + { + let status_sender = status.clone(); + let device_selector = DeviceSelector::run(status); + let selector_sender = device_selector.clone_sender(); + let thread = RunLoop::new_with_timeout( + move |alive| { + // Create a new device monitor. + let mut monitor = Monitor::new(new_device_cb, selector_sender, status_sender); + + // Start polling for new devices. + try_or!(monitor.run(alive), |_| callback + .call(Err(errors::AuthenticatorError::Platform))); + + // Send an error, if the callback wasn't called already. + callback.call(Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::NotAllowed, + ))); + }, + timeout, + ) + .map_err(|_| errors::AuthenticatorError::Platform)?; + + Ok(Self { + thread, + device_selector, + }) + } + + pub fn cancel(&mut self) { + info!("Transaction was cancelled."); + self.device_selector.stop(); + self.thread.cancel(); + } +} diff --git a/third_party/rust/authenticator/src/transport/stub/device.rs b/third_party/rust/authenticator/src/transport/stub/device.rs new file mode 100644 index 0000000000..1986fdd01f --- /dev/null +++ b/third_party/rust/authenticator/src/transport/stub/device.rs @@ -0,0 +1,110 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::transport::hid::HIDDevice; +use crate::transport::FidoDevice; +use crate::transport::{AuthenticatorInfo, ECDHSecret, HIDError}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use std::hash::{Hash, Hasher}; +use std::io; +use std::io::{Read, Write}; +use std::path::PathBuf; + +#[derive(Debug, PartialEq, Eq)] +pub struct Device {} + +impl Read for Device { + fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { + panic!("not implemented"); + } +} + +impl Write for Device { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + panic!("not implemented"); + } + + fn flush(&mut self) -> io::Result<()> { + panic!("not implemented"); + } +} + +impl U2FDevice for Device { + fn get_cid<'a>(&'a self) -> &'a [u8; 4] { + panic!("not implemented"); + } + + fn set_cid(&mut self, cid: [u8; 4]) { + panic!("not implemented"); + } + + fn in_rpt_size(&self) -> usize { + panic!("not implemented"); + } + + fn out_rpt_size(&self) -> usize { + panic!("not implemented"); + } + + fn get_property(&self, prop_name: &str) -> io::Result<String> { + panic!("not implemented") + } + + fn get_device_info(&self) -> U2FDeviceInfo { + panic!("not implemented") + } + + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { + panic!("not implemented") + } +} + +impl HIDDevice for Device { + type BuildParameters = PathBuf; + type Id = PathBuf; + + fn new(parameters: Self::BuildParameters) -> Result<Self, (HIDError, Self::Id)> { + unimplemented!(); + } + + fn initialized(&self) -> bool { + unimplemented!(); + } + + fn id(&self) -> Self::Id { + unimplemented!() + } + + fn is_u2f(&mut self) -> bool { + unimplemented!() + } + + fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> { + unimplemented!() + } + + fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) { + unimplemented!() + } + + fn set_shared_secret(&mut self, secret: ECDHSecret) { + unimplemented!() + } + + fn get_shared_secret(&self) -> Option<&ECDHSecret> { + unimplemented!() + } + + fn clone_device_as_write_only(&self) -> Result<Self, HIDError> { + unimplemented!() + } +} + +impl Hash for Device { + fn hash<H: Hasher>(&self, state: &mut H) { + unimplemented!() + } +} + +impl FidoDevice for Device {} diff --git a/third_party/rust/authenticator/src/transport/stub/mod.rs b/third_party/rust/authenticator/src/transport/stub/mod.rs new file mode 100644 index 0000000000..0fab62d495 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/stub/mod.rs @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// No-op module to permit compiling token HID support for Android, where +// no results are returned. + +#![allow(unused_variables)] + +pub mod device; +pub mod transaction; diff --git a/third_party/rust/authenticator/src/transport/stub/transaction.rs b/third_party/rust/authenticator/src/transport/stub/transaction.rs new file mode 100644 index 0000000000..21a9687ecd --- /dev/null +++ b/third_party/rust/authenticator/src/transport/stub/transaction.rs @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::errors; +use crate::statecallback::StateCallback; +use crate::transport::device_selector::{ + DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent, +}; +use std::path::PathBuf; +use std::sync::mpsc::Sender; + +pub struct Transaction {} + +impl Transaction { + pub fn new<F, T>( + timeout: u64, + callback: StateCallback<crate::Result<T>>, + status: Sender<crate::StatusUpdate>, + new_device_cb: F, + ) -> crate::Result<Self> + where + F: Fn( + DeviceBuildParameters, + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Sync + + Send + + 'static, + T: 'static, + { + // Just to silence "unused"-warnings + let mut device_selector = DeviceSelector::run(status); + let _ = DeviceSelectorEvent::DevicesAdded(vec![]); + let _ = DeviceSelectorEvent::DeviceRemoved(PathBuf::new()); + let _ = device_selector.clone_sender(); + device_selector.stop(); + + callback.call(Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::NotSupported, + ))); + + Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::NotSupported, + )) + } + + pub fn cancel(&mut self) { + /* No-op. */ + } +} diff --git a/third_party/rust/authenticator/src/transport/windows/device.rs b/third_party/rust/authenticator/src/transport/windows/device.rs new file mode 100644 index 0000000000..d0b0feac7d --- /dev/null +++ b/third_party/rust/authenticator/src/transport/windows/device.rs @@ -0,0 +1,171 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use super::winapi::DeviceCapabilities; +use crate::consts::{CID_BROADCAST, FIDO_USAGE_PAGE, FIDO_USAGE_U2FHID, MAX_HID_RPT_SIZE}; +use crate::transport::hid::HIDDevice; +use crate::transport::{AuthenticatorInfo, ECDHSecret, FidoDevice, HIDError}; +use crate::u2ftypes::{U2FDevice, U2FDeviceInfo}; +use std::fs::{File, OpenOptions}; +use std::hash::{Hash, Hasher}; +use std::io::{self, Read, Write}; +use std::os::windows::io::AsRawHandle; + +#[derive(Debug)] +pub struct Device { + path: String, + file: File, + cid: [u8; 4], + dev_info: Option<U2FDeviceInfo>, + secret: Option<ECDHSecret>, + authenticator_info: Option<AuthenticatorInfo>, +} + +impl PartialEq for Device { + fn eq(&self, other: &Device) -> bool { + self.path == other.path + } +} + +impl Eq for Device {} + +impl Hash for Device { + fn hash<H: Hasher>(&self, state: &mut H) { + // The path should be the only identifying member for a device + // If the path is the same, its the same device + self.path.hash(state); + } +} + +impl Read for Device { + fn read(&mut self, bytes: &mut [u8]) -> io::Result<usize> { + // Windows always includes the report ID. + let mut input = [0u8; MAX_HID_RPT_SIZE + 1]; + let _ = self.file.read(&mut input)?; + bytes.clone_from_slice(&input[1..]); + Ok(bytes.len() as usize) + } +} + +impl Write for Device { + fn write(&mut self, bytes: &[u8]) -> io::Result<usize> { + self.file.write(bytes) + } + + fn flush(&mut self) -> io::Result<()> { + self.file.flush() + } +} + +impl U2FDevice for Device { + fn get_cid(&self) -> &[u8; 4] { + &self.cid + } + + fn set_cid(&mut self, cid: [u8; 4]) { + self.cid = cid; + } + + fn in_rpt_size(&self) -> usize { + MAX_HID_RPT_SIZE + } + + fn out_rpt_size(&self) -> usize { + MAX_HID_RPT_SIZE + } + + fn get_property(&self, _prop_name: &str) -> io::Result<String> { + Err(io::Error::new(io::ErrorKind::Other, "Not implemented")) + } + + fn get_device_info(&self) -> U2FDeviceInfo { + // unwrap is okay, as dev_info must have already been set, else + // a programmer error + self.dev_info.clone().unwrap() + } + + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { + self.dev_info = Some(dev_info); + } +} + +impl HIDDevice for Device { + type BuildParameters = String; + type Id = String; + + fn new(path: String) -> Result<Self, (HIDError, Self::Id)> { + debug!("Opening device {:?}", path); + let file = OpenOptions::new() + .read(true) + .write(true) + .open(&path) + .map_err(|e| (HIDError::IO(Some(path.clone().into()), e), path.clone()))?; + let mut res = Self { + path, + file, + cid: CID_BROADCAST, + dev_info: None, + secret: None, + authenticator_info: None, + }; + if res.is_u2f() { + info!("new device {:?}", res.path); + Ok(res) + } else { + Err((HIDError::DeviceNotSupported, res.path.clone())) + } + } + + fn initialized(&self) -> bool { + // During successful init, the broadcast channel id gets repplaced by an actual one + self.cid != CID_BROADCAST + } + + fn id(&self) -> Self::Id { + self.path.clone() + } + + fn is_u2f(&mut self) -> bool { + match DeviceCapabilities::new(self.file.as_raw_handle()) { + Ok(caps) => caps.usage() == FIDO_USAGE_U2FHID && caps.usage_page() == FIDO_USAGE_PAGE, + _ => false, + } + } + + fn get_shared_secret(&self) -> Option<&ECDHSecret> { + self.secret.as_ref() + } + + fn set_shared_secret(&mut self, secret: ECDHSecret) { + self.secret = Some(secret); + } + + fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> { + self.authenticator_info.as_ref() + } + + fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) { + self.authenticator_info = Some(authenticator_info); + } + + /// This is used for cancellation of blocking read()-requests. + /// With this, we can clone the Device, pass it to another thread and call "cancel()" on that. + fn clone_device_as_write_only(&self) -> Result<Self, HIDError> { + let file = OpenOptions::new() + .write(true) + .open(&self.path) + .map_err(|e| (HIDError::IO(Some(self.path.clone().into()), e)))?; + + Ok(Self { + path: self.path.clone(), + file, + cid: self.cid, + dev_info: self.dev_info.clone(), + secret: self.secret.clone(), + authenticator_info: self.authenticator_info.clone(), + }) + } +} + +impl FidoDevice for Device {} diff --git a/third_party/rust/authenticator/src/transport/windows/mod.rs b/third_party/rust/authenticator/src/transport/windows/mod.rs new file mode 100644 index 0000000000..09135391dd --- /dev/null +++ b/third_party/rust/authenticator/src/transport/windows/mod.rs @@ -0,0 +1,9 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +pub mod device; +pub mod transaction; + +mod monitor; +mod winapi; diff --git a/third_party/rust/authenticator/src/transport/windows/monitor.rs b/third_party/rust/authenticator/src/transport/windows/monitor.rs new file mode 100644 index 0000000000..8bc4fc70d3 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/windows/monitor.rs @@ -0,0 +1,115 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::transport::device_selector::DeviceSelectorEvent; +use crate::transport::platform::winapi::DeviceInfoSet; +use runloop::RunLoop; +use std::collections::{HashMap, HashSet}; +use std::error::Error; +use std::iter::FromIterator; +use std::sync::{mpsc::Sender, Arc}; +use std::thread; +use std::time::Duration; + +pub struct Monitor<F> +where + F: Fn(String, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool) + + Sync, +{ + runloops: HashMap<String, RunLoop>, + new_device_cb: Arc<F>, + selector_sender: Sender<DeviceSelectorEvent>, + status_sender: Sender<crate::StatusUpdate>, +} + +impl<F> Monitor<F> +where + F: Fn(String, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool) + + Send + + Sync + + 'static, +{ + pub fn new( + new_device_cb: F, + selector_sender: Sender<DeviceSelectorEvent>, + status_sender: Sender<crate::StatusUpdate>, + ) -> Self { + Self { + runloops: HashMap::new(), + new_device_cb: Arc::new(new_device_cb), + selector_sender, + status_sender, + } + } + + pub fn run(&mut self, alive: &dyn Fn() -> bool) -> Result<(), Box<dyn Error>> { + let mut stored = HashSet::new(); + + while alive() { + let device_info_set = DeviceInfoSet::new()?; + let devices = HashSet::from_iter(device_info_set.devices()); + + // Remove devices that are gone. + for path in stored.difference(&devices) { + self.remove_device(path); + } + + let paths: Vec<_> = devices.difference(&stored).cloned().collect(); + self.selector_sender + .send(DeviceSelectorEvent::DevicesAdded(paths.clone()))?; + // Add devices that were plugged in. + for path in paths { + self.add_device(&path); + } + + // Remember the new set. + stored = devices; + + // Wait a little before looking for devices again. + thread::sleep(Duration::from_millis(100)); + } + + // Remove all tracked devices. + self.remove_all_devices(); + + Ok(()) + } + + fn add_device(&mut self, path: &String) { + let f = self.new_device_cb.clone(); + let path = path.clone(); + let key = path.clone(); + let selector_sender = self.selector_sender.clone(); + let status_sender = self.status_sender.clone(); + debug!("Adding device {}", path); + + let runloop = RunLoop::new(move |alive| { + if alive() { + f(path, selector_sender, status_sender, alive); + } + }); + + if let Ok(runloop) = runloop { + self.runloops.insert(key, runloop); + } + } + + fn remove_device(&mut self, path: &String) { + let _ = self + .selector_sender + .send(DeviceSelectorEvent::DeviceRemoved(path.clone())); + + debug!("Removing device {}", path); + if let Some(runloop) = self.runloops.remove(path) { + runloop.cancel(); + } + } + + fn remove_all_devices(&mut self) { + while !self.runloops.is_empty() { + let path = self.runloops.keys().next().unwrap().clone(); + self.remove_device(&path); + } + } +} diff --git a/third_party/rust/authenticator/src/transport/windows/transaction.rs b/third_party/rust/authenticator/src/transport/windows/transaction.rs new file mode 100644 index 0000000000..35baa8f3c4 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/windows/transaction.rs @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::errors; +use crate::statecallback::StateCallback; +use crate::transport::device_selector::{ + DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent, +}; +use crate::transport::platform::monitor::Monitor; +use runloop::RunLoop; +use std::sync::mpsc::Sender; + +pub struct Transaction { + // Handle to the thread loop. + thread: RunLoop, + device_selector: DeviceSelector, +} + +impl Transaction { + pub fn new<F, T>( + timeout: u64, + callback: StateCallback<crate::Result<T>>, + status: Sender<crate::StatusUpdate>, + new_device_cb: F, + ) -> crate::Result<Self> + where + F: Fn( + DeviceBuildParameters, + Sender<DeviceSelectorEvent>, + Sender<crate::StatusUpdate>, + &dyn Fn() -> bool, + ) + Sync + + Send + + 'static, + T: 'static, + { + let status_sender = status.clone(); + let device_selector = DeviceSelector::run(status); + let selector_sender = device_selector.clone_sender(); + let thread = RunLoop::new_with_timeout( + move |alive| { + // Create a new device monitor. + let mut monitor = Monitor::new(new_device_cb, selector_sender, status_sender); + + // Start polling for new devices. + try_or!(monitor.run(alive), |_| callback + .call(Err(errors::AuthenticatorError::Platform))); + + // Send an error, if the callback wasn't called already. + callback.call(Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::NotAllowed, + ))); + }, + timeout, + ) + .map_err(|_| errors::AuthenticatorError::Platform)?; + + Ok(Self { + thread, + device_selector, + }) + } + + pub fn cancel(&mut self) { + info!("Transaction was cancelled."); + self.device_selector.stop(); + self.thread.cancel(); + } +} diff --git a/third_party/rust/authenticator/src/transport/windows/winapi.rs b/third_party/rust/authenticator/src/transport/windows/winapi.rs new file mode 100644 index 0000000000..9ed88138a8 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/windows/winapi.rs @@ -0,0 +1,268 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::io; +use std::mem; +use std::ptr; +use std::slice; + +use std::ffi::OsString; +use std::os::windows::ffi::OsStringExt; + +use crate::util::io_err; + +extern crate libc; +extern crate winapi; + +use winapi::shared::{guiddef, minwindef, ntdef, windef}; +use winapi::shared::{hidclass, hidpi, hidusage}; +use winapi::um::{handleapi, setupapi}; + +#[link(name = "setupapi")] +extern "system" { + fn SetupDiGetClassDevsW( + ClassGuid: *const guiddef::GUID, + Enumerator: ntdef::PCSTR, + hwndParent: windef::HWND, + flags: minwindef::DWORD, + ) -> setupapi::HDEVINFO; + + fn SetupDiDestroyDeviceInfoList(DeviceInfoSet: setupapi::HDEVINFO) -> minwindef::BOOL; + + fn SetupDiEnumDeviceInterfaces( + DeviceInfoSet: setupapi::HDEVINFO, + DeviceInfoData: setupapi::PSP_DEVINFO_DATA, + InterfaceClassGuid: *const guiddef::GUID, + MemberIndex: minwindef::DWORD, + DeviceInterfaceData: setupapi::PSP_DEVICE_INTERFACE_DATA, + ) -> minwindef::BOOL; + + fn SetupDiGetDeviceInterfaceDetailW( + DeviceInfoSet: setupapi::HDEVINFO, + DeviceInterfaceData: setupapi::PSP_DEVICE_INTERFACE_DATA, + DeviceInterfaceDetailData: setupapi::PSP_DEVICE_INTERFACE_DETAIL_DATA_W, + DeviceInterfaceDetailDataSize: minwindef::DWORD, + RequiredSize: minwindef::PDWORD, + DeviceInfoData: setupapi::PSP_DEVINFO_DATA, + ) -> minwindef::BOOL; +} + +#[link(name = "hid")] +extern "system" { + fn HidD_GetPreparsedData( + HidDeviceObject: ntdef::HANDLE, + PreparsedData: *mut hidpi::PHIDP_PREPARSED_DATA, + ) -> ntdef::BOOLEAN; + + fn HidD_FreePreparsedData(PreparsedData: hidpi::PHIDP_PREPARSED_DATA) -> ntdef::BOOLEAN; + + fn HidP_GetCaps( + PreparsedData: hidpi::PHIDP_PREPARSED_DATA, + Capabilities: hidpi::PHIDP_CAPS, + ) -> ntdef::NTSTATUS; +} + +fn from_wide_ptr(ptr: *const u16, len: usize) -> String { + assert!(!ptr.is_null() && len % 2 == 0); + let slice = unsafe { slice::from_raw_parts(ptr, len / 2) }; + OsString::from_wide(slice).to_string_lossy().into_owned() +} + +pub struct DeviceInfoSet { + set: setupapi::HDEVINFO, +} + +impl DeviceInfoSet { + pub fn new() -> io::Result<Self> { + let flags = setupapi::DIGCF_PRESENT | setupapi::DIGCF_DEVICEINTERFACE; + let set = unsafe { + SetupDiGetClassDevsW( + &hidclass::GUID_DEVINTERFACE_HID, + ptr::null_mut(), + ptr::null_mut(), + flags, + ) + }; + if set == handleapi::INVALID_HANDLE_VALUE { + return Err(io_err("SetupDiGetClassDevsW failed!")); + } + + Ok(Self { set }) + } + + pub fn get(&self) -> setupapi::HDEVINFO { + self.set + } + + pub fn devices(&self) -> DeviceInfoSetIter { + DeviceInfoSetIter::new(self) + } +} + +impl Drop for DeviceInfoSet { + fn drop(&mut self) { + let _ = unsafe { SetupDiDestroyDeviceInfoList(self.set) }; + } +} + +pub struct DeviceInfoSetIter<'a> { + set: &'a DeviceInfoSet, + index: minwindef::DWORD, +} + +impl<'a> DeviceInfoSetIter<'a> { + fn new(set: &'a DeviceInfoSet) -> Self { + Self { set, index: 0 } + } +} + +impl<'a> Iterator for DeviceInfoSetIter<'a> { + type Item = String; + + fn next(&mut self) -> Option<Self::Item> { + let mut device_interface_data = + mem::MaybeUninit::<setupapi::SP_DEVICE_INTERFACE_DATA>::zeroed(); + unsafe { + (*device_interface_data.as_mut_ptr()).cbSize = + mem::size_of::<setupapi::SP_DEVICE_INTERFACE_DATA>() as minwindef::UINT; + } + + let rv = unsafe { + SetupDiEnumDeviceInterfaces( + self.set.get(), + ptr::null_mut(), + &hidclass::GUID_DEVINTERFACE_HID, + self.index, + device_interface_data.as_mut_ptr(), + ) + }; + if rv == 0 { + return None; // We're past the last device index. + } + + // Determine the size required to hold a detail struct. + let mut required_size = 0; + unsafe { + SetupDiGetDeviceInterfaceDetailW( + self.set.get(), + device_interface_data.as_mut_ptr(), + ptr::null_mut(), + required_size, + &mut required_size, + ptr::null_mut(), + ) + }; + if required_size == 0 { + return None; // An error occurred. + } + + let detail = DeviceInterfaceDetailData::new(required_size as usize); + if detail.is_none() { + return None; // malloc() failed. + } + + let detail = detail.unwrap(); + let rv = unsafe { + SetupDiGetDeviceInterfaceDetailW( + self.set.get(), + device_interface_data.as_mut_ptr(), + detail.get(), + required_size, + ptr::null_mut(), + ptr::null_mut(), + ) + }; + if rv == 0 { + return None; // An error occurred. + } + + self.index += 1; + Some(detail.path()) + } +} + +struct DeviceInterfaceDetailData { + data: setupapi::PSP_DEVICE_INTERFACE_DETAIL_DATA_W, + path_len: usize, +} + +impl DeviceInterfaceDetailData { + fn new(size: usize) -> Option<Self> { + let mut cb_size = mem::size_of::<setupapi::SP_DEVICE_INTERFACE_DETAIL_DATA_W>(); + if cfg!(target_pointer_width = "32") { + cb_size = 4 + 2; // 4-byte uint + default TCHAR size. size_of is inaccurate. + } + + if size < cb_size { + warn!("DeviceInterfaceDetailData is too small. {}", size); + return None; + } + + let data = unsafe { libc::malloc(size) as setupapi::PSP_DEVICE_INTERFACE_DETAIL_DATA_W }; + if data.is_null() { + return None; + } + + // Set total size of the structure. + unsafe { (*data).cbSize = cb_size as minwindef::UINT }; + + // Compute offset of `SP_DEVICE_INTERFACE_DETAIL_DATA_W.DevicePath`. + let offset = memoffset::offset_of!(setupapi::SP_DEVICE_INTERFACE_DETAIL_DATA_W, DevicePath); + + Some(Self { + data, + path_len: size - offset, + }) + } + + fn get(&self) -> setupapi::PSP_DEVICE_INTERFACE_DETAIL_DATA_W { + self.data + } + + fn path(&self) -> String { + unsafe { from_wide_ptr((*self.data).DevicePath.as_ptr(), self.path_len - 2) } + } +} + +impl Drop for DeviceInterfaceDetailData { + fn drop(&mut self) { + unsafe { libc::free(self.data as *mut libc::c_void) }; + } +} + +pub struct DeviceCapabilities { + caps: hidpi::HIDP_CAPS, +} + +impl DeviceCapabilities { + pub fn new(handle: ntdef::HANDLE) -> io::Result<Self> { + let mut preparsed_data = ptr::null_mut(); + let rv = unsafe { HidD_GetPreparsedData(handle, &mut preparsed_data) }; + if rv == 0 || preparsed_data.is_null() { + return Err(io_err("HidD_GetPreparsedData failed!")); + } + + let mut caps = mem::MaybeUninit::<hidpi::HIDP_CAPS>::uninit(); + unsafe { + let rv = HidP_GetCaps(preparsed_data, caps.as_mut_ptr()); + HidD_FreePreparsedData(preparsed_data); + + if rv != hidpi::HIDP_STATUS_SUCCESS { + return Err(io_err("HidP_GetCaps failed!")); + } + + Ok(Self { + caps: caps.assume_init(), + }) + } + } + + pub fn usage(&self) -> hidusage::USAGE { + self.caps.Usage + } + + pub fn usage_page(&self) -> hidusage::USAGE { + self.caps.UsagePage + } +} diff --git a/third_party/rust/authenticator/src/u2fhid-capi.h b/third_party/rust/authenticator/src/u2fhid-capi.h new file mode 100644 index 0000000000..b097cd5217 --- /dev/null +++ b/third_party/rust/authenticator/src/u2fhid-capi.h @@ -0,0 +1,119 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __U2FHID_CAPI +#define __U2FHID_CAPI +#include <stdlib.h> +#include "nsString.h" + +extern "C" { + +const uint8_t U2F_RESBUF_ID_REGISTRATION = 0; +const uint8_t U2F_RESBUF_ID_KEYHANDLE = 1; +const uint8_t U2F_RESBUF_ID_SIGNATURE = 2; +const uint8_t U2F_RESBUF_ID_APPID = 3; +const uint8_t U2F_RESBUF_ID_VENDOR_NAME = 4; +const uint8_t U2F_RESBUF_ID_DEVICE_NAME = 5; +const uint8_t U2F_RESBUF_ID_FIRMWARE_MAJOR = 6; +const uint8_t U2F_RESBUF_ID_FIRMWARE_MINOR = 7; +const uint8_t U2F_RESBUF_ID_FIRMWARE_BUILD = 8; + +const uint64_t U2F_FLAG_REQUIRE_RESIDENT_KEY = 1; +const uint64_t U2F_FLAG_REQUIRE_USER_VERIFICATION = 2; +const uint64_t U2F_FLAG_REQUIRE_PLATFORM_ATTACHMENT = 4; + +const uint8_t U2F_AUTHENTICATOR_TRANSPORT_USB = 1; +const uint8_t U2F_AUTHENTICATOR_TRANSPORT_NFC = 2; +const uint8_t U2F_AUTHENTICATOR_TRANSPORT_BLE = 4; +const uint8_t CTAP_AUTHENTICATOR_TRANSPORT_INTERNAL = 8; + +const uint8_t U2F_OK = 0; +const uint8_t U2F_ERROR_UKNOWN = 1; +const uint8_t U2F_ERROR_NOT_SUPPORTED = 2; +const uint8_t U2F_ERROR_INVALID_STATE = 3; +const uint8_t U2F_ERROR_CONSTRAINT = 4; +const uint8_t U2F_ERROR_NOT_ALLOWED = 5; +const uint8_t CTAP_ERROR_PIN_REQUIRED = 6; +const uint8_t CTAP_ERROR_PIN_INVALID = 7; +const uint8_t CTAP_ERROR_PIN_AUTH_BLOCKED = 8; +const uint8_t CTAP_ERROR_PIN_BLOCKED = 9; + +// NOTE: Preconditions +// * All rust_u2f_mgr* pointers must refer to pointers which are returned +// by rust_u2f_mgr_new, and must be freed with rust_u2f_mgr_free. +// * All rust_u2f_khs* pointers must refer to pointers which are returned +// by rust_u2f_khs_new, and must be freed with rust_u2f_khs_free. +// * All rust_u2f_res* pointers must refer to pointers passed to the +// register() and sign() callbacks. They can be null on failure. + +// The `rust_u2f_mgr` opaque type is equivalent to the rust type `U2FManager` +// TODO(MS): Once CTAP2 support is added, this should probably be renamed. +struct rust_ctap_manager; + +// The `rust_u2f_app_ids` opaque type is equivalent to the rust type `U2FAppIds` +struct rust_u2f_app_ids; + +// The `rust_u2f_key_handles` opaque type is equivalent to the rust type +// `U2FKeyHandles` +struct rust_u2f_key_handles; + +// The `rust_u2f_res` opaque type is equivalent to the rust type `U2FResult` +struct rust_u2f_result; + +// The callback passed to register() and sign(). +typedef void (*rust_u2f_callback)(uint64_t, rust_u2f_result*); + +/// U2FManager functions. + +rust_ctap_manager* rust_u2f_mgr_new(); +/* unsafe */ void rust_u2f_mgr_free(rust_ctap_manager* mgr); + +uint64_t rust_u2f_mgr_register(rust_ctap_manager* mgr, uint64_t flags, + uint64_t timeout, rust_u2f_callback, + const uint8_t* challenge_ptr, + size_t challenge_len, + const uint8_t* application_ptr, + size_t application_len, + const rust_u2f_key_handles* khs); + +uint64_t rust_u2f_mgr_sign(rust_ctap_manager* mgr, uint64_t flags, + uint64_t timeout, rust_u2f_callback, + const uint8_t* challenge_ptr, size_t challenge_len, + const rust_u2f_app_ids* app_ids, + const rust_u2f_key_handles* khs); + +void rust_u2f_mgr_cancel(rust_ctap_manager* mgr); + +/// U2FAppIds functions. + +rust_u2f_app_ids* rust_u2f_app_ids_new(); +void rust_u2f_app_ids_add(rust_u2f_app_ids* ids, const uint8_t* id, + size_t id_len); +/* unsafe */ void rust_u2f_app_ids_free(rust_u2f_app_ids* ids); + +/// U2FKeyHandles functions. + +rust_u2f_key_handles* rust_u2f_khs_new(); +void rust_u2f_khs_add(rust_u2f_key_handles* khs, const uint8_t* key_handle, + size_t key_handle_len, uint8_t transports); +/* unsafe */ void rust_u2f_khs_free(rust_u2f_key_handles* khs); + +/// U2FResult functions. + +// Returns 0 for success, or the U2F_ERROR error code >= 1. +uint8_t rust_u2f_result_error(const rust_u2f_result* res); + +// Call this before `[..]_copy()` to allocate enough space. +bool rust_u2f_resbuf_length(const rust_u2f_result* res, uint8_t bid, + size_t* len); +bool rust_u2f_resbuf_contains(const rust_u2f_result* res, uint8_t bid); +bool rust_u2f_resbuf_copy(const rust_u2f_result* res, uint8_t bid, + uint8_t* dst); +/* unsafe */ void rust_u2f_res_free(rust_u2f_result* res); + +} + +#endif // __U2FHID_CAPI diff --git a/third_party/rust/authenticator/src/u2fprotocol.rs b/third_party/rust/authenticator/src/u2fprotocol.rs new file mode 100644 index 0000000000..08d7844351 --- /dev/null +++ b/third_party/rust/authenticator/src/u2fprotocol.rs @@ -0,0 +1,398 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#![cfg_attr(feature = "cargo-clippy", allow(clippy::needless_lifetimes))] + +extern crate std; + +use rand::{thread_rng, RngCore}; +use std::ffi::CString; +use std::io; +use std::io::{Read, Write}; + +use crate::consts::*; +use crate::u2ftypes::*; +use crate::util::io_err; + +//////////////////////////////////////////////////////////////////////// +// Device Commands +//////////////////////////////////////////////////////////////////////// + +pub fn u2f_init_device<T>(dev: &mut T) -> bool +where + T: U2FDevice + Read + Write, +{ + let mut nonce = [0u8; 8]; + thread_rng().fill_bytes(&mut nonce); + + // Initialize the device and check its version. + init_device(dev, &nonce).is_ok() && is_v2_device(dev).unwrap_or(false) +} + +pub fn u2f_register<T>(dev: &mut T, challenge: &[u8], application: &[u8]) -> io::Result<Vec<u8>> +where + T: U2FDevice + Read + Write, +{ + if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid parameter sizes", + )); + } + + let mut register_data = Vec::with_capacity(2 * PARAMETER_SIZE); + register_data.extend(challenge); + register_data.extend(application); + + let flags = U2F_REQUEST_USER_PRESENCE; + let (resp, status) = send_ctap1(dev, U2F_REGISTER, flags, ®ister_data)?; + status_word_to_result(status, resp) +} + +pub fn u2f_sign<T>( + dev: &mut T, + challenge: &[u8], + application: &[u8], + key_handle: &[u8], +) -> io::Result<Vec<u8>> +where + T: U2FDevice + Read + Write, +{ + if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid parameter sizes", + )); + } + + if key_handle.len() > 256 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Key handle too large", + )); + } + + let mut sign_data = Vec::with_capacity(2 * PARAMETER_SIZE + 1 + key_handle.len()); + sign_data.extend(challenge); + sign_data.extend(application); + sign_data.push(key_handle.len() as u8); + sign_data.extend(key_handle); + + let flags = U2F_REQUEST_USER_PRESENCE; + let (resp, status) = send_ctap1(dev, U2F_AUTHENTICATE, flags, &sign_data)?; + status_word_to_result(status, resp) +} + +pub fn u2f_is_keyhandle_valid<T>( + dev: &mut T, + challenge: &[u8], + application: &[u8], + key_handle: &[u8], +) -> io::Result<bool> +where + T: U2FDevice + Read + Write, +{ + if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid parameter sizes", + )); + } + + if key_handle.len() > 256 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Key handle too large", + )); + } + + let mut sign_data = Vec::with_capacity(2 * PARAMETER_SIZE + 1 + key_handle.len()); + sign_data.extend(challenge); + sign_data.extend(application); + sign_data.push(key_handle.len() as u8); + sign_data.extend(key_handle); + + let flags = U2F_CHECK_IS_REGISTERED; + let (_, status) = send_ctap1(dev, U2F_AUTHENTICATE, flags, &sign_data)?; + Ok(status == SW_CONDITIONS_NOT_SATISFIED) +} + +//////////////////////////////////////////////////////////////////////// +// Internal Device Commands +//////////////////////////////////////////////////////////////////////// + +fn init_device<T>(dev: &mut T, nonce: &[u8]) -> io::Result<()> +where + T: U2FDevice + Read + Write, +{ + assert_eq!(nonce.len(), INIT_NONCE_SIZE); + // Send Init to broadcast address to create a new channel + let raw = sendrecv(dev, HIDCmd::Init, nonce)?; + let rsp = U2FHIDInitResp::read(&raw, nonce)?; + // Get the new Channel ID + dev.set_cid(rsp.cid); + + let vendor = dev + .get_property("Manufacturer") + .unwrap_or_else(|_| String::from("Unknown Vendor")); + let product = dev + .get_property("Product") + .unwrap_or_else(|_| String::from("Unknown Device")); + + dev.set_device_info(U2FDeviceInfo { + vendor_name: vendor.as_bytes().to_vec(), + device_name: product.as_bytes().to_vec(), + version_interface: rsp.version_interface, + version_major: rsp.version_major, + version_minor: rsp.version_minor, + version_build: rsp.version_build, + cap_flags: rsp.cap_flags, + }); + + Ok(()) +} + +fn is_v2_device<T>(dev: &mut T) -> io::Result<bool> +where + T: U2FDevice + Read + Write, +{ + let (data, status) = send_ctap1(dev, U2F_VERSION, 0x00, &[])?; + let actual = CString::new(data)?; + let expected = CString::new("U2F_V2")?; + status_word_to_result(status, actual == expected) +} + +//////////////////////////////////////////////////////////////////////// +// Error Handling +//////////////////////////////////////////////////////////////////////// + +fn status_word_to_result<T>(status: [u8; 2], val: T) -> io::Result<T> { + use self::io::ErrorKind::{InvalidData, InvalidInput}; + + match status { + SW_NO_ERROR => Ok(val), + SW_WRONG_DATA => Err(io::Error::new(InvalidData, "wrong data")), + SW_WRONG_LENGTH => Err(io::Error::new(InvalidInput, "wrong length")), + SW_CONDITIONS_NOT_SATISFIED => Err(io_err("conditions not satisfied")), + _ => Err(io_err(&format!("failed with status {:?}", status))), + } +} + +//////////////////////////////////////////////////////////////////////// +// Device Communication Functions +//////////////////////////////////////////////////////////////////////// + +pub fn sendrecv<T>(dev: &mut T, cmd: HIDCmd, send: &[u8]) -> io::Result<Vec<u8>> +where + T: U2FDevice + Read + Write, +{ + // Send initialization packet. + let mut count = U2FHIDInit::write(dev, cmd.into(), send)?; + + // Send continuation packets. + let mut sequence = 0u8; + while count < send.len() { + count += U2FHIDCont::write(dev, sequence, &send[count..])?; + sequence += 1; + } + + // Now we read. This happens in 2 chunks: The initial packet, which has the + // size we expect overall, then continuation packets, which will fill in + // data until we have everything. + let (_, mut data) = U2FHIDInit::read(dev)?; + + let mut sequence = 0u8; + while data.len() < data.capacity() { + let max = data.capacity() - data.len(); + data.extend_from_slice(&U2FHIDCont::read(dev, sequence, max)?); + sequence += 1; + } + + Ok(data) +} + +fn send_ctap1<T>(dev: &mut T, cmd: u8, p1: u8, send: &[u8]) -> io::Result<(Vec<u8>, [u8; 2])> +where + T: U2FDevice + Read + Write, +{ + let apdu = CTAP1RequestAPDU::serialize(cmd, p1, send)?; + let mut data = sendrecv(dev, HIDCmd::Msg, &apdu)?; + + if data.len() < 2 { + return Err(io_err("unexpected response")); + } + + let split_at = data.len() - 2; + let status = data.split_off(split_at); + Ok((data, [status[0], status[1]])) +} + +//////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////// + +#[cfg(test)] +pub(crate) mod tests { + use super::{init_device, is_v2_device, send_ctap1, sendrecv, U2FDevice}; + use crate::consts::{Capability, HIDCmd, CID_BROADCAST, SW_NO_ERROR}; + use crate::transport::device_selector::Device; + use crate::transport::hid::HIDDevice; + use crate::u2ftypes::U2FDeviceInfo; + use rand::{thread_rng, RngCore}; + + #[test] + fn test_init_device() { + let mut device = Device::new("u2fprotocol").unwrap(); + let nonce = vec![0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]; + + // channel id + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + + // init packet + let mut msg = CID_BROADCAST.to_vec(); + msg.extend(vec![HIDCmd::Init.into(), 0x00, 0x08]); // cmd + bcnt + msg.extend_from_slice(&nonce); + device.add_write(&msg, 0); + + // init_resp packet + let mut msg = CID_BROADCAST.to_vec(); + msg.extend(vec![HIDCmd::Init.into(), 0x00, 0x11]); // cmd + bcnt + msg.extend_from_slice(&nonce); + msg.extend_from_slice(&cid); // new channel id + msg.extend(vec![0x02, 0x04, 0x01, 0x08, 0x01]); // versions + flags + device.add_read(&msg, 0); + + init_device(&mut device, &nonce).unwrap(); + assert_eq!(device.get_cid(), &cid); + + let dev_info = device.get_device_info(); + assert_eq!(dev_info.version_interface, 0x02); + assert_eq!(dev_info.version_major, 0x04); + assert_eq!(dev_info.version_minor, 0x01); + assert_eq!(dev_info.version_build, 0x08); + assert_eq!(dev_info.cap_flags, Capability::WINK); // 0x01 + } + + #[test] + fn test_get_version() { + let mut device = Device::new("u2fprotocol").unwrap(); + // channel id + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + + device.set_cid(cid.clone()); + + let info = U2FDeviceInfo { + vendor_name: Vec::new(), + device_name: Vec::new(), + version_interface: 0x02, + version_major: 0x04, + version_minor: 0x01, + version_build: 0x08, + cap_flags: Capability::WINK, + }; + device.set_device_info(info); + + // ctap1.0 U2F_VERSION request + let mut msg = cid.to_vec(); + msg.extend(&[HIDCmd::Msg.into(), 0x0, 0x7]); // cmd + bcnt + msg.extend(&[0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0]); + device.add_write(&msg, 0); + + // fido response + let mut msg = cid.to_vec(); + msg.extend(&[HIDCmd::Msg.into(), 0x0, 0x08]); // cmd + bcnt + msg.extend(&[0x55, 0x32, 0x46, 0x5f, 0x56, 0x32]); // 'U2F_V2' + msg.extend(&SW_NO_ERROR); + device.add_read(&msg, 0); + + let res = is_v2_device(&mut device).expect("Failed to get version"); + assert!(res); + } + + #[test] + fn test_sendrecv_multiple() { + let mut device = Device::new("u2fprotocol").unwrap(); + let cid = [0x01, 0x02, 0x03, 0x04]; + device.set_cid(cid); + + // init packet + let mut msg = cid.to_vec(); + msg.extend(vec![HIDCmd::Ping.into(), 0x00, 0xe4]); // cmd + length = 228 + // write msg, append [1u8; 57], 171 bytes remain + device.add_write(&msg, 1); + device.add_read(&msg, 1); + + // cont packet + let mut msg = cid.to_vec(); + msg.push(0x00); // seq = 0 + // write msg, append [1u8; 59], 112 bytes remaining + device.add_write(&msg, 1); + device.add_read(&msg, 1); + + // cont packet + let mut msg = cid.to_vec(); + msg.push(0x01); // seq = 1 + // write msg, append [1u8; 59], 53 bytes remaining + device.add_write(&msg, 1); + device.add_read(&msg, 1); + + // cont packet + let mut msg = cid.to_vec(); + msg.push(0x02); // seq = 2 + msg.extend_from_slice(&[1u8; 53]); + // write msg, append remaining 53 bytes. + device.add_write(&msg, 0); + device.add_read(&msg, 0); + + let data = [1u8; 228]; + let d = sendrecv(&mut device, HIDCmd::Ping, &data).unwrap(); + assert_eq!(d.len(), 228); + assert_eq!(d, &data[..]); + } + + #[test] + fn test_sendapdu() { + let cid = [0x01, 0x02, 0x03, 0x04]; + let data = [0x01, 0x02, 0x03, 0x04, 0x05]; + let mut device = Device::new("u2fprotocol").unwrap(); + device.set_cid(cid); + + let mut msg = cid.to_vec(); + // sendrecv header + msg.extend(vec![HIDCmd::Msg.into(), 0x00, 0x0e]); // len = 14 + // apdu header + msg.extend(vec![ + 0x00, + HIDCmd::Ping.into(), + 0xaa, + 0x00, + 0x00, + 0x00, + 0x05, + ]); + // apdu data + msg.extend_from_slice(&data); + device.add_write(&msg, 0); + + // Send data back + let mut msg = cid.to_vec(); + msg.extend(vec![HIDCmd::Msg.into(), 0x00, 0x07]); + msg.extend_from_slice(&data); + msg.extend_from_slice(&SW_NO_ERROR); + device.add_read(&msg, 0); + + let (result, status) = send_ctap1(&mut device, HIDCmd::Ping.into(), 0xaa, &data).unwrap(); + assert_eq!(result, &data); + assert_eq!(status, SW_NO_ERROR); + } + + #[test] + fn test_get_property() { + let device = Device::new("u2fprotocol").unwrap(); + + assert_eq!(device.get_property("a").unwrap(), "a not implemented"); + } +} diff --git a/third_party/rust/authenticator/src/u2ftypes.rs b/third_party/rust/authenticator/src/u2ftypes.rs new file mode 100644 index 0000000000..782c698c17 --- /dev/null +++ b/third_party/rust/authenticator/src/u2ftypes.rs @@ -0,0 +1,363 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::consts::*; +use crate::util::io_err; +use serde::Serialize; +use std::{cmp, fmt, io, str}; + +pub fn to_hex(data: &[u8], joiner: &str) -> String { + let parts: Vec<String> = data.iter().map(|byte| format!("{:02x}", byte)).collect(); + parts.join(joiner) +} + +pub fn trace_hex(data: &[u8]) { + if log_enabled!(log::Level::Trace) { + trace!("USB send: {}", to_hex(data, "")); + } +} + +// Trait for representing U2F HID Devices. Requires getters/setters for the +// channel ID, created during device initialization. +pub trait U2FDevice { + fn get_cid(&self) -> &[u8; 4]; + fn set_cid(&mut self, cid: [u8; 4]); + + fn in_rpt_size(&self) -> usize; + fn in_init_data_size(&self) -> usize { + self.in_rpt_size() - INIT_HEADER_SIZE + } + fn in_cont_data_size(&self) -> usize { + self.in_rpt_size() - CONT_HEADER_SIZE + } + + fn out_rpt_size(&self) -> usize; + fn out_init_data_size(&self) -> usize { + self.out_rpt_size() - INIT_HEADER_SIZE + } + fn out_cont_data_size(&self) -> usize { + self.out_rpt_size() - CONT_HEADER_SIZE + } + + fn get_property(&self, prop_name: &str) -> io::Result<String>; + fn get_device_info(&self) -> U2FDeviceInfo; + fn set_device_info(&mut self, dev_info: U2FDeviceInfo); +} + +// Init structure for U2F Communications. Tells the receiver what channel +// communication is happening on, what command is running, and how much data to +// expect to receive over all. +// +// Spec at https://fidoalliance.org/specs/fido-u2f-v1. +// 0-nfc-bt-amendment-20150514/fido-u2f-hid-protocol.html#message--and-packet-structure +pub struct U2FHIDInit {} + +impl U2FHIDInit { + pub fn read<T>(dev: &mut T) -> io::Result<(HIDCmd, Vec<u8>)> + where + T: U2FDevice + io::Read, + { + let mut frame = vec![0u8; dev.in_rpt_size()]; + let mut count = dev.read(&mut frame)?; + + while dev.get_cid() != &frame[..4] { + count = dev.read(&mut frame)?; + } + + if count != dev.in_rpt_size() { + return Err(io_err("invalid init packet")); + } + + let cmd = HIDCmd::from(frame[4] | TYPE_INIT); + + let cap = (frame[5] as usize) << 8 | (frame[6] as usize); + let mut data = Vec::with_capacity(cap); + + let len = cmp::min(cap, dev.in_init_data_size()); + data.extend_from_slice(&frame[7..7 + len]); + + Ok((cmd, data)) + } + + pub fn write<T>(dev: &mut T, cmd: u8, data: &[u8]) -> io::Result<usize> + where + T: U2FDevice + io::Write, + { + if data.len() > 0xffff { + return Err(io_err("payload length > 2^16")); + } + + let mut frame = vec![0u8; dev.out_rpt_size() + 1]; + frame[1..5].copy_from_slice(dev.get_cid()); + frame[5] = cmd; + frame[6] = (data.len() >> 8) as u8; + frame[7] = data.len() as u8; + + let count = cmp::min(data.len(), dev.out_init_data_size()); + frame[8..8 + count].copy_from_slice(&data[..count]); + trace_hex(&frame); + + if dev.write(&frame)? != frame.len() { + return Err(io_err("device write failed")); + } + + Ok(count) + } +} + +// Continuation structure for U2F Communications. After an Init structure is +// sent, continuation structures are used to transmit all extra data that +// wouldn't fit in the initial packet. The sequence number increases with every +// packet, until all data is received. +// +// https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-hid-protocol. +// html#message--and-packet-structure +pub struct U2FHIDCont {} + +impl U2FHIDCont { + pub fn read<T>(dev: &mut T, seq: u8, max: usize) -> io::Result<Vec<u8>> + where + T: U2FDevice + io::Read, + { + let mut frame = vec![0u8; dev.in_rpt_size()]; + let mut count = dev.read(&mut frame)?; + + while dev.get_cid() != &frame[..4] { + count = dev.read(&mut frame)?; + } + + if count != dev.in_rpt_size() { + return Err(io_err("invalid cont packet")); + } + + if seq != frame[4] { + return Err(io_err("invalid sequence number")); + } + + let max = cmp::min(max, dev.in_cont_data_size()); + Ok(frame[5..5 + max].to_vec()) + } + + pub fn write<T>(dev: &mut T, seq: u8, data: &[u8]) -> io::Result<usize> + where + T: U2FDevice + io::Write, + { + let mut frame = vec![0u8; dev.out_rpt_size() + 1]; + frame[1..5].copy_from_slice(dev.get_cid()); + frame[5] = seq; + + let count = cmp::min(data.len(), dev.out_cont_data_size()); + frame[6..6 + count].copy_from_slice(&data[..count]); + trace_hex(&frame); + + if dev.write(&frame)? != frame.len() { + return Err(io_err("device write failed")); + } + + Ok(count) + } +} + +// Reply sent after initialization command. Contains information about U2F USB +// Key versioning, as well as the communication channel to be used for all +// further requests. +// +// https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-hid-protocol. +// html#u2fhid_init +pub struct U2FHIDInitResp { + pub cid: [u8; 4], + pub version_interface: u8, + pub version_major: u8, + pub version_minor: u8, + pub version_build: u8, + pub cap_flags: Capability, +} + +impl U2FHIDInitResp { + pub fn read(data: &[u8], nonce: &[u8]) -> io::Result<U2FHIDInitResp> { + assert_eq!(nonce.len(), INIT_NONCE_SIZE); + + if data.len() != INIT_NONCE_SIZE + 9 { + return Err(io_err("invalid init response")); + } + + if nonce != &data[..INIT_NONCE_SIZE] { + return Err(io_err("invalid nonce")); + } + + let rsp = U2FHIDInitResp { + cid: [ + data[INIT_NONCE_SIZE], + data[INIT_NONCE_SIZE + 1], + data[INIT_NONCE_SIZE + 2], + data[INIT_NONCE_SIZE + 3], + ], + version_interface: data[INIT_NONCE_SIZE + 4], + version_major: data[INIT_NONCE_SIZE + 5], + version_minor: data[INIT_NONCE_SIZE + 6], + version_build: data[INIT_NONCE_SIZE + 7], + cap_flags: Capability::from_bits_truncate(data[INIT_NONCE_SIZE + 8]), + }; + + Ok(rsp) + } +} + +/// CTAP1 (FIDO v1.x / U2F / "APDU-like") request framing format, used for +/// communication with authenticators over *all* transports. +/// +/// This implementation follows the [FIDO v1.2 spec][fido12rawf], but only +/// implements extended APDUs (supported by USB HID, NFC and BLE transports). +/// +/// # Technical details +/// +/// FIDO v1.0 U2F framing [claims][fido10rawf] to be based on +/// [ISO/IEC 7816-4:2005][iso7816] (smart card) APDUs, but has several +/// differences, errors and omissions which make it incompatible. +/// +/// FIDO v1.1 and v1.2 fixed *most* of these issues, but as a result is *not* +/// fully compatible with the FIDO v1.0 specification: +/// +/// * FIDO v1.0 *only* defines extended APDUs, though +/// [v1.0 NFC implementors][fido10nfc] need to also handle short APDUs. +/// +/// FIDO v1.1 and later define *both* short and extended APDUs, but defers to +/// transport-level guidance about which to use (where extended APDU support +/// is mandatory for all transports, and short APDU support is only mandatory +/// for NFC transports). +/// +/// * FIDO v1.0 doesn't special-case N<sub>c</sub> (command data length) = 0 +/// (ie: L<sub>c</sub> is *always* present). +/// +/// * FIDO v1.0 declares extended L<sub>c</sub> as a 24-bit integer, rather than +/// 16-bit with padding byte. +/// +/// * FIDO v1.0 omits L<sub>e</sub> bytes entirely, +/// [except for short APDUs over NFC][fido10nfc]. +/// +/// Unfortunately, FIDO v2.x gives ambiguous compatibility guidance: +/// +/// * [The FIDO v2.0 spec describes framing][fido20u2f] in +/// [FIDO v1.0 U2F Raw Message Format][fido10rawf], [cites][fido20u2fcite] the +/// FIDO v1.0 format by *name*, but actually links to the +/// [FIDO v1.2 format][fido12rawf]. +/// +/// * [The FIDO v2.1 spec also describes framing][fido21u2f] in +/// [FIDO v1.0 U2F Raw Message Format][fido10rawf], but [cites][fido21u2fcite] +/// the [FIDO v1.2 U2F Raw Message Format][fido12rawf] as a reference by name +/// and URL. +/// +/// [fido10nfc]: https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-nfc-protocol.html#framing +/// [fido10raw]: https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-raw-message-formats.html +/// [fido10rawf]: https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-raw-message-formats.html#u2f-message-framing +/// [fido12rawf]: https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#u2f-message-framing +/// [fido20u2f]: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#u2f-framing +/// [fido20u2fcite]: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#biblio-u2frawmsgs +/// [fido21u2f]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#u2f-framing +/// [fido21u2fcite]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#biblio-u2frawmsgs +/// [iso7816]: https://www.iso.org/standard/36134.html +pub struct CTAP1RequestAPDU {} + +impl CTAP1RequestAPDU { + /// Serializes a CTAP command into + /// [FIDO v1.2 U2F Raw Message Format][fido12raw]. See + /// [the struct documentation][Self] for implementation notes. + /// + /// # Arguments + /// + /// * `ins`: U2F command code, as documented in + /// [FIDO v1.2 U2F Raw Format][fido12cmd]. + /// * `p1`: Command parameter 1 / control byte. + /// * `data`: Request data, as documented in + /// [FIDO v1.2 Raw Message Formats][fido12raw], of up to 65535 bytes. + /// + /// [fido12cmd]: https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#command-and-parameter-values + /// [fido12raw]: https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html + pub fn serialize(ins: u8, p1: u8, data: &[u8]) -> io::Result<Vec<u8>> { + if data.len() > 0xffff { + return Err(io_err("payload length > 2^16")); + } + // Size of header + data. + let data_size = if data.is_empty() { 0 } else { 2 + data.len() }; + let mut bytes = vec![0u8; U2FAPDUHEADER_SIZE + data_size]; + + // bytes[0] (CLA): Always 0 in FIDO v1.x. + bytes[1] = ins; + bytes[2] = p1; + // bytes[3] (P2): Always 0 in FIDO v1.x. + + // bytes[4] (Lc1/Le1): Always 0 for extended APDUs. + if !data.is_empty() { + bytes[5] = (data.len() >> 8) as u8; // Lc2 + bytes[6] = data.len() as u8; // Lc3 + + bytes[7..7 + data.len()].copy_from_slice(data); + } + + // Last two bytes (Le): Always 0 for Ne = 65536 + Ok(bytes) + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct U2FDeviceInfo { + pub vendor_name: Vec<u8>, + pub device_name: Vec<u8>, + pub version_interface: u8, + pub version_major: u8, + pub version_minor: u8, + pub version_build: u8, + pub cap_flags: Capability, +} + +impl fmt::Display for U2FDeviceInfo { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Vendor: {}, Device: {}, Interface: {}, Firmware: v{}.{}.{}, Capabilities: {}", + str::from_utf8(&self.vendor_name).unwrap(), + str::from_utf8(&self.device_name).unwrap(), + &self.version_interface, + &self.version_major, + &self.version_minor, + &self.version_build, + to_hex(&[self.cap_flags.bits()], ":"), + ) + } +} + +//////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////// + +#[cfg(test)] +pub(crate) mod tests { + use super::CTAP1RequestAPDU; + + #[test] + fn test_ctap1_serialize() { + // Command with no data, Lc should be omitted. + assert_eq!( + vec![0, 1, 2, 0, 0, 0, 0], + CTAP1RequestAPDU::serialize(1, 2, &[]).unwrap() + ); + + // Command with data, Lc should be included. + assert_eq!( + vec![0, 1, 2, 0, 0, 0, 1, 42, 0, 0], + CTAP1RequestAPDU::serialize(1, 2, &[42]).unwrap() + ); + + // Command with 300 bytes data, longer Lc. + let d = [0xFF; 300]; + let mut expected = vec![0, 1, 2, 0, 0, 0x1, 0x2c]; + expected.extend_from_slice(&d); + expected.extend_from_slice(&[0, 0]); // Lc + assert_eq!(expected, CTAP1RequestAPDU::serialize(1, 2, &d).unwrap()); + + // Command with 64k of data should error + let big = [0xFF; 65536]; + assert!(CTAP1RequestAPDU::serialize(1, 2, &big).is_err()); + } +} diff --git a/third_party/rust/authenticator/src/util.rs b/third_party/rust/authenticator/src/util.rs new file mode 100644 index 0000000000..221d718e3b --- /dev/null +++ b/third_party/rust/authenticator/src/util.rs @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate libc; + +use std::io; + +macro_rules! try_or { + ($val:expr, $or:expr) => { + match $val { + Ok(v) => v, + Err(e) => { + return $or(e); + } + } + }; +} + +pub trait Signed { + fn is_negative(&self) -> bool; +} + +impl Signed for i32 { + fn is_negative(&self) -> bool { + *self < 0 + } +} + +impl Signed for usize { + fn is_negative(&self) -> bool { + (*self as isize) < 0 + } +} + +#[cfg(any(target_os = "linux"))] +pub fn from_unix_result<T: Signed>(rv: T) -> io::Result<T> { + if rv.is_negative() { + let errno = unsafe { *libc::__errno_location() }; + Err(io::Error::from_raw_os_error(errno)) + } else { + Ok(rv) + } +} + +#[cfg(any(target_os = "freebsd"))] +pub fn from_unix_result<T: Signed>(rv: T) -> io::Result<T> { + if rv.is_negative() { + let errno = unsafe { *libc::__error() }; + Err(io::Error::from_raw_os_error(errno)) + } else { + Ok(rv) + } +} + +#[cfg(any(target_os = "openbsd"))] +pub fn from_unix_result<T: Signed>(rv: T) -> io::Result<T> { + if rv.is_negative() { + Err(io::Error::last_os_error()) + } else { + Ok(rv) + } +} + +pub fn io_err(msg: &str) -> io::Error { + io::Error::new(io::ErrorKind::Other, msg) +} + +#[cfg(test)] +pub fn decode_hex(s: &str) -> Vec<u8> { + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()) + .collect() +} diff --git a/third_party/rust/authenticator/src/virtualdevices/mod.rs b/third_party/rust/authenticator/src/virtualdevices/mod.rs new file mode 100644 index 0000000000..5c0a9d39fc --- /dev/null +++ b/third_party/rust/authenticator/src/virtualdevices/mod.rs @@ -0,0 +1,8 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#[cfg(feature = "webdriver")] +pub mod webdriver; + +pub mod software_u2f; diff --git a/third_party/rust/authenticator/src/virtualdevices/software_u2f.rs b/third_party/rust/authenticator/src/virtualdevices/software_u2f.rs new file mode 100644 index 0000000000..cda4ca82fc --- /dev/null +++ b/third_party/rust/authenticator/src/virtualdevices/software_u2f.rs @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +use crate::consts::Capability; +use crate::{RegisterResult, SignResult}; + +pub struct SoftwareU2FToken {} + +// This is simply for platforms that aren't using the U2F Token, usually for builds +// without --feature webdriver +#[allow(dead_code)] + +impl SoftwareU2FToken { + pub fn new() -> SoftwareU2FToken { + Self {} + } + + pub fn register( + &self, + _flags: crate::RegisterFlags, + _timeout: u64, + _challenge: Vec<u8>, + _application: crate::AppId, + _key_handles: Vec<crate::KeyHandle>, + ) -> crate::Result<crate::RegisterResult> { + Ok(RegisterResult::CTAP1(vec![0u8; 16], self.dev_info())) + } + + /// The implementation of this method must return quickly and should + /// report its status via the status and callback methods + pub fn sign( + &self, + _flags: crate::SignFlags, + _timeout: u64, + _challenge: Vec<u8>, + _app_ids: Vec<crate::AppId>, + _key_handles: Vec<crate::KeyHandle>, + ) -> crate::Result<crate::SignResult> { + Ok(SignResult::CTAP1( + vec![0u8; 0], + vec![0u8; 0], + vec![0u8; 0], + self.dev_info(), + )) + } + + pub fn dev_info(&self) -> crate::u2ftypes::U2FDeviceInfo { + crate::u2ftypes::U2FDeviceInfo { + vendor_name: b"Mozilla".to_vec(), + device_name: b"Authenticator Webdriver Token".to_vec(), + version_interface: 0, + version_major: 1, + version_minor: 2, + version_build: 3, + cap_flags: Capability::empty(), + } + } +} + +//////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////// + +#[cfg(test)] +mod tests {} diff --git a/third_party/rust/authenticator/src/virtualdevices/webdriver/mod.rs b/third_party/rust/authenticator/src/virtualdevices/webdriver/mod.rs new file mode 100644 index 0000000000..b1ef27d813 --- /dev/null +++ b/third_party/rust/authenticator/src/virtualdevices/webdriver/mod.rs @@ -0,0 +1,9 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +mod testtoken; +mod virtualmanager; +mod web_api; + +pub use virtualmanager::VirtualManager; diff --git a/third_party/rust/authenticator/src/virtualdevices/webdriver/testtoken.rs b/third_party/rust/authenticator/src/virtualdevices/webdriver/testtoken.rs new file mode 100644 index 0000000000..9bf60bbaf5 --- /dev/null +++ b/third_party/rust/authenticator/src/virtualdevices/webdriver/testtoken.rs @@ -0,0 +1,140 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::errors; +use crate::virtualdevices::software_u2f::SoftwareU2FToken; +use crate::{RegisterFlags, RegisterResult, SignFlags, SignResult}; + +pub enum TestWireProtocol { + CTAP1, + CTAP2, +} + +impl TestWireProtocol { + pub fn to_webdriver_string(&self) -> String { + match self { + TestWireProtocol::CTAP1 => "ctap1/u2f".to_string(), + TestWireProtocol::CTAP2 => "ctap2".to_string(), + } + } +} + +pub struct TestTokenCredential { + pub credential: Vec<u8>, + pub privkey: Vec<u8>, + pub user_handle: Vec<u8>, + pub sign_count: u64, + pub is_resident_credential: bool, + pub rp_id: String, +} + +pub struct TestToken { + pub id: u64, + pub protocol: TestWireProtocol, + pub transport: String, + pub is_user_consenting: bool, + pub has_user_verification: bool, + pub is_user_verified: bool, + pub has_resident_key: bool, + pub u2f_impl: Option<SoftwareU2FToken>, + pub credentials: Vec<TestTokenCredential>, +} + +impl TestToken { + pub fn new( + id: u64, + protocol: TestWireProtocol, + transport: String, + is_user_consenting: bool, + has_user_verification: bool, + is_user_verified: bool, + has_resident_key: bool, + ) -> TestToken { + match protocol { + TestWireProtocol::CTAP1 => Self { + id, + protocol, + transport, + is_user_consenting, + has_user_verification, + is_user_verified, + has_resident_key, + u2f_impl: Some(SoftwareU2FToken::new()), + credentials: Vec::new(), + }, + _ => unreachable!(), + } + } + + pub fn insert_credential( + &mut self, + credential: &[u8], + privkey: &[u8], + rp_id: String, + is_resident_credential: bool, + user_handle: &[u8], + sign_count: u64, + ) { + let c = TestTokenCredential { + credential: credential.to_vec(), + privkey: privkey.to_vec(), + rp_id, + is_resident_credential, + user_handle: user_handle.to_vec(), + sign_count, + }; + + match self + .credentials + .binary_search_by_key(&credential, |probe| &probe.credential) + { + Ok(_) => {} + Err(idx) => self.credentials.insert(idx, c), + } + } + + pub fn delete_credential(&mut self, credential: &[u8]) -> bool { + debug!("Asking to delete credential",); + if let Ok(idx) = self + .credentials + .binary_search_by_key(&credential, |probe| &probe.credential) + { + debug!("Asking to delete credential from idx {}", idx); + self.credentials.remove(idx); + return true; + } + + false + } + + pub fn register(&self) -> crate::Result<RegisterResult> { + if self.u2f_impl.is_some() { + return self.u2f_impl.as_ref().unwrap().register( + RegisterFlags::empty(), + 10_000, + vec![0; 32], + vec![0; 32], + vec![], + ); + } + Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::Unknown, + )) + } + + pub fn sign(&self) -> crate::Result<SignResult> { + if self.u2f_impl.is_some() { + return self.u2f_impl.as_ref().unwrap().sign( + SignFlags::empty(), + 10_000, + vec![0; 32], + vec![vec![0; 32]], + vec![], + ); + } + Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::Unknown, + )) + } +} diff --git a/third_party/rust/authenticator/src/virtualdevices/webdriver/virtualmanager.rs b/third_party/rust/authenticator/src/virtualdevices/webdriver/virtualmanager.rs new file mode 100644 index 0000000000..31e5d09e3a --- /dev/null +++ b/third_party/rust/authenticator/src/virtualdevices/webdriver/virtualmanager.rs @@ -0,0 +1,151 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use runloop::RunLoop; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::ops::Deref; +use std::sync::mpsc::Sender; +use std::sync::{Arc, Mutex}; +use std::vec; +use std::{io, string, thread}; + +use crate::authenticatorservice::{AuthenticatorTransport, RegisterArgs, SignArgs}; +use crate::errors; +use crate::statecallback::StateCallback; +use crate::virtualdevices::webdriver::{testtoken, web_api}; + +pub struct VirtualManagerState { + pub authenticator_counter: u64, + pub tokens: vec::Vec<testtoken::TestToken>, +} + +impl VirtualManagerState { + pub fn new() -> Arc<Mutex<VirtualManagerState>> { + Arc::new(Mutex::new(VirtualManagerState { + authenticator_counter: 0, + tokens: vec![], + })) + } +} + +pub struct VirtualManager { + addr: SocketAddr, + state: Arc<Mutex<VirtualManagerState>>, + rloop: Option<RunLoop>, +} + +impl VirtualManager { + pub fn new() -> io::Result<Self> { + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080); + let state = VirtualManagerState::new(); + let stateclone = state.clone(); + + let builder = thread::Builder::new().name("WebDriver Command Server".into()); + builder.spawn(move || { + web_api::serve(stateclone, addr); + })?; + + Ok(Self { + addr, + state, + rloop: None, + }) + } + + pub fn url(&self) -> string::String { + format!("http://{}/webauthn/authenticator", &self.addr) + } +} + +impl AuthenticatorTransport for VirtualManager { + fn register( + &mut self, + timeout: u64, + _ctap_args: RegisterArgs, + _status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::RegisterResult>>, + ) -> crate::Result<()> { + if self.rloop.is_some() { + error!("WebDriver state error, prior operation never cancelled."); + return Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::Unknown, + )); + } + + let state = self.state.clone(); + let rloop = try_or!( + RunLoop::new_with_timeout( + move |alive| { + while alive() { + let state_obj = state.lock().unwrap(); + + for token in state_obj.tokens.deref() { + if token.is_user_consenting { + let register_result = token.register(); + thread::spawn(move || { + callback.call(register_result); + }); + return; + } + } + } + }, + timeout + ), + |_| Err(errors::AuthenticatorError::Platform) + ); + + self.rloop = Some(rloop); + Ok(()) + } + + fn sign( + &mut self, + timeout: u64, + _ctap_args: SignArgs, + _status: Sender<crate::StatusUpdate>, + callback: StateCallback<crate::Result<crate::SignResult>>, + ) -> crate::Result<()> { + if self.rloop.is_some() { + error!("WebDriver state error, prior operation never cancelled."); + return Err(errors::AuthenticatorError::U2FToken( + errors::U2FTokenError::Unknown, + )); + } + + let state = self.state.clone(); + let rloop = try_or!( + RunLoop::new_with_timeout( + move |alive| { + while alive() { + let state_obj = state.lock().unwrap(); + + for token in state_obj.tokens.deref() { + if token.is_user_consenting { + let sign_result = token.sign(); + thread::spawn(move || { + callback.call(sign_result); + }); + return; + } + } + } + }, + timeout + ), + |_| Err(errors::AuthenticatorError::Platform) + ); + + self.rloop = Some(rloop); + Ok(()) + } + + fn cancel(&mut self) -> crate::Result<()> { + if let Some(r) = self.rloop.take() { + debug!("WebDriver operation cancelled."); + r.cancel(); + } + Ok(()) + } +} diff --git a/third_party/rust/authenticator/src/virtualdevices/webdriver/web_api.rs b/third_party/rust/authenticator/src/virtualdevices/webdriver/web_api.rs new file mode 100644 index 0000000000..07bfc9f612 --- /dev/null +++ b/third_party/rust/authenticator/src/virtualdevices/webdriver/web_api.rs @@ -0,0 +1,964 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use serde::{Deserialize, Serialize}; +use std::net::SocketAddr; +use std::string; +use std::sync::{Arc, Mutex}; +use warp::Filter; + +use crate::virtualdevices::webdriver::{testtoken, virtualmanager::VirtualManagerState}; + +fn default_as_false() -> bool { + false +} +fn default_as_true() -> bool { + false +} + +#[derive(Serialize, Deserialize, Clone, PartialEq)] +pub struct AuthenticatorConfiguration { + protocol: string::String, + transport: string::String, + #[serde(rename = "hasResidentKey")] + #[serde(default = "default_as_false")] + has_resident_key: bool, + #[serde(rename = "hasUserVerification")] + #[serde(default = "default_as_false")] + has_user_verification: bool, + #[serde(rename = "isUserConsenting")] + #[serde(default = "default_as_true")] + is_user_consenting: bool, + #[serde(rename = "isUserVerified")] + #[serde(default = "default_as_false")] + is_user_verified: bool, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq)] +pub struct CredentialParameters { + #[serde(rename = "credentialId")] + credential_id: String, + #[serde(rename = "isResidentCredential")] + is_resident_credential: bool, + #[serde(rename = "rpId")] + rp_id: String, + #[serde(rename = "privateKey")] + private_key: String, + #[serde(rename = "userHandle")] + #[serde(default)] + user_handle: String, + #[serde(rename = "signCount")] + sign_count: u64, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq)] +pub struct UserVerificationParameters { + #[serde(rename = "isUserVerified")] + is_user_verified: bool, +} + +impl CredentialParameters { + fn new_from_test_token_credential(tc: &testtoken::TestTokenCredential) -> CredentialParameters { + let credential_id = base64::encode_config(&tc.credential, base64::URL_SAFE); + + let private_key = base64::encode_config(&tc.privkey, base64::URL_SAFE); + + let user_handle = base64::encode_config(&tc.user_handle, base64::URL_SAFE); + + CredentialParameters { + credential_id, + is_resident_credential: tc.is_resident_credential, + rp_id: tc.rp_id.clone(), + private_key, + user_handle, + sign_count: tc.sign_count, + } + } +} + +fn with_state( + state: Arc<Mutex<VirtualManagerState>>, +) -> impl Filter<Extract = (Arc<Mutex<VirtualManagerState>>,), Error = std::convert::Infallible> + Clone +{ + warp::any().map(move || state.clone()) +} + +fn authenticator_add( + state: Arc<Mutex<VirtualManagerState>>, +) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { + warp::path!("webauthn" / "authenticator") + .and(warp::post()) + .and(warp::body::json()) + .and(with_state(state)) + .and_then(handlers::authenticator_add) +} + +fn authenticator_delete( + state: Arc<Mutex<VirtualManagerState>>, +) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { + warp::path!("webauthn" / "authenticator" / u64) + .and(warp::delete()) + .and(with_state(state)) + .and_then(handlers::authenticator_delete) +} + +fn authenticator_set_uv( + state: Arc<Mutex<VirtualManagerState>>, +) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { + warp::path!("webauthn" / "authenticator" / u64 / "uv") + .and(warp::post()) + .and(warp::body::json()) + .and(with_state(state)) + .and_then(handlers::authenticator_set_uv) +} + +// This is not part of the specification, but it's useful for debugging +fn authenticator_get( + state: Arc<Mutex<VirtualManagerState>>, +) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { + warp::path!("webauthn" / "authenticator" / u64) + .and(warp::get()) + .and(with_state(state)) + .and_then(handlers::authenticator_get) +} + +fn authenticator_credential_add( + state: Arc<Mutex<VirtualManagerState>>, +) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { + warp::path!("webauthn" / "authenticator" / u64 / "credential") + .and(warp::post()) + .and(warp::body::json()) + .and(with_state(state)) + .and_then(handlers::authenticator_credential_add) +} + +fn authenticator_credential_delete( + state: Arc<Mutex<VirtualManagerState>>, +) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { + warp::path!("webauthn" / "authenticator" / u64 / "credentials" / String) + .and(warp::delete()) + .and(with_state(state)) + .and_then(handlers::authenticator_credential_delete) +} + +fn authenticator_credentials_get( + state: Arc<Mutex<VirtualManagerState>>, +) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { + warp::path!("webauthn" / "authenticator" / u64 / "credentials") + .and(warp::get()) + .and(with_state(state)) + .and_then(handlers::authenticator_credentials_get) +} + +fn authenticator_credentials_clear( + state: Arc<Mutex<VirtualManagerState>>, +) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { + warp::path!("webauthn" / "authenticator" / u64 / "credentials") + .and(warp::delete()) + .and(with_state(state)) + .and_then(handlers::authenticator_credentials_clear) +} + +mod handlers { + use super::{CredentialParameters, UserVerificationParameters}; + use crate::virtualdevices::webdriver::{ + testtoken, virtualmanager::VirtualManagerState, web_api::AuthenticatorConfiguration, + }; + use serde::Serialize; + use std::convert::Infallible; + use std::ops::DerefMut; + use std::sync::{Arc, Mutex}; + use std::vec; + use warp::http::{uri, StatusCode}; + + #[derive(Serialize)] + struct JsonSuccess {} + + impl JsonSuccess { + pub fn blank() -> JsonSuccess { + JsonSuccess {} + } + } + + #[derive(Serialize)] + struct JsonError { + #[serde(skip_serializing_if = "Option::is_none")] + line: Option<u32>, + error: String, + details: String, + } + + impl JsonError { + pub fn new(error: &str, line: u32, details: &str) -> JsonError { + JsonError { + details: details.to_string(), + error: error.to_string(), + line: Some(line), + } + } + pub fn from_status_code(code: StatusCode) -> JsonError { + JsonError { + details: code.canonical_reason().unwrap().to_string(), + line: None, + error: "".to_string(), + } + } + pub fn from_error(error: &str) -> JsonError { + JsonError { + details: "".to_string(), + error: error.to_string(), + line: None, + } + } + } + + macro_rules! reply_error { + ($status:expr) => { + warp::reply::with_status( + warp::reply::json(&JsonError::from_status_code($status)), + $status, + ) + }; + } + + macro_rules! try_json { + ($val:expr, $status:expr) => { + match $val { + Ok(v) => v, + Err(e) => { + return Ok(warp::reply::with_status( + warp::reply::json(&JsonError::new( + $status.canonical_reason().unwrap(), + line!(), + &e.to_string(), + )), + $status, + )); + } + } + }; + } + + pub fn validate_rp_id(rp_id: &str) -> crate::Result<()> { + if let Ok(uri) = rp_id.parse::<uri::Uri>().map_err(|_| { + crate::errors::AuthenticatorError::U2FToken(crate::errors::U2FTokenError::Unknown) + }) { + if uri.scheme().is_none() + && uri.path_and_query().is_none() + && uri.port().is_none() + && uri.host().is_some() + && uri.authority().unwrap() == uri.host().unwrap() + // Don't try too hard to ensure it's a valid domain, just + // ensure there's a label delim in there somewhere + && uri.host().unwrap().find('.').is_some() + { + return Ok(()); + } + } + Err(crate::errors::AuthenticatorError::U2FToken( + crate::errors::U2FTokenError::Unknown, + )) + } + + pub async fn authenticator_add( + auth: AuthenticatorConfiguration, + state: Arc<Mutex<VirtualManagerState>>, + ) -> Result<impl warp::Reply, Infallible> { + let protocol = match auth.protocol.as_str() { + "ctap1/u2f" => testtoken::TestWireProtocol::CTAP1, + "ctap2" => testtoken::TestWireProtocol::CTAP2, + _ => { + return Ok(warp::reply::with_status( + warp::reply::json(&JsonError::from_error( + format!("unknown protocol: {}", auth.protocol).as_str(), + )), + StatusCode::BAD_REQUEST, + )) + } + }; + + let mut state_lock = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR); + let mut state_obj = state_lock.deref_mut(); + state_obj.authenticator_counter += 1; + + let tt = testtoken::TestToken::new( + state_obj.authenticator_counter, + protocol, + auth.transport, + auth.is_user_consenting, + auth.has_user_verification, + auth.is_user_verified, + auth.has_resident_key, + ); + + match state_obj + .tokens + .binary_search_by_key(&state_obj.authenticator_counter, |probe| probe.id) + { + Ok(_) => panic!("unexpected repeat of authenticator_id"), + Err(idx) => state_obj.tokens.insert(idx, tt), + } + + #[derive(Serialize)] + struct AddResult { + #[serde(rename = "authenticatorId")] + authenticator_id: u64, + } + + Ok(warp::reply::with_status( + warp::reply::json(&AddResult { + authenticator_id: state_obj.authenticator_counter, + }), + StatusCode::CREATED, + )) + } + + pub async fn authenticator_delete( + id: u64, + state: Arc<Mutex<VirtualManagerState>>, + ) -> Result<impl warp::Reply, Infallible> { + let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR); + match state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) { + Ok(idx) => state_obj.tokens.remove(idx), + Err(_) => { + return Ok(reply_error!(StatusCode::NOT_FOUND)); + } + }; + + Ok(warp::reply::with_status( + warp::reply::json(&JsonSuccess::blank()), + StatusCode::OK, + )) + } + + pub async fn authenticator_get( + id: u64, + state: Arc<Mutex<VirtualManagerState>>, + ) -> Result<impl warp::Reply, Infallible> { + let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR); + if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) { + let tt = &mut state_obj.tokens[idx]; + + let data = AuthenticatorConfiguration { + protocol: tt.protocol.to_webdriver_string(), + transport: tt.transport.clone(), + has_resident_key: tt.has_resident_key, + has_user_verification: tt.has_user_verification, + is_user_consenting: tt.is_user_consenting, + is_user_verified: tt.is_user_verified, + }; + + return Ok(warp::reply::with_status( + warp::reply::json(&data), + StatusCode::OK, + )); + } + + Ok(reply_error!(StatusCode::NOT_FOUND)) + } + + pub async fn authenticator_set_uv( + id: u64, + uv: UserVerificationParameters, + state: Arc<Mutex<VirtualManagerState>>, + ) -> Result<impl warp::Reply, Infallible> { + let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR); + + if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) { + let tt = &mut state_obj.tokens[idx]; + tt.is_user_verified = uv.is_user_verified; + return Ok(warp::reply::with_status( + warp::reply::json(&JsonSuccess::blank()), + StatusCode::OK, + )); + } + + Ok(reply_error!(StatusCode::NOT_FOUND)) + } + + pub async fn authenticator_credential_add( + id: u64, + auth: CredentialParameters, + state: Arc<Mutex<VirtualManagerState>>, + ) -> Result<impl warp::Reply, Infallible> { + let credential = try_json!( + base64::decode_config(&auth.credential_id, base64::URL_SAFE), + StatusCode::BAD_REQUEST + ); + + let privkey = try_json!( + base64::decode_config(&auth.private_key, base64::URL_SAFE), + StatusCode::BAD_REQUEST + ); + + let userhandle = try_json!( + base64::decode_config(&auth.user_handle, base64::URL_SAFE), + StatusCode::BAD_REQUEST + ); + + try_json!(validate_rp_id(&auth.rp_id), StatusCode::BAD_REQUEST); + + let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR); + if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) { + let tt = &mut state_obj.tokens[idx]; + + tt.insert_credential( + &credential, + &privkey, + auth.rp_id, + auth.is_resident_credential, + &userhandle, + auth.sign_count, + ); + + return Ok(warp::reply::with_status( + warp::reply::json(&JsonSuccess::blank()), + StatusCode::CREATED, + )); + } + + Ok(reply_error!(StatusCode::NOT_FOUND)) + } + + pub async fn authenticator_credential_delete( + id: u64, + credential_id: String, + state: Arc<Mutex<VirtualManagerState>>, + ) -> Result<impl warp::Reply, Infallible> { + let credential = try_json!( + base64::decode_config(&credential_id, base64::URL_SAFE), + StatusCode::BAD_REQUEST + ); + + let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR); + + debug!("Asking to delete {}", &credential_id); + + if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) { + let tt = &mut state_obj.tokens[idx]; + debug!("Asking to delete from token {}", tt.id); + if tt.delete_credential(&credential) { + return Ok(warp::reply::with_status( + warp::reply::json(&JsonSuccess::blank()), + StatusCode::OK, + )); + } + } + + Ok(reply_error!(StatusCode::NOT_FOUND)) + } + + pub async fn authenticator_credentials_get( + id: u64, + state: Arc<Mutex<VirtualManagerState>>, + ) -> Result<impl warp::Reply, Infallible> { + let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR); + + if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) { + let tt = &mut state_obj.tokens[idx]; + let mut creds: vec::Vec<CredentialParameters> = vec![]; + for ttc in &tt.credentials { + creds.push(CredentialParameters::new_from_test_token_credential(ttc)); + } + + return Ok(warp::reply::with_status( + warp::reply::json(&creds), + StatusCode::OK, + )); + } + + Ok(reply_error!(StatusCode::NOT_FOUND)) + } + + pub async fn authenticator_credentials_clear( + id: u64, + state: Arc<Mutex<VirtualManagerState>>, + ) -> Result<impl warp::Reply, Infallible> { + let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR); + if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) { + let tt = &mut state_obj.tokens[idx]; + + tt.credentials.clear(); + + return Ok(warp::reply::with_status( + warp::reply::json(&JsonSuccess::blank()), + StatusCode::OK, + )); + } + + Ok(reply_error!(StatusCode::NOT_FOUND)) + } +} + +#[tokio::main] +pub async fn serve(state: Arc<Mutex<VirtualManagerState>>, addr: SocketAddr) { + let routes = authenticator_add(state.clone()) + .or(authenticator_delete(state.clone())) + .or(authenticator_get(state.clone())) + .or(authenticator_set_uv(state.clone())) + .or(authenticator_credential_add(state.clone())) + .or(authenticator_credential_delete(state.clone())) + .or(authenticator_credentials_get(state.clone())) + .or(authenticator_credentials_clear(state.clone())); + + warp::serve(routes).run(addr).await; +} + +#[cfg(test)] +mod tests { + use super::handlers::validate_rp_id; + use super::testtoken::*; + use super::*; + use crate::virtualdevices::webdriver::virtualmanager::VirtualManagerState; + use std::sync::{Arc, Mutex}; + use warp::http::StatusCode; + + fn init() { + let _ = env_logger::builder().is_test(true).try_init(); + } + + #[test] + fn test_validate_rp_id() { + init(); + + assert_matches!( + validate_rp_id(&String::from("http://example.com")), + Err(crate::errors::AuthenticatorError::U2FToken( + crate::errors::U2FTokenError::Unknown, + )) + ); + assert_matches!( + validate_rp_id(&String::from("https://example.com")), + Err(crate::errors::AuthenticatorError::U2FToken( + crate::errors::U2FTokenError::Unknown, + )) + ); + assert_matches!( + validate_rp_id(&String::from("example.com:443")), + Err(crate::errors::AuthenticatorError::U2FToken( + crate::errors::U2FTokenError::Unknown, + )) + ); + assert_matches!( + validate_rp_id(&String::from("example.com/path")), + Err(crate::errors::AuthenticatorError::U2FToken( + crate::errors::U2FTokenError::Unknown, + )) + ); + assert_matches!( + validate_rp_id(&String::from("example.com:443/path")), + Err(crate::errors::AuthenticatorError::U2FToken( + crate::errors::U2FTokenError::Unknown, + )) + ); + assert_matches!( + validate_rp_id(&String::from("user:pass@example.com")), + Err(crate::errors::AuthenticatorError::U2FToken( + crate::errors::U2FTokenError::Unknown, + )) + ); + assert_matches!( + validate_rp_id(&String::from("com")), + Err(crate::errors::AuthenticatorError::U2FToken( + crate::errors::U2FTokenError::Unknown, + )) + ); + assert_matches!(validate_rp_id(&String::from("example.com")), Ok(())); + } + + fn mk_state_with_token_list(ids: &[u64]) -> Arc<Mutex<VirtualManagerState>> { + let state = VirtualManagerState::new(); + + { + let mut state_obj = state.lock().unwrap(); + for id in ids { + state_obj.tokens.push(TestToken::new( + *id, + TestWireProtocol::CTAP1, + "internal".to_string(), + true, + true, + true, + true, + )); + } + + state_obj.tokens.sort_by_key(|probe| probe.id) + } + + state + } + + fn assert_success_rsp_blank(body: &warp::hyper::body::Bytes) { + assert_eq!(String::from_utf8_lossy(&body), r#"{}"#) + } + + fn assert_creds_equals_test_token_params( + a: &[CredentialParameters], + b: &[TestTokenCredential], + ) { + assert_eq!(a.len(), b.len()); + + for (i, j) in a.iter().zip(b.iter()) { + assert_eq!( + i.credential_id, + base64::encode_config(&j.credential, base64::URL_SAFE) + ); + assert_eq!( + i.user_handle, + base64::encode_config(&j.user_handle, base64::URL_SAFE) + ); + assert_eq!( + i.private_key, + base64::encode_config(&j.privkey, base64::URL_SAFE) + ); + assert_eq!(i.rp_id, j.rp_id); + assert_eq!(i.sign_count, j.sign_count); + assert_eq!(i.is_resident_credential, j.is_resident_credential); + } + } + + #[tokio::test] + async fn test_authenticator_add() { + init(); + let filter = authenticator_add(mk_state_with_token_list(&[])); + + { + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator") + .reply(&filter) + .await; + assert!(res.status().is_client_error()); + } + + let valid_add = AuthenticatorConfiguration { + protocol: "ctap1/u2f".to_string(), + transport: "usb".to_string(), + has_resident_key: false, + has_user_verification: false, + is_user_consenting: false, + is_user_verified: false, + }; + + { + let mut invalid = valid_add.clone(); + invalid.protocol = "unknown".to_string(); + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator") + .json(&invalid) + .reply(&filter) + .await; + assert!(res.status().is_client_error()); + assert!(String::from_utf8_lossy(&res.body()) + .contains(&String::from("unknown protocol: unknown"))); + } + + { + let mut unknown = valid_add.clone(); + unknown.transport = "unknown".to_string(); + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator") + .json(&unknown) + .reply(&filter) + .await; + assert!(res.status().is_success()); + assert_eq!( + String::from_utf8_lossy(&res.body()), + r#"{"authenticatorId":1}"# + ) + } + + { + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator") + .json(&valid_add) + .reply(&filter) + .await; + assert!(res.status().is_success()); + assert_eq!( + String::from_utf8_lossy(&res.body()), + r#"{"authenticatorId":2}"# + ) + } + } + + #[tokio::test] + async fn test_authenticator_delete() { + init(); + let filter = authenticator_delete(mk_state_with_token_list(&[32])); + + { + let res = warp::test::request() + .method("DELETE") + .path("/webauthn/authenticator/3") + .reply(&filter) + .await; + assert!(res.status().is_client_error()); + } + + { + let res = warp::test::request() + .method("DELETE") + .path("/webauthn/authenticator/32") + .reply(&filter) + .await; + assert!(res.status().is_success()); + assert_success_rsp_blank(res.body()); + } + + { + let res = warp::test::request() + .method("DELETE") + .path("/webauthn/authenticator/42") + .reply(&filter) + .await; + assert!(res.status().is_client_error()); + } + } + + #[tokio::test] + async fn test_authenticator_change_uv() { + init(); + let state = mk_state_with_token_list(&[1]); + let filter = authenticator_set_uv(state.clone()); + + { + let state_obj = state.lock().unwrap(); + assert_eq!(true, state_obj.tokens[0].is_user_verified); + } + + { + // Empty POST is bad + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator/1/uv") + .reply(&filter) + .await; + assert!(res.status().is_client_error()); + } + + { + // Unexpected POST structure is bad + #[derive(Serialize)] + struct Unexpected { + id: u64, + } + let unexpected = Unexpected { id: 4 }; + + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator/1/uv") + .json(&unexpected) + .reply(&filter) + .await; + assert!(res.status().is_client_error()); + } + + { + let param_false = UserVerificationParameters { + is_user_verified: false, + }; + + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator/1/uv") + .json(¶m_false) + .reply(&filter) + .await; + assert_eq!(res.status(), 200); + assert_success_rsp_blank(res.body()); + + let state_obj = state.lock().unwrap(); + assert_eq!(false, state_obj.tokens[0].is_user_verified); + } + + { + let param_false = UserVerificationParameters { + is_user_verified: true, + }; + + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator/1/uv") + .json(¶m_false) + .reply(&filter) + .await; + assert_eq!(res.status(), 200); + assert_success_rsp_blank(res.body()); + + let state_obj = state.lock().unwrap(); + assert_eq!(true, state_obj.tokens[0].is_user_verified); + } + } + + #[tokio::test] + async fn test_authenticator_credentials() { + init(); + let state = mk_state_with_token_list(&[1]); + let filter = authenticator_credential_add(state.clone()) + .or(authenticator_credential_delete(state.clone())) + .or(authenticator_credentials_get(state.clone())) + .or(authenticator_credentials_clear(state.clone())); + + let valid_add_credential = CredentialParameters { + credential_id: r"c3VwZXIgcmVhZGVy".to_string(), + is_resident_credential: true, + rp_id: "valid.rpid".to_string(), + private_key: base64::encode_config(b"hello internet~", base64::URL_SAFE), + user_handle: base64::encode_config(b"hello internet~", base64::URL_SAFE), + sign_count: 0, + }; + + { + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator/1/credential") + .reply(&filter) + .await; + assert!(res.status().is_client_error()); + } + + { + let mut invalid = valid_add_credential.clone(); + invalid.credential_id = "!@#$ invalid base64".to_string(); + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator/1/credential") + .json(&invalid) + .reply(&filter) + .await; + assert!(res.status().is_client_error()); + } + + { + let mut invalid = valid_add_credential.clone(); + invalid.rp_id = "example".to_string(); + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator/1/credential") + .json(&invalid) + .reply(&filter) + .await; + assert!(res.status().is_client_error()); + } + + { + let mut invalid = valid_add_credential.clone(); + invalid.rp_id = "https://example.com".to_string(); + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator/1/credential") + .json(&invalid) + .reply(&filter) + .await; + assert!(res.status().is_client_error()); + + let state_obj = state.lock().unwrap(); + assert_eq!(0, state_obj.tokens[0].credentials.len()); + } + + { + let mut no_user_handle = valid_add_credential.clone(); + no_user_handle.user_handle = "".to_string(); + no_user_handle.credential_id = "YQo=".to_string(); + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator/1/credential") + .json(&no_user_handle) + .reply(&filter) + .await; + assert!(res.status().is_success()); + assert_success_rsp_blank(res.body()); + + let state_obj = state.lock().unwrap(); + assert_eq!(1, state_obj.tokens[0].credentials.len()); + let c = &state_obj.tokens[0].credentials[0]; + assert!(c.user_handle.is_empty()); + } + + { + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator/1/credential") + .json(&valid_add_credential) + .reply(&filter) + .await; + assert_eq!(res.status(), StatusCode::CREATED); + assert_success_rsp_blank(res.body()); + + let state_obj = state.lock().unwrap(); + assert_eq!(2, state_obj.tokens[0].credentials.len()); + let c = &state_obj.tokens[0].credentials[1]; + assert!(!c.user_handle.is_empty()); + } + + { + // Duplicate, should still be two credentials + let res = warp::test::request() + .method("POST") + .path("/webauthn/authenticator/1/credential") + .json(&valid_add_credential) + .reply(&filter) + .await; + assert_eq!(res.status(), StatusCode::CREATED); + assert_success_rsp_blank(res.body()); + + let state_obj = state.lock().unwrap(); + assert_eq!(2, state_obj.tokens[0].credentials.len()); + } + + { + let res = warp::test::request() + .method("GET") + .path("/webauthn/authenticator/1/credentials") + .reply(&filter) + .await; + assert_eq!(res.status(), 200); + let (_, body) = res.into_parts(); + let cred = serde_json::de::from_slice::<Vec<CredentialParameters>>(&body).unwrap(); + + let state_obj = state.lock().unwrap(); + assert_creds_equals_test_token_params(&cred, &state_obj.tokens[0].credentials); + } + + { + let res = warp::test::request() + .method("DELETE") + .path("/webauthn/authenticator/1/credentials/YmxhbmsK") + .reply(&filter) + .await; + assert_eq!(res.status(), 404); + } + + { + let res = warp::test::request() + .method("DELETE") + .path("/webauthn/authenticator/1/credentials/c3VwZXIgcmVhZGVy") + .reply(&filter) + .await; + assert_eq!(res.status(), 200); + assert_success_rsp_blank(res.body()); + + let state_obj = state.lock().unwrap(); + assert_eq!(1, state_obj.tokens[0].credentials.len()); + } + + { + let res = warp::test::request() + .method("DELETE") + .path("/webauthn/authenticator/1/credentials") + .reply(&filter) + .await; + assert!(res.status().is_success()); + assert_success_rsp_blank(res.body()); + + let state_obj = state.lock().unwrap(); + assert_eq!(0, state_obj.tokens[0].credentials.len()); + } + } +} |