From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../rust/authenticator/src/authenticatorservice.rs | 642 ++++++++ third_party/rust/authenticator/src/consts.rs | 151 ++ third_party/rust/authenticator/src/crypto/der.rs | 185 +++ third_party/rust/authenticator/src/crypto/dummy.rs | 57 + third_party/rust/authenticator/src/crypto/mod.rs | 1638 ++++++++++++++++++++ third_party/rust/authenticator/src/crypto/nss.rs | 481 ++++++ .../rust/authenticator/src/crypto/openssl.rs | 183 +++ .../rust/authenticator/src/ctap2/attestation.rs | 1129 ++++++++++++++ .../rust/authenticator/src/ctap2/client_data.rs | 339 ++++ .../src/ctap2/commands/authenticator_config.rs | 225 +++ .../src/ctap2/commands/bio_enrollment.rs | 660 ++++++++ .../authenticator/src/ctap2/commands/client_pin.rs | 851 ++++++++++ .../src/ctap2/commands/credential_management.rs | 457 ++++++ .../src/ctap2/commands/get_assertion.rs | 1494 ++++++++++++++++++ .../authenticator/src/ctap2/commands/get_info.rs | 1054 +++++++++++++ .../src/ctap2/commands/get_next_assertion.rs | 54 + .../src/ctap2/commands/get_version.rs | 121 ++ .../src/ctap2/commands/make_credentials.rs | 1061 +++++++++++++ .../rust/authenticator/src/ctap2/commands/mod.rs | 477 ++++++ .../rust/authenticator/src/ctap2/commands/reset.rs | 123 ++ .../authenticator/src/ctap2/commands/selection.rs | 123 ++ third_party/rust/authenticator/src/ctap2/mod.rs | 1518 ++++++++++++++++++ .../rust/authenticator/src/ctap2/preflight.rs | 530 +++++++ third_party/rust/authenticator/src/ctap2/server.rs | 629 ++++++++ third_party/rust/authenticator/src/ctap2/utils.rs | 39 + third_party/rust/authenticator/src/errors.rs | 135 ++ third_party/rust/authenticator/src/lib.rs | 113 ++ third_party/rust/authenticator/src/manager.rs | 218 +++ .../rust/authenticator/src/statecallback.rs | 166 ++ third_party/rust/authenticator/src/statemachine.rs | 427 +++++ .../rust/authenticator/src/status_update.rs | 117 ++ .../authenticator/src/transport/device_selector.rs | 477 ++++++ .../rust/authenticator/src/transport/errors.rs | 98 ++ .../authenticator/src/transport/freebsd/device.rs | 235 +++ .../authenticator/src/transport/freebsd/mod.rs | 9 + .../authenticator/src/transport/freebsd/monitor.rs | 161 ++ .../src/transport/freebsd/transaction.rs | 69 + .../authenticator/src/transport/freebsd/uhid.rs | 89 ++ .../rust/authenticator/src/transport/hid.rs | 252 +++ .../rust/authenticator/src/transport/hidproto.rs | 257 +++ .../authenticator/src/transport/linux/device.rs | 181 +++ .../authenticator/src/transport/linux/hidraw.rs | 80 + .../authenticator/src/transport/linux/hidwrapper.h | 12 + .../src/transport/linux/hidwrapper.rs | 54 + .../src/transport/linux/ioctl_aarch64le.rs | 5 + .../src/transport/linux/ioctl_armle.rs | 5 + .../src/transport/linux/ioctl_loongarch64.rs | 5 + .../src/transport/linux/ioctl_mips64le.rs | 5 + .../src/transport/linux/ioctl_mipsbe.rs | 5 + .../src/transport/linux/ioctl_mipsle.rs | 5 + .../src/transport/linux/ioctl_powerpc64be.rs | 5 + .../src/transport/linux/ioctl_powerpc64le.rs | 5 + .../src/transport/linux/ioctl_powerpcbe.rs | 5 + .../src/transport/linux/ioctl_riscv64.rs | 5 + .../src/transport/linux/ioctl_s390xbe.rs | 5 + .../authenticator/src/transport/linux/ioctl_x86.rs | 5 + .../src/transport/linux/ioctl_x86_64.rs | 5 + .../rust/authenticator/src/transport/linux/mod.rs | 12 + .../authenticator/src/transport/linux/monitor.rs | 194 +++ .../src/transport/linux/transaction.rs | 69 + .../authenticator/src/transport/macos/device.rs | 226 +++ .../authenticator/src/transport/macos/iokit.rs | 292 ++++ .../rust/authenticator/src/transport/macos/mod.rs | 9 + .../authenticator/src/transport/macos/monitor.rs | 212 +++ .../src/transport/macos/transaction.rs | 107 ++ .../authenticator/src/transport/mock/device.rs | 328 ++++ .../rust/authenticator/src/transport/mock/mod.rs | 6 + .../src/transport/mock/transaction.rs | 35 + .../rust/authenticator/src/transport/mod.rs | 369 +++++ .../authenticator/src/transport/netbsd/device.rs | 248 +++ .../rust/authenticator/src/transport/netbsd/fd.rs | 62 + .../rust/authenticator/src/transport/netbsd/mod.rs | 10 + .../authenticator/src/transport/netbsd/monitor.rs | 132 ++ .../src/transport/netbsd/transaction.rs | 69 + .../authenticator/src/transport/netbsd/uhid.rs | 77 + .../authenticator/src/transport/openbsd/device.rs | 224 +++ .../authenticator/src/transport/openbsd/mod.rs | 8 + .../authenticator/src/transport/openbsd/monitor.rs | 138 ++ .../src/transport/openbsd/transaction.rs | 69 + .../authenticator/src/transport/stub/device.rs | 115 ++ .../rust/authenticator/src/transport/stub/mod.rs | 11 + .../src/transport/stub/transaction.rs | 52 + .../authenticator/src/transport/windows/device.rs | 173 +++ .../authenticator/src/transport/windows/mod.rs | 9 + .../authenticator/src/transport/windows/monitor.rs | 125 ++ .../src/transport/windows/transaction.rs | 69 + .../authenticator/src/transport/windows/winapi.rs | 263 ++++ third_party/rust/authenticator/src/u2ftypes.rs | 341 ++++ third_party/rust/authenticator/src/util.rs | 76 + 89 files changed, 21566 insertions(+) create mode 100644 third_party/rust/authenticator/src/authenticatorservice.rs create mode 100644 third_party/rust/authenticator/src/consts.rs create mode 100644 third_party/rust/authenticator/src/crypto/der.rs create mode 100644 third_party/rust/authenticator/src/crypto/dummy.rs create mode 100644 third_party/rust/authenticator/src/crypto/mod.rs create mode 100644 third_party/rust/authenticator/src/crypto/nss.rs create mode 100644 third_party/rust/authenticator/src/crypto/openssl.rs create mode 100644 third_party/rust/authenticator/src/ctap2/attestation.rs create mode 100644 third_party/rust/authenticator/src/ctap2/client_data.rs create mode 100644 third_party/rust/authenticator/src/ctap2/commands/authenticator_config.rs create mode 100644 third_party/rust/authenticator/src/ctap2/commands/bio_enrollment.rs create mode 100644 third_party/rust/authenticator/src/ctap2/commands/client_pin.rs create mode 100644 third_party/rust/authenticator/src/ctap2/commands/credential_management.rs create mode 100644 third_party/rust/authenticator/src/ctap2/commands/get_assertion.rs create mode 100644 third_party/rust/authenticator/src/ctap2/commands/get_info.rs create mode 100644 third_party/rust/authenticator/src/ctap2/commands/get_next_assertion.rs create mode 100644 third_party/rust/authenticator/src/ctap2/commands/get_version.rs create mode 100644 third_party/rust/authenticator/src/ctap2/commands/make_credentials.rs create mode 100644 third_party/rust/authenticator/src/ctap2/commands/mod.rs create mode 100644 third_party/rust/authenticator/src/ctap2/commands/reset.rs create mode 100644 third_party/rust/authenticator/src/ctap2/commands/selection.rs create mode 100644 third_party/rust/authenticator/src/ctap2/mod.rs create mode 100644 third_party/rust/authenticator/src/ctap2/preflight.rs create mode 100644 third_party/rust/authenticator/src/ctap2/server.rs create mode 100644 third_party/rust/authenticator/src/ctap2/utils.rs create mode 100644 third_party/rust/authenticator/src/errors.rs create mode 100644 third_party/rust/authenticator/src/lib.rs create mode 100644 third_party/rust/authenticator/src/manager.rs create mode 100644 third_party/rust/authenticator/src/statecallback.rs create mode 100644 third_party/rust/authenticator/src/statemachine.rs create mode 100644 third_party/rust/authenticator/src/status_update.rs create mode 100644 third_party/rust/authenticator/src/transport/device_selector.rs create mode 100644 third_party/rust/authenticator/src/transport/errors.rs create mode 100644 third_party/rust/authenticator/src/transport/freebsd/device.rs create mode 100644 third_party/rust/authenticator/src/transport/freebsd/mod.rs create mode 100644 third_party/rust/authenticator/src/transport/freebsd/monitor.rs create mode 100644 third_party/rust/authenticator/src/transport/freebsd/transaction.rs create mode 100644 third_party/rust/authenticator/src/transport/freebsd/uhid.rs create mode 100644 third_party/rust/authenticator/src/transport/hid.rs create mode 100644 third_party/rust/authenticator/src/transport/hidproto.rs create mode 100644 third_party/rust/authenticator/src/transport/linux/device.rs create mode 100644 third_party/rust/authenticator/src/transport/linux/hidraw.rs create mode 100644 third_party/rust/authenticator/src/transport/linux/hidwrapper.h create mode 100644 third_party/rust/authenticator/src/transport/linux/hidwrapper.rs create mode 100644 third_party/rust/authenticator/src/transport/linux/ioctl_aarch64le.rs create mode 100644 third_party/rust/authenticator/src/transport/linux/ioctl_armle.rs create mode 100644 third_party/rust/authenticator/src/transport/linux/ioctl_loongarch64.rs create mode 100644 third_party/rust/authenticator/src/transport/linux/ioctl_mips64le.rs create mode 100644 third_party/rust/authenticator/src/transport/linux/ioctl_mipsbe.rs create mode 100644 third_party/rust/authenticator/src/transport/linux/ioctl_mipsle.rs create mode 100644 third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64be.rs create mode 100644 third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64le.rs create mode 100644 third_party/rust/authenticator/src/transport/linux/ioctl_powerpcbe.rs create mode 100644 third_party/rust/authenticator/src/transport/linux/ioctl_riscv64.rs create mode 100644 third_party/rust/authenticator/src/transport/linux/ioctl_s390xbe.rs create mode 100644 third_party/rust/authenticator/src/transport/linux/ioctl_x86.rs create mode 100644 third_party/rust/authenticator/src/transport/linux/ioctl_x86_64.rs create mode 100644 third_party/rust/authenticator/src/transport/linux/mod.rs create mode 100644 third_party/rust/authenticator/src/transport/linux/monitor.rs create mode 100644 third_party/rust/authenticator/src/transport/linux/transaction.rs create mode 100644 third_party/rust/authenticator/src/transport/macos/device.rs create mode 100644 third_party/rust/authenticator/src/transport/macos/iokit.rs create mode 100644 third_party/rust/authenticator/src/transport/macos/mod.rs create mode 100644 third_party/rust/authenticator/src/transport/macos/monitor.rs create mode 100644 third_party/rust/authenticator/src/transport/macos/transaction.rs create mode 100644 third_party/rust/authenticator/src/transport/mock/device.rs create mode 100644 third_party/rust/authenticator/src/transport/mock/mod.rs create mode 100644 third_party/rust/authenticator/src/transport/mock/transaction.rs create mode 100644 third_party/rust/authenticator/src/transport/mod.rs create mode 100644 third_party/rust/authenticator/src/transport/netbsd/device.rs create mode 100644 third_party/rust/authenticator/src/transport/netbsd/fd.rs create mode 100644 third_party/rust/authenticator/src/transport/netbsd/mod.rs create mode 100644 third_party/rust/authenticator/src/transport/netbsd/monitor.rs create mode 100644 third_party/rust/authenticator/src/transport/netbsd/transaction.rs create mode 100644 third_party/rust/authenticator/src/transport/netbsd/uhid.rs create mode 100644 third_party/rust/authenticator/src/transport/openbsd/device.rs create mode 100644 third_party/rust/authenticator/src/transport/openbsd/mod.rs create mode 100644 third_party/rust/authenticator/src/transport/openbsd/monitor.rs create mode 100644 third_party/rust/authenticator/src/transport/openbsd/transaction.rs create mode 100644 third_party/rust/authenticator/src/transport/stub/device.rs create mode 100644 third_party/rust/authenticator/src/transport/stub/mod.rs create mode 100644 third_party/rust/authenticator/src/transport/stub/transaction.rs create mode 100644 third_party/rust/authenticator/src/transport/windows/device.rs create mode 100644 third_party/rust/authenticator/src/transport/windows/mod.rs create mode 100644 third_party/rust/authenticator/src/transport/windows/monitor.rs create mode 100644 third_party/rust/authenticator/src/transport/windows/transaction.rs create mode 100644 third_party/rust/authenticator/src/transport/windows/winapi.rs create mode 100644 third_party/rust/authenticator/src/u2ftypes.rs create mode 100644 third_party/rust/authenticator/src/util.rs (limited to 'third_party/rust/authenticator/src') diff --git a/third_party/rust/authenticator/src/authenticatorservice.rs b/third_party/rust/authenticator/src/authenticatorservice.rs new file mode 100644 index 0000000000..e5935150ed --- /dev/null +++ b/third_party/rust/authenticator/src/authenticatorservice.rs @@ -0,0 +1,642 @@ +/* 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::ctap2::commands::client_pin::Pin; +use crate::ctap2::server::{ + AuthenticationExtensionsClientInputs, PublicKeyCredentialDescriptor, + PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, RelyingParty, + ResidentKeyRequirement, UserVerificationRequirement, +}; +use crate::errors::*; +use crate::manager::Manager; +use crate::statecallback::StateCallback; +use std::sync::{mpsc::Sender, Arc, Mutex}; + +#[derive(Debug, Clone)] +pub struct RegisterArgs { + pub client_data_hash: [u8; 32], + pub relying_party: RelyingParty, + pub origin: String, + pub user: PublicKeyCredentialUserEntity, + pub pub_cred_params: Vec, + pub exclude_list: Vec, + pub user_verification_req: UserVerificationRequirement, + pub resident_key_req: ResidentKeyRequirement, + pub extensions: AuthenticationExtensionsClientInputs, + pub pin: Option, + pub use_ctap1_fallback: bool, +} + +#[derive(Debug, Clone)] +pub struct SignArgs { + pub client_data_hash: [u8; 32], + pub origin: String, + pub relying_party_id: String, + pub allow_list: Vec, + pub user_verification_req: UserVerificationRequirement, + pub user_presence_req: bool, + pub extensions: AuthenticationExtensionsClientInputs, + pub pin: Option, + pub use_ctap1_fallback: bool, +} + +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, + callback: StateCallback>, + ) -> 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, + callback: StateCallback>, + ) -> crate::Result<()>; + + fn cancel(&mut self) -> crate::Result<()>; + fn reset( + &mut self, + timeout: u64, + status: Sender, + callback: StateCallback>, + ) -> crate::Result<()>; + fn set_pin( + &mut self, + timeout: u64, + new_pin: Pin, + status: Sender, + callback: StateCallback>, + ) -> crate::Result<()>; + fn manage( + &mut self, + timeout: u64, + status: Sender, + callback: StateCallback>, + ) -> crate::Result<()>; +} + +pub struct AuthenticatorService { + transports: Vec>>>, +} + +fn clone_and_configure_cancellation_callback( + mut callback: StateCallback, + transports_to_cancel: Vec>>>, +) -> StateCallback { + 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() -> crate::Result { + Ok(Self { + transports: Vec::new(), + }) + } + + /// Add any detected platform transports + pub fn add_detected_transports(&mut self) { + self.add_u2f_usb_hid_platform_transports(); + } + + pub fn add_transport(&mut self, boxed_token: Box) { + self.transports.push(Arc::new(Mutex::new(boxed_token))) + } + + pub fn add_u2f_usb_hid_platform_transports(&mut self) { + match Manager::new() { + Ok(token) => self.add_transport(Box::new(token)), + Err(e) => error!("Could not add CTAP2 HID transport: {}", e), + } + } + + pub fn register( + &mut self, + timeout: u64, + args: RegisterArgs, + status: Sender, + callback: StateCallback>, + ) -> 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, + args.clone(), + status.clone(), + clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel), + )?; + } + + Ok(()) + } + + pub fn sign( + &mut self, + timeout: u64, + args: SignArgs, + status: Sender, + callback: StateCallback>, + ) -> 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, + 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, + callback: StateCallback>, + ) -> 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, + callback: StateCallback>, + ) -> 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(()) + } + + pub fn manage( + &mut self, + timeout: u64, + status: Sender, + callback: StateCallback>, + ) -> crate::Result<()> { + let iterable_transports = self.transports.clone(); + if iterable_transports.is_empty() { + return Err(AuthenticatorError::NoConfiguredTransports); + } + + debug!( + "Manage 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().manage( + timeout, + status.clone(), + clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel), + )?; + } + + Ok(()) + } +} + +//////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////// + +#[cfg(test)] +mod tests { + use super::{AuthenticatorService, AuthenticatorTransport, Pin, RegisterArgs, SignArgs}; + use crate::consts::PARAMETER_SIZE; + use crate::ctap2::server::{ + PublicKeyCredentialUserEntity, RelyingParty, ResidentKeyRequirement, + UserVerificationRequirement, + }; + use crate::errors::AuthenticatorError; + use crate::statecallback::StateCallback; + use crate::StatusUpdate; + 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, + } + + impl TestTransportDriver { + pub fn new(consent: bool) -> io::Result { + Ok(Self { + consent, + was_cancelled: Arc::new(AtomicBool::new(false)), + }) + } + } + + impl AuthenticatorTransport for TestTransportDriver { + fn register( + &mut self, + _timeout: u64, + _args: RegisterArgs, + _status: Sender, + callback: StateCallback>, + ) -> crate::Result<()> { + if self.consent { + // The value we send is ignored, and this is easier than constructing a + // RegisterResult + let rv = Err(AuthenticatorError::Platform); + thread::spawn(move || callback.call(rv)); + } + Ok(()) + } + + fn sign( + &mut self, + _timeout: u64, + _ctap_args: SignArgs, + _status: Sender, + callback: StateCallback>, + ) -> crate::Result<()> { + if self.consent { + // The value we send is ignored, and this is easier than constructing a + // RegisterResult + let rv = Err(AuthenticatorError::Platform); + 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, + _callback: StateCallback>, + ) -> crate::Result<()> { + unimplemented!(); + } + + fn set_pin( + &mut self, + _timeout: u64, + _new_pin: Pin, + _status: Sender, + _callback: StateCallback>, + ) -> crate::Result<()> { + unimplemented!(); + } + + fn manage( + &mut self, + _timeout: u64, + _status: Sender, + _callback: StateCallback>, + ) -> crate::Result<()> { + unimplemented!(); + } + } + + fn mk_challenge() -> [u8; PARAMETER_SIZE] { + [0x11; PARAMETER_SIZE] + } + + #[test] + fn test_no_transports() { + init(); + let (status_tx, _) = channel::(); + + let mut s = AuthenticatorService::new().unwrap(); + assert_matches!( + s.register( + 1_000, + RegisterArgs { + client_data_hash: mk_challenge(), + relying_party: RelyingParty { + id: "example.com".to_string(), + name: None, + }, + origin: "example.com".to_string(), + user: PublicKeyCredentialUserEntity { + id: "user_id".as_bytes().to_vec(), + name: Some("A. User".to_string()), + display_name: None, + }, + pub_cred_params: vec![], + exclude_list: vec![], + user_verification_req: UserVerificationRequirement::Preferred, + resident_key_req: ResidentKeyRequirement::Preferred, + extensions: Default::default(), + pin: None, + use_ctap1_fallback: false, + }, + status_tx.clone(), + StateCallback::new(Box::new(move |_rv| {})), + ) + .unwrap_err(), + crate::errors::AuthenticatorError::NoConfiguredTransports + ); + + assert_matches!( + s.sign( + 1_000, + SignArgs { + client_data_hash: mk_challenge(), + origin: "example.com".to_string(), + relying_party_id: "example.com".to_string(), + allow_list: vec![], + user_verification_req: UserVerificationRequirement::Preferred, + user_presence_req: true, + extensions: Default::default(), + pin: None, + use_ctap1_fallback: false, + }, + 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::(); + + let mut s = AuthenticatorService::new().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, + RegisterArgs { + client_data_hash: mk_challenge(), + relying_party: RelyingParty { + id: "example.com".to_string(), + name: None, + }, + origin: "example.com".to_string(), + user: PublicKeyCredentialUserEntity { + id: "user_id".as_bytes().to_vec(), + name: Some("A. User".to_string()), + display_name: None, + }, + pub_cred_params: vec![], + exclude_list: vec![], + user_verification_req: UserVerificationRequirement::Preferred, + resident_key_req: ResidentKeyRequirement::Preferred, + extensions: Default::default(), + pin: None, + use_ctap1_fallback: false, + }, + status_tx, + callback.clone(), + ) + .is_ok()); + callback.wait(); + + assert!(!was_cancelled_one.load(Ordering::SeqCst)); + assert!(was_cancelled_two.load(Ordering::SeqCst)); + assert!(was_cancelled_three.load(Ordering::SeqCst)); + } + + #[test] + fn test_cancellation_sign() { + init(); + let (status_tx, _) = channel::(); + + let mut s = AuthenticatorService::new().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, + SignArgs { + client_data_hash: mk_challenge(), + origin: "example.com".to_string(), + relying_party_id: "example.com".to_string(), + allow_list: vec![], + user_verification_req: UserVerificationRequirement::Preferred, + user_presence_req: true, + extensions: Default::default(), + pin: None, + use_ctap1_fallback: false, + }, + status_tx, + callback.clone(), + ) + .is_ok()); + callback.wait(); + + assert!(!was_cancelled_one.load(Ordering::SeqCst)); + assert!(was_cancelled_two.load(Ordering::SeqCst)); + assert!(was_cancelled_three.load(Ordering::SeqCst)); + } + + #[test] + fn test_cancellation_race() { + init(); + let (status_tx, _) = channel::(); + + let mut s = AuthenticatorService::new().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, + RegisterArgs { + client_data_hash: mk_challenge(), + relying_party: RelyingParty { + id: "example.com".to_string(), + name: None, + }, + origin: "example.com".to_string(), + user: PublicKeyCredentialUserEntity { + id: "user_id".as_bytes().to_vec(), + name: Some("A. User".to_string()), + display_name: None, + }, + pub_cred_params: vec![], + exclude_list: vec![], + user_verification_req: UserVerificationRequirement::Preferred, + resident_key_req: ResidentKeyRequirement::Preferred, + extensions: Default::default(), + pin: None, + use_ctap1_fallback: false, + }, + 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!( + one ^ two, + "asserting that one={} xor two={} is true", + one, + two + ); + } +} diff --git a/third_party/rust/authenticator/src/consts.rs b/third_party/rust/authenticator/src/consts.rs new file mode 100644 index 0000000000..3579d85ee7 --- /dev/null +++ b/third_party/rust/authenticator/src/consts.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/. */ + +// 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 (Le). +/// +/// Fields `CLA`, `INS`, `P1` and `P2` are 1 byte each, and Le is 3 +/// bytes. If there is a data payload, add 2 bytes (Lc is 3 bytes, +/// and Le 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 From for u8 { + fn from(v: HIDCmd) -> u8 { + match v { + 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 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 +pub const U2F_DONT_ENFORCE_USER_PRESENCE_AND_SIGN: u8 = 0x08; // Sign, but don't verify user presence + +// 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; + } +} + +// 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/der.rs b/third_party/rust/authenticator/src/crypto/der.rs new file mode 100644 index 0000000000..39c5e0b676 --- /dev/null +++ b/third_party/rust/authenticator/src/crypto/der.rs @@ -0,0 +1,185 @@ +use super::CryptoError; + +pub const TAG_INTEGER: u8 = 0x02; +pub const TAG_BIT_STRING: u8 = 0x03; +#[cfg(all(test, feature = "crypto_nss"))] +pub const TAG_OCTET_STRING: u8 = 0x04; +pub const TAG_NULL: u8 = 0x05; +pub const TAG_OBJECT_ID: u8 = 0x06; +pub const TAG_SEQUENCE: u8 = 0x30; + +// Object identifiers in DER tag-length-value form +pub const OID_EC_PUBLIC_KEY_BYTES: &[u8] = &[ + /* RFC 5480 (id-ecPublicKey) */ + 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, +]; +pub const OID_SECP256R1_BYTES: &[u8] = &[ + /* RFC 5480 (secp256r1) */ + 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, +]; +pub const OID_ED25519_BYTES: &[u8] = &[/* RFC 8410 (id-ed25519) */ 0x2b, 0x65, 0x70]; +pub const OID_RS256_BYTES: &[u8] = &[ + /* RFC 4055 (sha256WithRSAEncryption) */ + 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, +]; + +pub type Result = std::result::Result; + +const MAX_TAG_AND_LENGTH_BYTES: usize = 4; +fn write_tag_and_length(out: &mut Vec, tag: u8, len: usize) -> Result<()> { + if len > 0xFFFF { + return Err(CryptoError::LibraryFailure); + } + out.push(tag); + if len > 0xFF { + out.push(0x82); + out.push((len >> 8) as u8); + out.push(len as u8); + } else if len > 0x7F { + out.push(0x81); + out.push(len as u8); + } else { + out.push(len as u8); + } + Ok(()) +} + +pub fn integer(val: &[u8]) -> Result> { + if val.is_empty() { + return Err(CryptoError::MalformedInput); + } + // trim leading zeros, leaving a single zero if the input is the zero vector. + let mut val = val; + while val.len() > 1 && val[0] == 0 { + val = &val[1..]; + } + let mut out = Vec::with_capacity(MAX_TAG_AND_LENGTH_BYTES + 1 + val.len()); + if val[0] & 0x80 != 0 { + // needs zero prefix + write_tag_and_length(&mut out, TAG_INTEGER, 1 + val.len())?; + out.push(0x00); + out.extend_from_slice(val); + } else { + write_tag_and_length(&mut out, TAG_INTEGER, val.len())?; + out.extend_from_slice(val); + } + Ok(out) +} + +pub fn bit_string(val: &[u8]) -> Result> { + let mut out = Vec::with_capacity(MAX_TAG_AND_LENGTH_BYTES + 1 + val.len()); + write_tag_and_length(&mut out, TAG_BIT_STRING, 1 + val.len())?; + out.push(0x00); // trailing bits aren't supported + out.extend_from_slice(val); + Ok(out) +} + +pub fn null() -> Result> { + let mut out = Vec::with_capacity(MAX_TAG_AND_LENGTH_BYTES); + write_tag_and_length(&mut out, TAG_NULL, 0)?; + Ok(out) +} + +pub fn object_id(val: &[u8]) -> Result> { + let mut out = Vec::with_capacity(MAX_TAG_AND_LENGTH_BYTES + val.len()); + write_tag_and_length(&mut out, TAG_OBJECT_ID, val.len())?; + out.extend_from_slice(val); + Ok(out) +} + +pub fn sequence(items: &[&[u8]]) -> Result> { + let len = items.iter().map(|i| i.len()).sum(); + let mut out = Vec::with_capacity(MAX_TAG_AND_LENGTH_BYTES + len); + write_tag_and_length(&mut out, TAG_SEQUENCE, len)?; + for item in items { + out.extend_from_slice(item); + } + Ok(out) +} + +#[cfg(all(test, feature = "crypto_nss"))] +pub fn octet_string(val: &[u8]) -> Result> { + let mut out = Vec::with_capacity(MAX_TAG_AND_LENGTH_BYTES + val.len()); + write_tag_and_length(&mut out, TAG_OCTET_STRING, val.len())?; + out.extend_from_slice(val); + Ok(out) +} + +#[cfg(all(test, feature = "crypto_nss"))] +pub fn context_specific_explicit_tag(tag: u8, content: &[u8]) -> Result> { + let mut out = Vec::with_capacity(MAX_TAG_AND_LENGTH_BYTES + content.len()); + write_tag_and_length(&mut out, 0xa0 + tag, content.len())?; + out.extend_from_slice(content); + Ok(out) +} + +// Given "tag || len || value || rest" where tag and len are of length one, len is in [0, 127], +// and value is of length len, returns (value, rest) +#[cfg(all(test, feature = "crypto_nss"))] +fn expect_tag_with_short_len(tag: u8, z: &[u8]) -> Result<(&[u8], &[u8])> { + if z.is_empty() { + return Err(CryptoError::MalformedInput); + } + let (h, z) = z.split_at(1); + if h[0] != tag || z.is_empty() { + return Err(CryptoError::MalformedInput); + } + let (h, z) = z.split_at(1); + if h[0] >= 0x80 || h[0] as usize > z.len() { + return Err(CryptoError::MalformedInput); + } + Ok(z.split_at(h[0] as usize)) +} + +// Given a DER encoded RFC 3279 Ecdsa-Sig-Value, +// Ecdsa-Sig-Value ::= SEQUENCE { +// r INTEGER, +// s INTEGER }, +// with r and s < 2^256, returns a 64 byte array containing +// r and s encoded as 32 byte zero-padded big endian unsigned +// integers +#[cfg(all(test, feature = "crypto_nss"))] +pub fn read_p256_sig(z: &[u8]) -> Result> { + // Strip the tag and length. + let (z, rest) = expect_tag_with_short_len(TAG_SEQUENCE, z)?; + + // The input should not have any trailing data. + if !rest.is_empty() { + return Err(CryptoError::MalformedInput); + } + + let read_u256 = |z| -> Result<(&[u8], &[u8])> { + let (r, z) = expect_tag_with_short_len(TAG_INTEGER, z)?; + // We're expecting r < 2^256, so no more than 33 bytes as a signed integer. + if r.is_empty() || r.len() > 33 { + return Err(CryptoError::MalformedInput); + } + // If it is 33 bytes the leading byte must be zero. + if r.len() == 33 && r[0] != 0 { + return Err(CryptoError::MalformedInput); + } + // Ensure r is no more than 32 bytes. + if r.len() == 33 { + Ok((&r[1..], z)) + } else { + Ok((r, z)) + } + }; + + let (r, z) = read_u256(z)?; + let (s, z) = read_u256(z)?; + + // We should have consumed the entire buffer + if !z.is_empty() { + return Err(CryptoError::MalformedInput); + } + + // Left pad each integer with zeros to length 32 and concatenate the results + let mut out = vec![0u8; 64]; + { + let (r_out, s_out) = out.split_at_mut(32); + r_out[32 - r.len()..].copy_from_slice(r); + s_out[32 - s.len()..].copy_from_slice(s); + } + Ok(out) +} 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..9d16856958 --- /dev/null +++ b/third_party/rust/authenticator/src/crypto/dummy.rs @@ -0,0 +1,57 @@ +use super::CryptoError; + +/* +This is a dummy implementation for CI, to avoid having to install NSS or openSSL in the CI-pipeline +*/ + +pub type Result = std::result::Result; + +pub fn ecdhe_p256_raw(_peer_spki: &[u8]) -> Result<(Vec, Vec)> { + unimplemented!() +} + +pub fn encrypt_aes_256_cbc_no_pad( + _key: &[u8], + _iv: Option<&[u8]>, + _data: &[u8], +) -> Result> { + unimplemented!() +} + +pub fn decrypt_aes_256_cbc_no_pad( + _key: &[u8], + _iv: Option<&[u8]>, + _data: &[u8], +) -> Result> { + unimplemented!() +} + +pub fn hmac_sha256(_key: &[u8], _data: &[u8]) -> Result> { + unimplemented!() +} + +pub fn sha256(_data: &[u8]) -> Result> { + unimplemented!() +} + +pub fn random_bytes(_count: usize) -> Result> { + unimplemented!() +} + +pub fn gen_p256() -> Result<(Vec, Vec)> { + unimplemented!() +} + +pub fn ecdsa_p256_sha256_sign_raw(_private: &[u8], _data: &[u8]) -> Result> { + unimplemented!() +} + +#[allow(dead_code)] +#[cfg(test)] +pub fn test_ecdsa_p256_sha256_verify_raw( + _public: &[u8], + _signature: &[u8], + _data: &[u8], +) -> Result<()> { + 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..fd74030ed7 --- /dev/null +++ b/third_party/rust/authenticator/src/crypto/mod.rs @@ -0,0 +1,1638 @@ +/* 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::ctap2::commands::client_pin::PinUvAuthTokenPermission; +use crate::ctap2::commands::get_info::AuthenticatorInfo; +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 std::convert::TryFrom; +use std::fmt; + +#[cfg(feature = "crypto_nss")] +mod nss; +#[cfg(feature = "crypto_nss")] +use nss as backend; + +#[cfg(feature = "crypto_openssl")] +mod openssl; +#[cfg(feature = "crypto_openssl")] +use self::openssl as backend; + +#[cfg(feature = "crypto_dummy")] +mod dummy; +#[cfg(feature = "crypto_dummy")] +use dummy as backend; + +use backend::{ + decrypt_aes_256_cbc_no_pad, ecdhe_p256_raw, encrypt_aes_256_cbc_no_pad, gen_p256, hmac_sha256, + random_bytes, sha256, +}; + +mod der; + +pub use backend::ecdsa_p256_sha256_sign_raw; + +pub struct PinUvAuthProtocol(Box); +impl PinUvAuthProtocol { + pub fn id(&self) -> u64 { + self.0.protocol_id() + } + pub fn encapsulate(&self, peer_cose_key: &COSEKey) -> Result { + self.0.encapsulate(peer_cose_key) + } +} + +/// The output of `PinUvAuthProtocol::encapsulate` is supposed to be used with the same +/// PinProtocolImpl. So we stash a copy of the calling PinUvAuthProtocol in the output SharedSecret. +/// We need a trick here to tell the compiler that every PinProtocolImpl we define will implement +/// Clone. +trait ClonablePinProtocolImpl { + fn clone_box(&self) -> Box; +} + +impl ClonablePinProtocolImpl for T +where + T: 'static + PinProtocolImpl + Clone + Send + Sync, +{ + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +impl Clone for PinUvAuthProtocol { + fn clone(&self) -> Self { + PinUvAuthProtocol(self.0.as_ref().clone_box()) + } +} + +/// CTAP 2.1, Section 6.5.4. PIN/UV Auth Protocol Abstract Definition +trait PinProtocolImpl: ClonablePinProtocolImpl { + fn protocol_id(&self) -> u64; + fn initialize(&self); + fn encrypt(&self, key: &[u8], plaintext: &[u8]) -> Result, CryptoError>; + fn decrypt(&self, key: &[u8], ciphertext: &[u8]) -> Result, CryptoError>; + fn authenticate(&self, key: &[u8], message: &[u8]) -> Result, CryptoError>; + fn kdf(&self, z: &[u8]) -> Result, CryptoError>; + fn encapsulate(&self, peer_cose_key: &COSEKey) -> Result { + // [CTAP 2.1] + // encapsulate(peerCoseKey) → (coseKey, sharedSecret) | error + // 1) Let sharedSecret be the result of calling ecdh(peerCoseKey). Return any + // resulting error. + // 2) Return (getPublicKey(), sharedSecret) + // + // ecdh(peerCoseKey) → sharedSecret | error + // Parse peerCoseKey as specified for getPublicKey, below, and produce a P-256 + // point, Y. If unsuccessful, or if the resulting point is not on the curve, return + // error. Calculate xY, the shared point. (I.e. the scalar-multiplication of the + // peer's point, Y, with the local private key agreement key.) Let Z be the + // 32-byte, big-endian encoding of the x-coordinate of the shared point. Return + // kdf(Z). + + match peer_cose_key.alg { + // There is no COSEAlgorithm for ECDHE with the KDF used here. Section 6.5.6. of CTAP + // 2.1 says to use value -25 (= ECDH_ES_HKDF256) even though "this is not the algorithm + // actually used". + COSEAlgorithm::ECDH_ES_HKDF256 => (), + other => return Err(CryptoError::UnsupportedAlgorithm(other)), + } + + let peer_cose_ec2_key = match peer_cose_key.key { + COSEKeyType::EC2(ref key) => key, + _ => return Err(CryptoError::UnsupportedKeyType), + }; + + let peer_spki = peer_cose_ec2_key.der_spki()?; + + let (shared_point, client_public_sec1) = ecdhe_p256_raw(&peer_spki)?; + + let client_cose_ec2_key = + COSEEC2Key::from_sec1_uncompressed(Curve::SECP256R1, &client_public_sec1)?; + + let client_cose_key = COSEKey { + alg: COSEAlgorithm::ECDH_ES_HKDF256, + key: COSEKeyType::EC2(client_cose_ec2_key), + }; + + let shared_secret = SharedSecret { + pin_protocol: PinUvAuthProtocol(self.clone_box()), + key: self.kdf(&shared_point)?, + inputs: PublicInputs { + peer: peer_cose_key.clone(), + client: client_cose_key, + }, + }; + + Ok(shared_secret) + } +} + +impl TryFrom<&AuthenticatorInfo> for PinUvAuthProtocol { + type Error = CommandError; + + fn try_from(info: &AuthenticatorInfo) -> Result { + // CTAP 2.1, Section 6.5.5.4 + // "If there are multiple mutually supported protocols, and the platform + // has no preference, it SHOULD select the one listed first in + // pinUvAuthProtocols." + if let Some(pin_protocols) = &info.pin_protocols { + for proto_id in pin_protocols.iter() { + match proto_id { + 1 => return Ok(PinUvAuthProtocol(Box::new(PinUvAuth1 {}))), + 2 => return Ok(PinUvAuthProtocol(Box::new(PinUvAuth2 {}))), + _ => continue, + } + } + } else { + match info.max_supported_version() { + crate::ctap2::commands::get_info::AuthenticatorVersion::U2F_V2 => { + return Err(CommandError::UnsupportedPinProtocol) + } + crate::ctap2::commands::get_info::AuthenticatorVersion::FIDO_2_0 => { + return Ok(PinUvAuthProtocol(Box::new(PinUvAuth1 {}))) + } + crate::ctap2::commands::get_info::AuthenticatorVersion::FIDO_2_1_PRE + | crate::ctap2::commands::get_info::AuthenticatorVersion::FIDO_2_1 => { + return Ok(PinUvAuthProtocol(Box::new(PinUvAuth2 {}))) + } + } + } + Err(CommandError::UnsupportedPinProtocol) + } +} + +impl fmt::Debug for PinUvAuthProtocol { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PinUvAuthProtocol") + .field("id", &self.id()) + .finish() + } +} + +/// CTAP 2.1, Section 6.5.6. +#[derive(Copy, Clone)] +pub struct PinUvAuth1; + +impl PinProtocolImpl for PinUvAuth1 { + fn protocol_id(&self) -> u64 { + 1 + } + + fn initialize(&self) {} + + fn encrypt(&self, key: &[u8], plaintext: &[u8]) -> Result, CryptoError> { + // [CTAP 2.1] + // encrypt(key, demPlaintext) → ciphertext + // Return the AES-256-CBC encryption of plaintext using an all-zero IV. (No padding is + // performed as the size of plaintext is required to be a multiple of the AES block + // length.) + encrypt_aes_256_cbc_no_pad(key, None, plaintext) + } + + fn decrypt(&self, key: &[u8], ciphertext: &[u8]) -> Result, CryptoError> { + // [CTAP 2.1] + // decrypt(key, demCiphertext) → plaintext | error + // If the size of ciphertext is not a multiple of the AES block length, return error. + // Otherwise return the AES-256-CBC decryption of ciphertext using an all-zero IV. + decrypt_aes_256_cbc_no_pad(key, None, ciphertext) + } + + fn authenticate(&self, key: &[u8], message: &[u8]) -> Result, CryptoError> { + // [CTAP 2.1] + // authenticate(key, message) → signature + // Return the first 16 bytes of the result of computing HMAC-SHA-256 with the given + // key and message. + let mut hmac = hmac_sha256(key, message)?; + hmac.truncate(16); + Ok(hmac) + } + + fn kdf(&self, z: &[u8]) -> Result, CryptoError> { + // kdf(Z) → sharedSecret + // Return SHA-256(Z) + sha256(z) + } +} + +/// CTAP 2.1, Section 6.5.7. +#[derive(Copy, Clone)] +pub struct PinUvAuth2; + +impl PinProtocolImpl for PinUvAuth2 { + fn protocol_id(&self) -> u64 { + 2 + } + + fn initialize(&self) {} + + fn encrypt(&self, key: &[u8], plaintext: &[u8]) -> Result, CryptoError> { + // [CTAP 2.1] + // encrypt(key, demPlaintext) → ciphertext + // 1. Discard the first 32 bytes of key. (This selects the AES-key portion of the + // shared secret.) + // 2. Let iv be a 16-byte, random bytestring. + // 3. Let ct be the AES-256-CBC encryption of demPlaintext using key and iv. (No + // padding is performed as the size of demPlaintext is required to be a multiple of + // the AES block length.) + // 4. Return iv || ct. + if key.len() != 64 { + return Err(CryptoError::LibraryFailure); + } + let key = &key[32..64]; + + let iv = random_bytes(16)?; + let mut ct = encrypt_aes_256_cbc_no_pad(key, Some(&iv), plaintext)?; + + let mut out = iv; + out.append(&mut ct); + Ok(out) + } + + fn decrypt(&self, key: &[u8], ciphertext: &[u8]) -> Result, CryptoError> { + // decrypt(key, demCiphertext) → plaintext | error + // 1. Discard the first 32 bytes of key. (This selects the AES-key portion of the + // shared secret.) + // 2. If demCiphertext is less than 16 bytes in length, return an error + // 3. Split demCiphertext after the 16th byte to produce two subspans, iv and ct. + // 4. Return the AES-256-CBC decryption of ct using key and iv. + if key.len() < 64 || ciphertext.len() < 16 { + return Err(CryptoError::LibraryFailure); + } + let key = &key[32..64]; + let (iv, ct) = ciphertext.split_at(16); + decrypt_aes_256_cbc_no_pad(key, Some(iv), ct) + } + + fn authenticate(&self, key: &[u8], message: &[u8]) -> Result, CryptoError> { + // authenticate(key, message) → signature + // 1. If key is longer than 32 bytes, discard the excess. (This selects the HMAC-key + // portion of the shared secret. When key is the pinUvAuthToken, it is exactly 32 + // bytes long and thus this step has no effect.) + // 2. Return the result of computing HMAC-SHA-256 on key and message. + if key.len() < 32 { + return Err(CryptoError::LibraryFailure); + } + let key = &key[0..32]; + hmac_sha256(key, message) + } + + fn kdf(&self, z: &[u8]) -> Result, CryptoError> { + // kdf(Z) → sharedSecret + // return HKDF-SHA-256(salt, Z, L = 32, info = "CTAP2 HMAC key") || + // HKDF-SHA-256(salt, Z, L = 32, info = "CTAP2 AES key") + // where salt = [0u8; 32]. + // + // From Section 2 of RFC 5869, we have + // HKDF(salt, Z, 32, info) = + // HKDF-Expand(HKDF-Extract(salt, Z), info || 0x01) + // + // And for HKDF-SHA256 both Extract and Expand are instantiated with HMAC-SHA256. + + let prk = hmac_sha256(&[0u8; 32], z)?; + let mut shared_secret = hmac_sha256(&prk, "CTAP2 HMAC key\x01".as_bytes())?; + shared_secret.append(&mut hmac_sha256(&prk, "CTAP2 AES key\x01".as_bytes())?); + Ok(shared_secret) + } +} + +#[derive(Clone, Debug)] +struct PublicInputs { + client: COSEKey, + peer: COSEKey, +} + +#[derive(Clone, Debug)] +pub struct SharedSecret { + pub pin_protocol: PinUvAuthProtocol, + key: Vec, + inputs: PublicInputs, +} + +impl SharedSecret { + pub fn encrypt(&self, plaintext: &[u8]) -> Result, CryptoError> { + self.pin_protocol.0.encrypt(&self.key, plaintext) + } + pub fn decrypt(&self, ciphertext: &[u8]) -> Result, CryptoError> { + self.pin_protocol.0.decrypt(&self.key, ciphertext) + } + pub fn decrypt_pin_token( + &self, + permissions: PinUvAuthTokenPermission, + encrypted_pin_token: &[u8], + ) -> Result { + let pin_token = self.decrypt(encrypted_pin_token)?; + Ok(PinUvAuthToken { + pin_protocol: self.pin_protocol.clone(), + pin_token, + permissions, + }) + } + pub fn authenticate(&self, message: &[u8]) -> Result, CryptoError> { + self.pin_protocol.0.authenticate(&self.key, message) + } + pub fn client_input(&self) -> &COSEKey { + &self.inputs.client + } + pub fn peer_input(&self) -> &COSEKey { + &self.inputs.peer + } +} + +#[derive(Clone, Debug)] +pub struct PinUvAuthToken { + pub pin_protocol: PinUvAuthProtocol, + pin_token: Vec, + pub permissions: PinUvAuthTokenPermission, +} + +impl PinUvAuthToken { + pub fn derive(self, message: &[u8]) -> Result { + let pin_auth = self.pin_protocol.0.authenticate(&self.pin_token, message)?; + Ok(PinUvAuthParam { + pin_auth, + pin_protocol: self.pin_protocol, + permissions: self.permissions, + }) + } +} + +#[derive(Clone, Debug)] +pub struct PinUvAuthParam { + pin_auth: Vec, + pub pin_protocol: PinUvAuthProtocol, + #[allow(dead_code)] // Not yet used + permissions: PinUvAuthTokenPermission, +} + +impl PinUvAuthParam { + pub(crate) fn create_empty() -> Self { + let pin_protocol = PinUvAuthProtocol(Box::new(PinUvAuth1 {})); + Self { + pin_auth: vec![], + pin_protocol, + permissions: PinUvAuthTokenPermission::empty(), + } + } +} + +impl Serialize for PinUvAuthParam { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serde_bytes::serialize(&self.pin_auth[..], serializer) + } +} + +/// A Curve 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)] +pub enum Curve { + // +---------+-------+----------+------------------------------------+ + // | 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 Serialize for Curve { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_i64(*self as i64) + } +} + +impl TryFrom for Curve { + type Error = CryptoError; + fn try_from(i: i64) -> Result { + match i { + i if i == Curve::SECP256R1 as i64 => Ok(Curve::SECP256R1), + i if i == Curve::SECP384R1 as i64 => Ok(Curve::SECP384R1), + i if i == Curve::SECP521R1 as i64 => Ok(Curve::SECP521R1), + i if i == Curve::X25519 as i64 => Ok(Curve::X25519), + i if i == Curve::X448 as i64 => Ok(Curve::X448), + i if i == Curve::Ed25519 as i64 => Ok(Curve::Ed25519), + i if i == Curve::Ed448 as i64 => Ok(Curve::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 +#[rustfmt::skip] +#[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(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_i64(*self as i64) + } +} + +impl<'de> Deserialize<'de> for COSEAlgorithm { + fn deserialize(deserializer: D) -> Result + 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(self, v: i64) -> Result + where + E: SerdeError, + { + COSEAlgorithm::try_from(v).map_err(|_| { + SerdeError::invalid_value(Unexpected::Signed(v), &"valid COSEAlgorithm") + }) + } + } + + deserializer.deserialize_any(COSEAlgorithmVisitor) + } +} + +impl TryFrom for COSEAlgorithm { + type Error = CryptoError; + fn try_from(i: i64) -> Result { + match i { + i if i == COSEAlgorithm::RS512 as i64 => Ok(COSEAlgorithm::RS512), + i if i == COSEAlgorithm::RS384 as i64 => Ok(COSEAlgorithm::RS384), + i if i == COSEAlgorithm::RS256 as i64 => Ok(COSEAlgorithm::RS256), + i if i == COSEAlgorithm::ES256K as i64 => Ok(COSEAlgorithm::ES256K), + i if i == COSEAlgorithm::HSS_LMS as i64 => Ok(COSEAlgorithm::HSS_LMS), + i if i == COSEAlgorithm::SHAKE256 as i64 => Ok(COSEAlgorithm::SHAKE256), + i if i == COSEAlgorithm::SHA512 as i64 => Ok(COSEAlgorithm::SHA512), + i if i == COSEAlgorithm::SHA384 as i64 => Ok(COSEAlgorithm::SHA384), + i if i == COSEAlgorithm::RSAES_OAEP_SHA_512 as i64 => { + Ok(COSEAlgorithm::RSAES_OAEP_SHA_512) + } + i if i == COSEAlgorithm::RSAES_OAEP_SHA_256 as i64 => { + Ok(COSEAlgorithm::RSAES_OAEP_SHA_256) + } + i if i == COSEAlgorithm::RSAES_OAEP_RFC_8017_default as i64 => { + Ok(COSEAlgorithm::RSAES_OAEP_RFC_8017_default) + } + i if i == COSEAlgorithm::PS512 as i64 => Ok(COSEAlgorithm::PS512), + i if i == COSEAlgorithm::PS384 as i64 => Ok(COSEAlgorithm::PS384), + i if i == COSEAlgorithm::PS256 as i64 => Ok(COSEAlgorithm::PS256), + i if i == COSEAlgorithm::ES512 as i64 => Ok(COSEAlgorithm::ES512), + i if i == COSEAlgorithm::ES384 as i64 => Ok(COSEAlgorithm::ES384), + i if i == COSEAlgorithm::ECDH_SS_A256KW as i64 => Ok(COSEAlgorithm::ECDH_SS_A256KW), + i if i == COSEAlgorithm::ECDH_SS_A192KW as i64 => Ok(COSEAlgorithm::ECDH_SS_A192KW), + i if i == COSEAlgorithm::ECDH_SS_A128KW as i64 => Ok(COSEAlgorithm::ECDH_SS_A128KW), + i if i == COSEAlgorithm::ECDH_ES_A256KW as i64 => Ok(COSEAlgorithm::ECDH_ES_A256KW), + i if i == COSEAlgorithm::ECDH_ES_A192KW as i64 => Ok(COSEAlgorithm::ECDH_ES_A192KW), + i if i == COSEAlgorithm::ECDH_ES_A128KW as i64 => Ok(COSEAlgorithm::ECDH_ES_A128KW), + i if i == COSEAlgorithm::ECDH_SS_HKDF512 as i64 => Ok(COSEAlgorithm::ECDH_SS_HKDF512), + i if i == COSEAlgorithm::ECDH_SS_HKDF256 as i64 => Ok(COSEAlgorithm::ECDH_SS_HKDF256), + i if i == COSEAlgorithm::ECDH_ES_HKDF512 as i64 => Ok(COSEAlgorithm::ECDH_ES_HKDF512), + i if i == COSEAlgorithm::ECDH_ES_HKDF256 as i64 => Ok(COSEAlgorithm::ECDH_ES_HKDF256), + i if i == COSEAlgorithm::SHAKE128 as i64 => Ok(COSEAlgorithm::SHAKE128), + i if i == COSEAlgorithm::SHA512_256 as i64 => Ok(COSEAlgorithm::SHA512_256), + i if i == COSEAlgorithm::SHA256 as i64 => Ok(COSEAlgorithm::SHA256), + i if i == COSEAlgorithm::SHA256_64 as i64 => Ok(COSEAlgorithm::SHA256_64), + i if i == COSEAlgorithm::SHA1 as i64 => Ok(COSEAlgorithm::SHA1), + i if i == COSEAlgorithm::Direct_HKDF_AES256 as i64 => { + Ok(COSEAlgorithm::Direct_HKDF_AES256) + } + i if i == COSEAlgorithm::Direct_HKDF_AES128 as i64 => { + Ok(COSEAlgorithm::Direct_HKDF_AES128) + } + i if i == COSEAlgorithm::Direct_HKDF_SHA512 as i64 => { + Ok(COSEAlgorithm::Direct_HKDF_SHA512) + } + i if i == COSEAlgorithm::Direct_HKDF_SHA256 as i64 => { + Ok(COSEAlgorithm::Direct_HKDF_SHA256) + } + i if i == COSEAlgorithm::EDDSA as i64 => Ok(COSEAlgorithm::EDDSA), + i if i == COSEAlgorithm::ES256 as i64 => Ok(COSEAlgorithm::ES256), + i if i == COSEAlgorithm::Direct as i64 => Ok(COSEAlgorithm::Direct), + i if i == COSEAlgorithm::A256KW as i64 => Ok(COSEAlgorithm::A256KW), + i if i == COSEAlgorithm::A192KW as i64 => Ok(COSEAlgorithm::A192KW), + i if i == COSEAlgorithm::A128KW as i64 => Ok(COSEAlgorithm::A128KW), + i if i == COSEAlgorithm::A128GCM as i64 => Ok(COSEAlgorithm::A128GCM), + i if i == COSEAlgorithm::A192GCM as i64 => Ok(COSEAlgorithm::A192GCM), + i if i == COSEAlgorithm::A256GCM as i64 => Ok(COSEAlgorithm::A256GCM), + i if i == COSEAlgorithm::HMAC256_64 as i64 => Ok(COSEAlgorithm::HMAC256_64), + i if i == COSEAlgorithm::HMAC256_256 as i64 => Ok(COSEAlgorithm::HMAC256_256), + i if i == COSEAlgorithm::HMAC384_384 as i64 => Ok(COSEAlgorithm::HMAC384_384), + i if i == COSEAlgorithm::HMAC512_512 as i64 => Ok(COSEAlgorithm::HMAC512_512), + i if i == COSEAlgorithm::AES_CCM_16_64_128 as i64 => { + Ok(COSEAlgorithm::AES_CCM_16_64_128) + } + i if i == COSEAlgorithm::AES_CCM_16_64_256 as i64 => { + Ok(COSEAlgorithm::AES_CCM_16_64_256) + } + i if i == COSEAlgorithm::AES_CCM_64_64_128 as i64 => { + Ok(COSEAlgorithm::AES_CCM_64_64_128) + } + i if i == COSEAlgorithm::AES_CCM_64_64_256 as i64 => { + Ok(COSEAlgorithm::AES_CCM_64_64_256) + } + i if i == COSEAlgorithm::AES_MAC_128_64 as i64 => Ok(COSEAlgorithm::AES_MAC_128_64), + i if i == COSEAlgorithm::AES_MAC_256_64 as i64 => Ok(COSEAlgorithm::AES_MAC_256_64), + i if i == COSEAlgorithm::ChaCha20_Poly1305 as i64 => { + Ok(COSEAlgorithm::ChaCha20_Poly1305) + } + i if i == COSEAlgorithm::AES_MAC_128_128 as i64 => Ok(COSEAlgorithm::AES_MAC_128_128), + i if i == COSEAlgorithm::AES_MAC_256_128 as i64 => Ok(COSEAlgorithm::AES_MAC_256_128), + i if i == COSEAlgorithm::AES_CCM_16_128_128 as i64 => { + Ok(COSEAlgorithm::AES_CCM_16_128_128) + } + i if i == COSEAlgorithm::AES_CCM_16_128_256 as i64 => { + Ok(COSEAlgorithm::AES_CCM_16_128_256) + } + i if i == COSEAlgorithm::AES_CCM_64_128_128 as i64 => { + Ok(COSEAlgorithm::AES_CCM_64_128_128) + } + i if i == COSEAlgorithm::AES_CCM_64_128_256 as i64 => { + Ok(COSEAlgorithm::AES_CCM_64_128_256) + } + i if i == COSEAlgorithm::IV_GENERATION as i64 => Ok(COSEAlgorithm::IV_GENERATION), + i if i == COSEAlgorithm::INSECURE_RS1 as i64 => 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: Curve, + /// The key's public X coordinate. + pub x: Vec, + /// The key's public Y coordinate. + pub y: Vec, +} + +impl COSEEC2Key { + // The SEC 1 uncompressed point format is "0x04 || x coordinate || y coordinate". + // See Section 2.3.3 of "SEC 1: Elliptic Curve Cryptography" https://www.secg.org/sec1-v2.pdf. + pub fn from_sec1_uncompressed(curve: Curve, key: &[u8]) -> Result { + if !(curve == Curve::SECP256R1 && key.len() == 65) { + return Err(CryptoError::UnsupportedCurve(curve)); + } + if key[0] != 0x04 { + return Err(CryptoError::MalformedInput); + } + let key = &key[1..]; + let (x, y) = key.split_at(key.len() / 2); + Ok(COSEEC2Key { + curve, + x: x.to_vec(), + y: y.to_vec(), + }) + } + + pub fn der_spki(&self) -> Result, CryptoError> { + if self.curve != Curve::SECP256R1 { + return Err(CryptoError::UnsupportedCurve(self.curve)); + } + + // SubjectPublicKeyInfo + der::sequence(&[ + // algorithm: AlgorithmIdentifier + &der::sequence(&[ + // algorithm + &der::object_id(der::OID_EC_PUBLIC_KEY_BYTES)?, + // parameters + &der::object_id(der::OID_SECP256R1_BYTES)?, + ])?, + // subjectPublicKey + &der::bit_string( + // SEC 1 uncompressed format + &[&[0x04], self.x.as_slice(), self.y.as_slice()].concat(), + )?, + ]) + } +} + +/// 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: Curve, + /// The key's public X coordinate. + pub x: Vec, +} + +impl COSEOKPKey { + pub fn der_spki(&self) -> Result, CryptoError> { + if self.curve != Curve::Ed25519 { + return Err(CryptoError::UnsupportedCurve(self.curve)); + } + + // SubjectPublicKeyInfo + der::sequence(&[ + // algorithm: AlgorithmIdentifier + &der::sequence(&[ + // algorithm + &der::object_id(der::OID_ED25519_BYTES)?, + // parameters + // (absent as per RFC 8410) + ])?, + // subjectPublicKey + &der::bit_string( + // RFC 8410 + self.x.as_slice(), + )?, + ]) + } +} + +/// A COSE RSA PublicKey. This is a provided credential from a registered authenticator. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct COSERSAKey { + /// An RSA modulus + pub n: Vec, + /// An RSA exponent + pub e: Vec, +} + +impl COSERSAKey { + pub fn der_spki(&self) -> Result, CryptoError> { + // SubjectPublicKeyInfo + der::sequence(&[ + // algorithm: AlgorithmIdentifier + &der::sequence(&[ + // algorithm + &der::object_id(der::OID_RS256_BYTES)?, + // parameters + &der::null()?, + ])?, + // subjectPublicKey + &der::bit_string( + // RFC 4055 RSAPublicKey + &der::sequence(&[&der::integer(&self.n)?, &der::integer(&self.e)?])?, + )?, + ]) + } +} + +// https://tools.ietf.org/html/rfc8152#section-13 +#[allow(non_camel_case_types)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +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, +} + +impl Serialize for COSEKeyTypeId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_i64(*self as i64) + } +} + +impl TryFrom for COSEKeyTypeId { + type Error = CryptoError; + fn try_from(i: i64) -> Result { + match i { + i if i == COSEKeyTypeId::OKP as i64 => Ok(COSEKeyTypeId::OKP), + i if i == COSEKeyTypeId::EC2 as i64 => Ok(COSEKeyTypeId::EC2), + i if i == COSEKeyTypeId::RSA as i64 => Ok(COSEKeyTypeId::RSA), + _ => 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 { + /// Identifies this as an Elliptic Curve EC2 key + EC2(COSEEC2Key), + /// Identifies this as an Elliptic Curve octet key pair + OKP(COSEOKPKey), + /// Identifies this as an RSA key + RSA(COSERSAKey), +} + +/// 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 COSEKey { + /// Generates a new key pair for the specified algorithm. + /// Returns an PKCS#8 encoding of the private key, and the public key as a COSEKey. + pub fn generate(alg: COSEAlgorithm) -> Result<(Vec, Self), CryptoError> { + if alg != COSEAlgorithm::ES256 && alg != COSEAlgorithm::ECDH_ES_HKDF256 { + return Err(CryptoError::UnsupportedAlgorithm(alg)); + } + let (private, public) = gen_p256()?; + let cose_ec2_key = COSEEC2Key::from_sec1_uncompressed(Curve::SECP256R1, &public)?; + let public = COSEKey { + alg, + key: COSEKeyType::EC2(cose_ec2_key), + }; + Ok((private, public)) + } + + pub fn der_spki(&self) -> Result, CryptoError> { + match &self.key { + COSEKeyType::EC2(ec2_key) => ec2_key.der_spki(), + COSEKeyType::OKP(okp_key) => okp_key.der_spki(), + COSEKeyType::RSA(rsa_key) => rsa_key.der_spki(), + } + } +} + +impl<'de> Deserialize<'de> for COSEKey { + fn deserialize(deserializer: D) -> std::result::Result + 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(self, mut map: M) -> std::result::Result + where + M: MapAccess<'de>, + { + let mut key_type: Option = None; + let mut alg: Option = None; + // OKP / EC2 + let mut curve: Option = None; + let mut x: Option> = None; + let mut y: Option> = None; + + // RSA specific + let mut n: Option> = None; + let mut e: Option> = None; + + while let Some(key) = map.next_key()? { + // See https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters + match key { + 1 => { + if key_type.is_some() { + return Err(SerdeError::duplicate_field("key_type")); + } + let value: i64 = map.next_value()?; + let val = COSEKeyTypeId::try_from(value).map_err(|_| { + SerdeError::custom(format!("unsupported key_type {value}")) + })?; + key_type = Some(val); + } + 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); + } + -1 => match key_type { + None => return Err(SerdeError::missing_field("key_type")), + Some(COSEKeyTypeId::OKP) | Some(COSEKeyTypeId::EC2) => { + if curve.is_some() { + return Err(SerdeError::duplicate_field("curve")); + } + let value: i64 = map.next_value()?; + let val = Curve::try_from(value).map_err(|_| { + SerdeError::custom(format!("unsupported curve {value}")) + })?; + curve = Some(val); + } + Some(COSEKeyTypeId::RSA) => { + if n.is_some() { + return Err(SerdeError::duplicate_field("n")); + } + let value: ByteBuf = map.next_value()?; + n = Some(value.to_vec()); + } + }, + -2 => match key_type { + None => return Err(SerdeError::missing_field("key_type")), + Some(COSEKeyTypeId::OKP) | Some(COSEKeyTypeId::EC2) => { + if x.is_some() { + return Err(SerdeError::duplicate_field("x")); + } + let value: ByteBuf = map.next_value()?; + x = Some(value.to_vec()); + } + Some(COSEKeyTypeId::RSA) => { + if e.is_some() { + return Err(SerdeError::duplicate_field("e")); + } + let value: ByteBuf = map.next_value()?; + e = Some(value.to_vec()); + } + }, + -3 if key_type == Some(COSEKeyTypeId::EC2) => { + if y.is_some() { + return Err(SerdeError::duplicate_field("y")); + } + let value: ByteBuf = map.next_value()?; + y = Some(value.to_vec()); + } + other => { + return Err(SerdeError::custom(format!("unexpected field: {other}"))); + } + }; + } + + let key_type = key_type.ok_or_else(|| SerdeError::missing_field("key_type (1)"))?; + let alg = alg.ok_or_else(|| SerdeError::missing_field("alg (3)"))?; + + let res = match key_type { + COSEKeyTypeId::OKP => { + let curve = curve.ok_or_else(|| SerdeError::missing_field("curve (-1)"))?; + let x = x.ok_or_else(|| SerdeError::missing_field("x (-2)"))?; + COSEKeyType::OKP(COSEOKPKey { curve, x }) + } + COSEKeyTypeId::EC2 => { + let curve = curve.ok_or_else(|| SerdeError::missing_field("curve (-1)"))?; + let x = x.ok_or_else(|| SerdeError::missing_field("x (-2)"))?; + let y = y.ok_or_else(|| SerdeError::missing_field("y (-3)"))?; + COSEKeyType::EC2(COSEEC2Key { curve, x, y }) + } + COSEKeyTypeId::RSA => { + let n = n.ok_or_else(|| SerdeError::missing_field("n (-1)"))?; + let e = e.ok_or_else(|| SerdeError::missing_field("e (-2)"))?; + COSEKeyType::RSA(COSERSAKey { e, n }) + } + }; + Ok(COSEKey { alg, key: res }) + } + } + + deserializer.deserialize_bytes(COSEKeyVisitor) + } +} + +impl Serialize for COSEKey { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + let map_len = match &self.key { + COSEKeyType::OKP(_) => 4, + COSEKeyType::EC2(_) => 5, + COSEKeyType::RSA(_) => 4, + }; + 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, &serde_bytes::Bytes::new(&key.x))?; + } + COSEKeyType::EC2(key) => { + map.serialize_entry(&1, &COSEKeyTypeId::EC2)?; + map.serialize_entry(&3, &self.alg)?; + map.serialize_entry(&-1, &key.curve)?; + 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, &serde_bytes::Bytes::new(&key.n))?; + map.serialize_entry(&-2, &serde_bytes::Bytes::new(&key.e))?; + } + } + + map.end() + } +} + +/// Errors that can be returned from COSE functions. +#[derive(Debug, Clone, Serialize)] +pub enum CryptoError { + // DecodingFailure, + LibraryFailure, + MalformedInput, + // MissingHeader, + // UnexpectedHeaderValue, + // UnexpectedTag, + // UnexpectedType, + // Unimplemented, + // VerificationFailed, + // SigningFailed, + // InvalidArgument, + UnknownKeyType, + UnknownSignatureScheme, + UnknownAlgorithm, + WrongSaltLength, + UnsupportedAlgorithm(COSEAlgorithm), + UnsupportedCurve(Curve), + UnsupportedKeyType, + Backend(String), +} + +impl From for CommandError { + fn from(e: CryptoError) -> Self { + CommandError::Crypto(e) + } +} + +impl From for AuthenticatorError { + fn from(e: CryptoError) -> Self { + AuthenticatorError::HIDError(HIDError::Command(CommandError::Crypto(e))) + } +} + +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 { + // 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 std::convert::TryFrom; + + #[cfg(feature = "crypto_nss")] + use super::backend::{ecdsa_p256_sha256_sign_raw, test_ecdsa_p256_sha256_verify_raw}; + use super::{ + backend::hmac_sha256, backend::sha256, backend::test_ecdh_p256_raw, COSEAlgorithm, COSEKey, + Curve, PinProtocolImpl, PinUvAuth1, PinUvAuth2, PinUvAuthProtocol, PublicInputs, + SharedSecret, + }; + use crate::crypto::{COSEEC2Key, COSEKeyType, COSEOKPKey, COSERSAKey}; + use crate::ctap2::attestation::AAGuid; + use crate::ctap2::commands::client_pin::Pin; + use crate::ctap2::commands::get_info::{ + tests::AAGUID_RAW, AuthenticatorOptions, AuthenticatorVersion, + }; + use crate::util::decode_hex; + use crate::AuthenticatorInfo; + use serde_cbor::de::from_slice; + + // Extracted from RFC 8410 Example 10.1 + const SAMPLE_ED25519_KEY: &[u8] = &[ + 0x19, 0xbf, 0x44, 0x09, 0x69, 0x84, 0xcd, 0xfe, 0x85, 0x41, 0xba, 0xc1, 0x67, 0xdc, 0x3b, + 0x96, 0xc8, 0x50, 0x86, 0xaa, 0x30, 0xb6, 0xb6, 0xcb, 0x0c, 0x5c, 0x38, 0xad, 0x70, 0x31, + 0x66, 0xe1, + ]; + + const SAMPLE_P256_X: &[u8] = &[ + 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, + ]; + const SAMPLE_P256_Y: &[u8] = &[ + 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, + ]; + + const SAMPLE_RSA_MODULUS: &[u8] = &[ + 0xd4, 0xd2, 0x53, 0xed, 0x7a, 0x69, 0xb1, 0x84, 0xc9, 0xfb, 0x70, 0x30, 0x0c, 0x51, 0xb1, + 0x8f, 0x89, 0x6c, 0xb1, 0x31, 0x6d, 0x87, 0xbe, 0xe1, 0xc7, 0xf7, 0xb0, 0x4f, 0xe7, 0x27, + 0xa7, 0xb7, 0x7c, 0x55, 0x20, 0x37, 0xa8, 0xac, 0x40, 0xf4, 0xbc, 0x59, 0xc4, 0x92, 0x8f, + 0x13, 0x5b, 0x5e, 0xa7, 0x18, 0x05, 0xcc, 0xd7, 0x9c, 0xfb, 0x88, 0x6c, 0xf1, 0xbc, 0x6b, + 0x1b, 0x8d, 0xb7, 0x8d, 0x2d, 0xaa, 0xcb, 0xee, 0xdb, 0xab, 0x49, 0x36, 0x77, 0xe5, 0xd1, + 0x84, 0xa1, 0x40, 0x3f, 0xf6, 0xf7, 0x98, 0x6c, 0xaa, 0x24, 0x48, 0x30, 0x44, 0xdc, 0x68, + 0xbd, 0x9e, 0x74, 0x37, 0xaf, 0x27, 0x12, 0x90, 0x74, 0x0d, 0x9e, 0x3c, 0xa5, 0x3a, 0x1d, + 0xb8, 0x54, 0x92, 0xd4, 0x6d, 0x1f, 0xf9, 0x39, 0xb8, 0x1d, 0x8a, 0x5e, 0xbe, 0x12, 0xbd, + 0xe2, 0x9c, 0xf2, 0x5a, 0x48, 0x5d, 0x71, 0x2c, 0x71, 0x72, 0x6d, 0xd2, 0xcb, 0x37, 0xb1, + 0xe6, 0x2f, 0x76, 0x43, 0xda, 0xca, 0x44, 0x30, 0x7b, 0x28, 0xe7, 0xe4, 0xec, 0xa9, 0xc9, + 0x1a, 0x5f, 0xe5, 0x51, 0x03, 0x25, 0x60, 0x7c, 0x5a, 0x69, 0x12, 0x4d, 0x50, 0xfd, 0xb2, + 0xb8, 0x6e, 0x13, 0xb2, 0x92, 0xda, 0x0e, 0x31, 0xc9, 0xf1, 0x9c, 0xde, 0x17, 0x63, 0xe4, + 0xcb, 0xac, 0xd5, 0xee, 0x84, 0x06, 0xde, 0x67, 0x2d, 0xb8, 0xd2, 0xe1, 0x4b, 0xbb, 0x49, + 0xea, 0x45, 0xd4, 0xa1, 0x7f, 0x46, 0xf2, 0xd6, 0x0c, 0x05, 0x9d, 0x1d, 0x1a, 0x99, 0x41, + 0x20, 0x5e, 0x1a, 0xa4, 0xcc, 0x21, 0x44, 0x58, 0x8b, 0xcd, 0x98, 0xe4, 0x3d, 0x53, 0x20, + 0xfc, 0xfc, 0x7b, 0x9f, 0x43, 0x35, 0xfb, 0x38, 0x37, 0x23, 0xd0, 0x76, 0xe3, 0x3d, 0x4f, + 0x89, 0x9b, 0x89, 0x32, 0x81, 0x89, 0xed, 0x58, 0xc0, 0x80, 0x18, 0x83, 0x5b, 0xaf, 0x5a, + 0xa5, + ]; + + #[test] + fn test_rsa_key_to_der_spki() { + // $ ascii2der | xxd -i + // SEQUENCE { + // SEQUENCE { + // # sha256WithRSAEncryption + // OBJECT_IDENTIFIER { 1.2.840.113549.1.1.11 } + // NULL {} + // } + // BIT_STRING { + // `00` + // SEQUENCE { + // INTEGER { `00d4d253ed7a69b184c9fb70300c51b18f896cb1316d87bee1c7f7b04fe727a7b77c552037a8ac40f4bc59c4928f135b5ea71805ccd79cfb886cf1bc6b1b8db78d2daacbeedbab493677e5d184a1403ff6f7986caa24483044dc68bd9e7437af271290740d9e3ca53a1db85492d46d1ff939b81d8a5ebe12bde29cf25a485d712c71726dd2cb37b1e62f7643daca44307b28e7e4eca9c91a5fe5510325607c5a69124d50fdb2b86e13b292da0e31c9f19cde1763e4cbacd5ee8406de672db8d2e14bbb49ea45d4a17f46f2d60c059d1d1a9941205e1aa4cc2144588bcd98e43d5320fcfc7b9f4335fb383723d076e33d4f899b89328189ed58c08018835baf5aa5` } + // INTEGER { 65537 } + // } + // } + // } + let expected: &[u8] = &[ + 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, + 0x01, 0x01, 0x0b, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, + 0x02, 0x82, 0x01, 0x01, 0x00, 0xd4, 0xd2, 0x53, 0xed, 0x7a, 0x69, 0xb1, 0x84, 0xc9, + 0xfb, 0x70, 0x30, 0x0c, 0x51, 0xb1, 0x8f, 0x89, 0x6c, 0xb1, 0x31, 0x6d, 0x87, 0xbe, + 0xe1, 0xc7, 0xf7, 0xb0, 0x4f, 0xe7, 0x27, 0xa7, 0xb7, 0x7c, 0x55, 0x20, 0x37, 0xa8, + 0xac, 0x40, 0xf4, 0xbc, 0x59, 0xc4, 0x92, 0x8f, 0x13, 0x5b, 0x5e, 0xa7, 0x18, 0x05, + 0xcc, 0xd7, 0x9c, 0xfb, 0x88, 0x6c, 0xf1, 0xbc, 0x6b, 0x1b, 0x8d, 0xb7, 0x8d, 0x2d, + 0xaa, 0xcb, 0xee, 0xdb, 0xab, 0x49, 0x36, 0x77, 0xe5, 0xd1, 0x84, 0xa1, 0x40, 0x3f, + 0xf6, 0xf7, 0x98, 0x6c, 0xaa, 0x24, 0x48, 0x30, 0x44, 0xdc, 0x68, 0xbd, 0x9e, 0x74, + 0x37, 0xaf, 0x27, 0x12, 0x90, 0x74, 0x0d, 0x9e, 0x3c, 0xa5, 0x3a, 0x1d, 0xb8, 0x54, + 0x92, 0xd4, 0x6d, 0x1f, 0xf9, 0x39, 0xb8, 0x1d, 0x8a, 0x5e, 0xbe, 0x12, 0xbd, 0xe2, + 0x9c, 0xf2, 0x5a, 0x48, 0x5d, 0x71, 0x2c, 0x71, 0x72, 0x6d, 0xd2, 0xcb, 0x37, 0xb1, + 0xe6, 0x2f, 0x76, 0x43, 0xda, 0xca, 0x44, 0x30, 0x7b, 0x28, 0xe7, 0xe4, 0xec, 0xa9, + 0xc9, 0x1a, 0x5f, 0xe5, 0x51, 0x03, 0x25, 0x60, 0x7c, 0x5a, 0x69, 0x12, 0x4d, 0x50, + 0xfd, 0xb2, 0xb8, 0x6e, 0x13, 0xb2, 0x92, 0xda, 0x0e, 0x31, 0xc9, 0xf1, 0x9c, 0xde, + 0x17, 0x63, 0xe4, 0xcb, 0xac, 0xd5, 0xee, 0x84, 0x06, 0xde, 0x67, 0x2d, 0xb8, 0xd2, + 0xe1, 0x4b, 0xbb, 0x49, 0xea, 0x45, 0xd4, 0xa1, 0x7f, 0x46, 0xf2, 0xd6, 0x0c, 0x05, + 0x9d, 0x1d, 0x1a, 0x99, 0x41, 0x20, 0x5e, 0x1a, 0xa4, 0xcc, 0x21, 0x44, 0x58, 0x8b, + 0xcd, 0x98, 0xe4, 0x3d, 0x53, 0x20, 0xfc, 0xfc, 0x7b, 0x9f, 0x43, 0x35, 0xfb, 0x38, + 0x37, 0x23, 0xd0, 0x76, 0xe3, 0x3d, 0x4f, 0x89, 0x9b, 0x89, 0x32, 0x81, 0x89, 0xed, + 0x58, 0xc0, 0x80, 0x18, 0x83, 0x5b, 0xaf, 0x5a, 0xa5, 0x02, 0x03, 0x01, 0x00, 0x01, + ]; + let rsa_key = COSERSAKey { + e: vec![1, 0, 1], + n: SAMPLE_RSA_MODULUS.to_vec(), + }; + let cose_key: COSEKey = COSEKey { + alg: COSEAlgorithm::RS256, + key: COSEKeyType::RSA(rsa_key), + }; + let actual = cose_key.der_spki().expect("Failed to serialize to SPKI"); + assert_eq!(expected, &actual); + } + + #[test] + fn test_rsa_key_to_cbor() { + let key = COSERSAKey { + e: vec![1, 0, 1], + n: SAMPLE_RSA_MODULUS.to_vec(), + }; + let cose_key: COSEKey = COSEKey { + alg: COSEAlgorithm::RS256, + key: COSEKeyType::RSA(key), + }; + let cose_key_cbor = serde_cbor::to_vec(&cose_key).expect("Failed to serialize key"); + let actual = serde_cbor::from_slice(&cose_key_cbor).expect("Failed to deserialize key"); + assert_eq!(cose_key, actual); + } + + #[test] + fn test_ec2_key_to_der_spki() { + // $ ascii2der | xxd -i + // SEQUENCE { + // SEQUENCE { + // # ecPublicKey + // OBJECT_IDENTIFIER { 1.2.840.10045.2.1 } + // # secp256r1 + // OBJECT_IDENTIFIER { 1.2.840.10045.3.1.7 } + // } + // BIT_STRING { `00` `04fc9ed36f7c1aa915ce3ea177f07567f07f16f9479d95ad8ed4971d3305e31a8050b733af8c0b0ee1da8de0acf9d8e13282f063b7b30d73d4d32c9aad6dfa8b27` } + // } + let expected = [ + 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, 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 ec2_key = COSEEC2Key { + curve: Curve::SECP256R1, + x: SAMPLE_P256_X.to_vec(), + y: SAMPLE_P256_Y.to_vec(), + }; + let cose_key = COSEKey { + alg: COSEAlgorithm::EDDSA, + key: COSEKeyType::EC2(ec2_key), + }; + let actual = cose_key.der_spki().expect("Failed to serialize key"); + assert_eq!(actual, expected); + } + + #[test] + fn test_ec2_key_to_cbor() { + let ec2_key = COSEEC2Key { + curve: Curve::SECP256R1, + x: SAMPLE_P256_X.to_vec(), + y: SAMPLE_P256_Y.to_vec(), + }; + let cose_key = COSEKey { + alg: COSEAlgorithm::EDDSA, + key: COSEKeyType::EC2(ec2_key), + }; + let cose_key_cbor = serde_cbor::to_vec(&cose_key).expect("Failed to serialize key"); + let actual = serde_cbor::from_slice(&cose_key_cbor).expect("Failed to deserialize key"); + assert_eq!(cose_key, actual); + } + + #[test] + fn test_okp_key_to_der_spki() { + // RFC 8410 Example 10.1 + let expected = [ + 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00, 0x19, 0xbf, + 0x44, 0x09, 0x69, 0x84, 0xcd, 0xfe, 0x85, 0x41, 0xba, 0xc1, 0x67, 0xdc, 0x3b, 0x96, + 0xc8, 0x50, 0x86, 0xaa, 0x30, 0xb6, 0xb6, 0xcb, 0x0c, 0x5c, 0x38, 0xad, 0x70, 0x31, + 0x66, 0xe1, + ]; + let okp_key = COSEOKPKey { + curve: Curve::Ed25519, + x: SAMPLE_ED25519_KEY.to_vec(), + }; + let cose_key = COSEKey { + alg: COSEAlgorithm::EDDSA, + key: COSEKeyType::OKP(okp_key), + }; + let actual = cose_key.der_spki().expect("Failed to serialize key"); + assert_eq!(actual, expected); + } + + #[test] + fn test_okp_key_to_cbor() { + let okp_key = COSEOKPKey { + curve: Curve::Ed25519, + x: SAMPLE_ED25519_KEY.to_vec(), + }; + let cose_key = COSEKey { + alg: COSEAlgorithm::EDDSA, + key: COSEKeyType::OKP(okp_key), + }; + let cose_key_cbor = serde_cbor::to_vec(&cose_key).expect("Failed to serialize key"); + let actual = serde_cbor::from_slice(&cose_key_cbor).expect("Failed to deserialize key"); + assert_eq!(cose_key, actual); + } + + #[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, Curve::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/main/tests/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 client_ec2_key = COSEEC2Key { + curve: Curve::SECP256R1, + x: EC_PUB_X.clone(), + y: EC_PUB_Y.clone(), + }; + + let peer_ec2_key = COSEEC2Key { + curve: Curve::SECP256R1, + x: DEV_PUB_X, + y: DEV_PUB_Y, + }; + + // We are using `test_cose_ec2_p256_ecdh_sha256()` 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 peer_spki = peer_ec2_key.der_spki().unwrap(); + let shared_point = test_ecdh_p256_raw(&peer_spki, &EC_PUB_X, &EC_PUB_Y, &EC_PRIV).unwrap(); + let shared_secret = SharedSecret { + pin_protocol: PinUvAuthProtocol(Box::new(PinUvAuth1 {})), + key: sha256(&shared_point).unwrap(), + inputs: PublicInputs { + client: COSEKey { + alg: COSEAlgorithm::ES256, + key: COSEKeyType::EC2(client_ec2_key), + }, + peer: COSEKey { + alg: COSEAlgorithm::ES256, + key: COSEKeyType::EC2(peer_ec2_key), + }, + }, + }; + assert_eq!(shared_secret.key, SHARED); + + let token_enc = shared_secret.encrypt(&TOKEN).unwrap(); + assert_eq!(token_enc, TOKEN_ENC); + + let token = shared_secret.decrypt(&TOKEN_ENC).unwrap(); + assert_eq!(token, TOKEN); + + let pin = Pin::new("1234"); + let pin_hash_enc = shared_secret.encrypt(&pin.for_pin_token()).unwrap(); + assert_eq!(pin_hash_enc, PIN_HASH_ENC); + } + + #[test] + fn test_pin_uv_auth2_kdf() { + // We don't pull a complete HKDF implementation from the crypto backend, so we need to + // check that PinUvAuth2::kdf makes the right sequence of HMAC-SHA256 calls. + // + // ```python + // from cryptography.hazmat.primitives.kdf.hkdf import HKDF + // from cryptography.hazmat.primitives import hashes + // from cryptography.hazmat.backends import default_backend + // + // Z = b"\xFF" * 32 + // + // hmac_key = HKDF( + // algorithm=hashes.SHA256(), + // length=32, + // salt=b"\x00" * 32, + // info=b"CTAP2 HMAC key", + // ).derive(Z) + // + // aes_key = HKDF( + // algorithm=hashes.SHA256(), + // length=32, + // salt=b"\x00" * 32, + // info=b"CTAP2 AES key", + // ).derive(Z) + // + // print((hmac_key+aes_key).hex()) + // ``` + let input = decode_hex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"); + let expected = decode_hex("570B4ED82AA5DFB49DB79DBEAF4B315D8ABB1A9867B245F3367026987C0D47A17D9A93C39BAEC741D141C6238D8E1846DE323D8EED022CB397D19A73B98945E2"); + let output = PinUvAuth2 {}.kdf(&input).unwrap(); + assert_eq!(&expected, &output); + } + + #[test] + fn test_hmac_sha256() { + let key = "key"; + let message = "The quick brown fox jumps over the lazy dog"; + let expected = + decode_hex("f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8"); + + let result = hmac_sha256(key.as_bytes(), message.as_bytes()).expect("HMAC-SHA256 failed"); + 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 = hmac_sha256(key.as_bytes(), message.as_bytes()).expect("HMAC-SHA256 failed"); + 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, + ]; + + let mut input = vec![0x00; 64]; + { + let pin_bytes = pin.as_bytes(); + let (head, _) = input.split_at_mut(pin_bytes.len()); + head.copy_from_slice(pin_bytes); + } + + let new_pin_enc = PinUvAuth1 {} + .encrypt(&shared_secret, &input) + .expect("Failed to encrypt pin"); + assert_eq!(new_pin_enc, expected_new_pin_enc); + + let pin_auth = PinUvAuth1 {} + .authenticate(&shared_secret, &new_pin_enc) + .expect("HMAC-SHA256 failed"); + assert_eq!(pin_auth[0..16], expected_pin_auth); + } + + #[test] + fn test_pin_protocol() { + let mut info = AuthenticatorInfo { + versions: vec![AuthenticatorVersion::U2F_V2, AuthenticatorVersion::FIDO_2_0], + extensions: vec![], + aaguid: AAGuid(AAGUID_RAW), + options: AuthenticatorOptions { + platform_device: false, + resident_key: true, + client_pin: Some(false), + user_presence: true, + ..Default::default() + }, + max_msg_size: Some(1200), + pin_protocols: None, + ..Default::default() + }; + + // Valid pin_protocols + info.pin_protocols = Some(vec![1, 2]); + let pin = PinUvAuthProtocol::try_from(&info).unwrap(); + assert_eq!(pin.id(), 1); // The one listed first + + // Invalid pin_protocols + info.pin_protocols = Some(vec![0, 10]); + PinUvAuthProtocol::try_from(&info).unwrap_err(); + + info.pin_protocols = None; + // No PIN protocols. CTAP1 - not supported + info.versions = vec![AuthenticatorVersion::U2F_V2]; + PinUvAuthProtocol::try_from(&info).unwrap_err(); + + // No PIN protocols. CTAP2.0 - Fallback to 1 + info.versions = vec![AuthenticatorVersion::U2F_V2, AuthenticatorVersion::FIDO_2_0]; + let pin = PinUvAuthProtocol::try_from(&info).unwrap(); + assert_eq!(pin.id(), 1); + + // No PIN protocols. CTAP2.1 - Fallback to 2 + info.versions = vec![AuthenticatorVersion::FIDO_2_1]; + let pin = PinUvAuthProtocol::try_from(&info).unwrap(); + assert_eq!(pin.id(), 2); + + // No PIN protocols. CTAP2.1_PRE - Fallback to 2 + info.versions = vec![ + AuthenticatorVersion::FIDO_2_0, + AuthenticatorVersion::FIDO_2_1_PRE, + ]; + let pin = PinUvAuthProtocol::try_from(&info).unwrap(); + assert_eq!(pin.id(), 2); + } + + #[test] + #[cfg(feature = "crypto_nss")] + fn test_sign() { + let (good_private, good_public) = + COSEKey::generate(COSEAlgorithm::ES256).expect("could not generate a key pair"); + let good_spki = match good_public.key { + COSEKeyType::EC2(ref x) => x.der_spki().expect("could not serialize public key"), + _ => unreachable!(), + }; + + let good_data = vec![1, 2, 3, 4, 5, 6, 7, 8]; + let good_signature = + ecdsa_p256_sha256_sign_raw(&good_private, &good_data).expect("could not sign"); + let good_signature2 = + ecdsa_p256_sha256_sign_raw(&good_private, &good_data).expect("could not sign"); + + // Signing is randomized + assert_ne!(good_signature, good_signature2); + + // Good signature verifies + assert!(test_ecdsa_p256_sha256_verify_raw(&good_spki, &good_signature, &good_data).is_ok()); + + // Wrong data does not verify + let other_data = vec![0, 0, 0, 0, 5, 6, 7, 8]; + assert!( + test_ecdsa_p256_sha256_verify_raw(&good_spki, &good_signature, &other_data).is_err() + ); + + // Wrong signature does not verify + let other_signature = + ecdsa_p256_sha256_sign_raw(&good_private, &other_data).expect("could not sign"); + assert!( + test_ecdsa_p256_sha256_verify_raw(&good_spki, &other_signature, &good_data).is_err() + ); + + // Wrong key does not verify + let (_, other_public) = + COSEKey::generate(COSEAlgorithm::ES256).expect("could not generate a key pair"); + let other_spki = match other_public.key { + COSEKeyType::EC2(ref x) => x.der_spki().expect("could not serialize public key"), + _ => unreachable!(), + }; + assert!( + test_ecdsa_p256_sha256_verify_raw(&other_spki, &good_signature, &good_data).is_err() + ); + } +} 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..56c23db5dd --- /dev/null +++ b/third_party/rust/authenticator/src/crypto/nss.rs @@ -0,0 +1,481 @@ +use super::CryptoError; +use nss_gk_api::p11::{ + PK11Origin, PK11_CreateContextBySymKey, PK11_Decrypt, PK11_DigestFinal, PK11_DigestOp, + PK11_Encrypt, PK11_ExportDERPrivateKeyInfo, PK11_GenerateKeyPairWithOpFlags, + PK11_GenerateRandom, PK11_HashBuf, PK11_ImportDERPrivateKeyInfoAndReturnKey, PK11_ImportSymKey, + PK11_PubDeriveWithKDF, PK11_SignWithMechanism, PrivateKey, PublicKey, + SECKEY_DecodeDERSubjectPublicKeyInfo, SECKEY_ExtractPublicKey, SECOidTag, Slot, + SubjectPublicKeyInfo, AES_BLOCK_SIZE, PK11_ATTR_EXTRACTABLE, PK11_ATTR_INSENSITIVE, + PK11_ATTR_SESSION, SHA256_LENGTH, +}; +use nss_gk_api::{IntoResult, SECItem, SECItemBorrowed, ScopedSECItem, PR_FALSE}; +use pkcs11_bindings::{ + CKA_DERIVE, CKA_ENCRYPT, CKA_SIGN, CKD_NULL, CKF_DERIVE, CKM_AES_CBC, CKM_ECDH1_DERIVE, + CKM_ECDSA_SHA256, CKM_EC_KEY_PAIR_GEN, CKM_SHA256_HMAC, CKM_SHA512_HMAC, +}; +use std::convert::TryFrom; +use std::os::raw::{c_int, c_uint}; +use std::ptr; + +use super::der; + +#[cfg(test)] +use nss_gk_api::p11::PK11_VerifyWithMechanism; + +impl From for CryptoError { + fn from(e: nss_gk_api::Error) -> Self { + CryptoError::Backend(format!("{e}")) + } +} + +pub type Result = std::result::Result; + +fn nss_public_key_from_der_spki(spki: &[u8]) -> Result { + // TODO: replace this with an nss-gk-api function + // https://github.com/mozilla/nss-gk-api/issues/7 + 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) +} + +/// ECDH using NSS types. Computes the x coordinate of scalar multiplication of `peer_public` by +/// `client_private`. +fn ecdh_nss_raw(client_private: PrivateKey, peer_public: PublicKey) -> Result> { + 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()?; + Ok(ecdh_x_coord_bytes.to_vec()) +} + +fn generate_p256_nss() -> Result<(PrivateKey, PublicKey)> { + // Hard-coding the P256 OID here is easier than extracting a group name from peer_public and + // comparing it with P256. We'll fail in `PK11_GenerateKeyPairWithOpFlags` if peer_public is on + // the wrong curve. + let oid_bytes = der::object_id(der::OID_SECP256R1_BYTES)?; + let mut oid = SECItemBorrowed::wrap(&oid_bytes); + 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 + 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_EXTRACTABLE | PK11_ATTR_INSENSITIVE | PK11_ATTR_SESSION, + CKF_DERIVE, + CKF_DERIVE, + ptr::null_mut(), + ) + .into_result()?; + + let client_public = PublicKey::from_ptr(client_public_ptr)?; + + Ok((client_private, client_public)) + } +} + +/// This returns a PKCS#8 ECPrivateKey and an uncompressed SEC1 public key. +pub fn gen_p256() -> Result<(Vec, Vec)> { + nss_gk_api::init(); + + let (client_private, client_public) = generate_p256_nss()?; + + let pkcs8_priv = unsafe { + let pkcs8_priv_item: ScopedSECItem = + PK11_ExportDERPrivateKeyInfo(*client_private, ptr::null_mut()).into_result()?; + pkcs8_priv_item.into_vec() + }; + + let sec1_pub = client_public.key_data()?; + + Ok((pkcs8_priv, sec1_pub)) +} + +pub fn ecdsa_p256_sha256_sign_raw(private: &[u8], data: &[u8]) -> Result> { + nss_gk_api::init(); + + let slot = Slot::internal()?; + + let imported_private: PrivateKey = unsafe { + let mut imported_private_ptr = ptr::null_mut(); + PK11_ImportDERPrivateKeyInfoAndReturnKey( + *slot, + SECItemBorrowed::wrap(private).as_mut(), + ptr::null_mut(), + ptr::null_mut(), + PR_FALSE, + PR_FALSE, + 255, /* todo: expose KU_ flags in nss-gk-api */ + &mut imported_private_ptr, + ptr::null_mut(), + ); + imported_private_ptr.into_result()? + }; + + let signature_buf = vec![0; 64]; + unsafe { + PK11_SignWithMechanism( + *imported_private, + CKM_ECDSA_SHA256, + ptr::null_mut(), + SECItemBorrowed::wrap(&signature_buf).as_mut(), + SECItemBorrowed::wrap(data).as_mut(), + ) + .into_result()?; + } + + let (r, s) = signature_buf.split_at(32); + der::sequence(&[&der::integer(r)?, &der::integer(s)?]) +} + +/// Ephemeral ECDH over P256. Takes a DER SubjectPublicKeyInfo that encodes a public key. Generates +/// an ephemeral P256 key pair. Returns +/// 1) the x coordinate of the shared point, and +/// 2) the uncompressed SEC 1 encoding of the ephemeral public key. +pub fn ecdhe_p256_raw(peer_spki: &[u8]) -> Result<(Vec, Vec)> { + nss_gk_api::init(); + + let peer_public = nss_public_key_from_der_spki(peer_spki)?; + + let (client_private, client_public) = generate_p256_nss()?; + + let shared_point = ecdh_nss_raw(client_private, peer_public)?; + + Ok((shared_point, client_public.key_data()?)) +} + +/// AES-256-CBC encryption for data that is a multiple of the AES block size (16 bytes) in length. +/// Uses the zero IV if `iv` is None. +pub fn encrypt_aes_256_cbc_no_pad(key: &[u8], iv: Option<&[u8]>, data: &[u8]) -> Result> { + nss_gk_api::init(); + + if key.len() != 32 { + return Err(CryptoError::LibraryFailure); + } + + let iv = iv.unwrap_or(&[0u8; AES_BLOCK_SIZE]); + + if iv.len() != AES_BLOCK_SIZE { + return Err(CryptoError::LibraryFailure); + } + + let in_len = match c_uint::try_from(data.len()) { + Ok(in_len) => in_len, + _ => return Err(CryptoError::LibraryFailure), + }; + + if data.len() % AES_BLOCK_SIZE != 0 { + return Err(CryptoError::LibraryFailure); + } + + 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 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; data.len()]; + 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) +} + +/// AES-256-CBC decryption for data that is a multiple of the AES block size (16 bytes) in length. +/// Uses the zero IV if `iv` is None. +pub fn decrypt_aes_256_cbc_no_pad(key: &[u8], iv: Option<&[u8]>, data: &[u8]) -> Result> { + nss_gk_api::init(); + + if key.len() != 32 { + return Err(CryptoError::LibraryFailure); + } + + let iv = iv.unwrap_or(&[0u8; AES_BLOCK_SIZE]); + + if iv.len() != AES_BLOCK_SIZE { + return Err(CryptoError::LibraryFailure); + } + + let in_len = match c_uint::try_from(data.len()) { + Ok(in_len) => in_len, + _ => return Err(CryptoError::LibraryFailure), + }; + + if data.len() % AES_BLOCK_SIZE != 0 { + return Err(CryptoError::LibraryFailure); + } + + 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 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; data.len()]; + 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) +} + +/// Textbook HMAC-SHA256 +pub fn hmac_sha256(key: &[u8], data: &[u8]) -> Result> { + nss_gk_api::init(); + + let data_len = match u32::try_from(data.len()) { + Ok(data_len) => data_len, + _ => return Err(CryptoError::LibraryFailure), + }; + + let slot = Slot::internal()?; + let sym_key = unsafe { + PK11_ImportSymKey( + *slot, + CKM_SHA256_HMAC, + PK11Origin::PK11_OriginUnwrap, + CKA_SIGN, + SECItemBorrowed::wrap(key).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, data.as_ptr(), data_len).into_result()? }; + let mut digest = vec![0u8; SHA256_LENGTH]; + 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 as usize, SHA256_LENGTH); + Ok(digest) +} + +/// Textbook SHA256 +pub fn sha256(data: &[u8]) -> Result> { + nss_gk_api::init(); + + let data_len: i32 = match i32::try_from(data.len()) { + Ok(data_len) => data_len, + _ => return Err(CryptoError::LibraryFailure), + }; + let mut digest = vec![0u8; SHA256_LENGTH]; + unsafe { + PK11_HashBuf( + SECOidTag::SEC_OID_SHA256, + digest.as_mut_ptr(), + data.as_ptr(), + data_len, + ) + .into_result()? + }; + Ok(digest) +} + +pub fn random_bytes(count: usize) -> Result> { + nss_gk_api::init(); + + let count_cint: c_int = match c_int::try_from(count) { + Ok(c) => c, + _ => return Err(CryptoError::LibraryFailure), + }; + + let mut out = vec![0u8; count]; + unsafe { PK11_GenerateRandom(out.as_mut_ptr(), count_cint).into_result()? }; + Ok(out) +} + +#[cfg(test)] +pub fn test_ecdh_p256_raw( + peer_spki: &[u8], + client_public_x: &[u8], + client_public_y: &[u8], + client_private: &[u8], +) -> Result> { + nss_gk_api::init(); + + let peer_public = nss_public_key_from_der_spki(peer_spki)?; + + // NSS has no mechanism to import a raw elliptic curve coordinate as a private key. + // We need to encode it in an RFC 5208 PrivateKeyInfo: + // + // PrivateKeyInfo ::= SEQUENCE { + // version Version, + // privateKeyAlgorithm PrivateKeyAlgorithmIdentifier, + // privateKey PrivateKey, + // attributes [0] IMPLICIT Attributes OPTIONAL } + // + // Version ::= INTEGER + // PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier + // PrivateKey ::= OCTET STRING + // Attributes ::= SET OF Attribute + // + // The privateKey field will contain an RFC 5915 ECPrivateKey: + // ECPrivateKey ::= SEQUENCE { + // version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1), + // privateKey OCTET STRING, + // parameters [0] ECParameters {{ NamedCurve }} OPTIONAL, + // publicKey [1] BIT STRING OPTIONAL + // } + + // PrivateKeyInfo + let priv_key_info = der::sequence(&[ + // version + &der::integer(&[0x00])?, + // privateKeyAlgorithm + &der::sequence(&[ + &der::object_id(der::OID_EC_PUBLIC_KEY_BYTES)?, + &der::object_id(der::OID_SECP256R1_BYTES)?, + ])?, + // privateKey + &der::octet_string( + // ECPrivateKey + &der::sequence(&[ + // version + &der::integer(&[0x01])?, + // privateKey + &der::octet_string(client_private)?, + // publicKey + &der::context_specific_explicit_tag( + 1, // publicKey + &der::bit_string(&[&[0x04], client_public_x, client_public_y].concat())?, + )?, + ])?, + )?, + ])?; + + // Now we can import the private key. + let slot = Slot::internal()?; + let mut priv_key_info_item = SECItemBorrowed::wrap(&priv_key_info); + let priv_key_info_item_ptr: *mut SECItem = priv_key_info_item.as_mut(); + let mut client_private_ptr = ptr::null_mut(); + unsafe { + PK11_ImportDERPrivateKeyInfoAndReturnKey( + *slot, + priv_key_info_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 shared_point = ecdh_nss_raw(client_private, peer_public)?; + + Ok(shared_point) +} + +#[cfg(test)] +pub fn test_ecdsa_p256_sha256_verify_raw( + public: &[u8], + signature: &[u8], + data: &[u8], +) -> Result<()> { + nss_gk_api::init(); + + let signature = der::read_p256_sig(signature)?; + let public = nss_public_key_from_der_spki(public)?; + unsafe { + PK11_VerifyWithMechanism( + *public, + CKM_ECDSA_SHA256, + ptr::null_mut(), + SECItemBorrowed::wrap(&signature).as_mut(), + SECItemBorrowed::wrap(data).as_mut(), + ptr::null_mut(), + ) + .into_result()? + } + Ok(()) +} 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..065ea201a5 --- /dev/null +++ b/third_party/rust/authenticator/src/crypto/openssl.rs @@ -0,0 +1,183 @@ +use super::CryptoError; +use openssl::bn::BigNumContext; +use openssl::derive::Deriver; +use openssl::ec::{EcGroup, EcKey, PointConversionForm}; +use openssl::error::ErrorStack; +use openssl::hash::{hash, MessageDigest}; +use openssl::nid::Nid; +use openssl::pkey::{PKey, Private, Public}; +use openssl::rand::rand_bytes; +use openssl::sign::Signer; +use openssl::symm::{Cipher, Crypter, Mode}; +use std::os::raw::c_int; + +#[cfg(test)] +use openssl::ec::EcPoint; + +#[cfg(test)] +use openssl::bn::BigNum; + +const AES_BLOCK_SIZE: usize = 16; + +impl From for CryptoError { + fn from(e: ErrorStack) -> Self { + CryptoError::Backend(format!("{e}")) + } +} + +impl From<&ErrorStack> for CryptoError { + fn from(e: &ErrorStack) -> Self { + CryptoError::Backend(format!("{e}")) + } +} + +pub type Result = std::result::Result; + +/// ECDH using OpenSSL types. Computes the x coordinate of scalar multiplication of `peer_public` +/// by `client_private`. +fn ecdh_openssl_raw(client_private: EcKey, peer_public: EcKey) -> Result> { + let client_pkey = PKey::from_ec_key(client_private)?; + let peer_pkey = PKey::from_ec_key(peer_public)?; + let mut deriver = Deriver::new(&client_pkey)?; + deriver.set_peer(&peer_pkey)?; + let shared_point = deriver.derive_to_vec()?; + Ok(shared_point) +} + +/// Ephemeral ECDH over P256. Takes a DER SubjectPublicKeyInfo that encodes a public key. Generates +/// an ephemeral P256 key pair. Returns +/// 1) the x coordinate of the shared point, and +/// 2) the uncompressed SEC 1 encoding of the ephemeral public key. +pub fn ecdhe_p256_raw(peer_spki: &[u8]) -> Result<(Vec, Vec)> { + let peer_public = EcKey::public_key_from_der(peer_spki)?; + + // Hard-coding the P256 group here is easier than extracting a group name from peer_public and + // comparing it with P256. We'll fail in key derivation if peer_public is on the wrong curve. + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1)?; + + let mut bn_ctx = BigNumContext::new()?; + let client_private = EcKey::generate(&group)?; + let client_public_sec1 = client_private.public_key().to_bytes( + &group, + PointConversionForm::UNCOMPRESSED, + &mut bn_ctx, + )?; + + let shared_point = ecdh_openssl_raw(client_private, peer_public)?; + + Ok((shared_point, client_public_sec1)) +} + +/// AES-256-CBC encryption for data that is a multiple of the AES block size (16 bytes) in length. +/// Uses the zero IV if `iv` is None. +pub fn encrypt_aes_256_cbc_no_pad(key: &[u8], iv: Option<&[u8]>, data: &[u8]) -> Result> { + let iv = iv.unwrap_or(&[0u8; AES_BLOCK_SIZE]); + + let mut encrypter = Crypter::new(Cipher::aes_256_cbc(), Mode::Encrypt, key, Some(iv))?; + encrypter.pad(false); + + let in_len = data.len(); + if in_len % AES_BLOCK_SIZE != 0 { + return Err(CryptoError::LibraryFailure); + } + + // OpenSSL would panic if we didn't allocate an extra block here. + let mut out = vec![0; in_len + AES_BLOCK_SIZE]; + let mut out_len = 0; + out_len += encrypter.update(data, out.as_mut_slice())?; + out_len += encrypter.finalize(out.as_mut_slice())?; + debug_assert_eq!(in_len, out_len); + + out.truncate(out_len); + Ok(out) +} + +/// AES-256-CBC decryption for data that is a multiple of the AES block size (16 bytes) in length. +/// Uses the zero IV if `iv` is None. +pub fn decrypt_aes_256_cbc_no_pad(key: &[u8], iv: Option<&[u8]>, data: &[u8]) -> Result> { + let iv = iv.unwrap_or(&[0u8; AES_BLOCK_SIZE]); + + let mut encrypter = Crypter::new(Cipher::aes_256_cbc(), Mode::Decrypt, key, Some(iv))?; + encrypter.pad(false); + + let in_len = data.len(); + if in_len % AES_BLOCK_SIZE != 0 { + return Err(CryptoError::LibraryFailure); + } + + // OpenSSL would panic if we didn't allocate an extra block here. + let mut out = vec![0; in_len + AES_BLOCK_SIZE]; + let mut out_len = 0; + out_len += encrypter.update(data, out.as_mut_slice())?; + out_len += encrypter.finalize(out.as_mut_slice())?; + debug_assert_eq!(in_len, out_len); + + out.truncate(out_len); + Ok(out) +} + +/// Textbook HMAC-SHA256 +pub fn hmac_sha256(key: &[u8], data: &[u8]) -> Result> { + let key = PKey::hmac(key)?; + let mut signer = Signer::new(MessageDigest::sha256(), &key)?; + signer.update(data)?; + Ok(signer.sign_to_vec()?) +} + +pub fn sha256(data: &[u8]) -> Result> { + let digest = hash(MessageDigest::sha256(), data)?; + Ok(digest.as_ref().to_vec()) +} + +pub fn random_bytes(count: usize) -> Result> { + if count > c_int::MAX as usize { + return Err(CryptoError::LibraryFailure); + } + let mut out = vec![0u8; count]; + rand_bytes(&mut out)?; + Ok(out) +} + +#[cfg(test)] +pub fn test_ecdh_p256_raw( + peer_spki: &[u8], + client_public_x: &[u8], + client_public_y: &[u8], + client_private: &[u8], +) -> Result> { + let peer_public = EcKey::public_key_from_der(peer_spki)?; + let group = peer_public.group(); + + let mut client_pub_sec1 = vec![]; + client_pub_sec1.push(0x04); // SEC 1 encoded uncompressed point + client_pub_sec1.extend_from_slice(&client_public_x); + client_pub_sec1.extend_from_slice(&client_public_y); + + let mut ctx = BigNumContext::new()?; + let client_pub_point = EcPoint::from_bytes(&group, &client_pub_sec1, &mut ctx)?; + let client_priv_bignum = BigNum::from_slice(client_private)?; + let client_private = + EcKey::from_private_components(&group, &client_priv_bignum, &client_pub_point)?; + + let shared_point = ecdh_openssl_raw(client_private, peer_public)?; + + Ok(shared_point) +} + +pub fn gen_p256() -> Result<(Vec, Vec)> { + unimplemented!() +} + +pub fn ecdsa_p256_sha256_sign_raw(_private: &[u8], _data: &[u8]) -> Result> { + unimplemented!() +} + +#[allow(dead_code)] +#[cfg(test)] +pub fn test_ecdsa_p256_sha256_verify_raw( + _public: &[u8], + _signature: &[u8], + _data: &[u8], +) -> Result<()> { + unimplemented!() +} 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..af33b159b7 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/attestation.rs @@ -0,0 +1,1129 @@ +use super::utils::{from_slice_stream, read_be_u16, read_be_u32, read_byte}; +use crate::crypto::COSEAlgorithm; +use crate::ctap2::server::{CredentialProtectionPolicy, RpIdHash}; +use crate::ctap2::utils::serde_parse_err; +use crate::{crypto::COSEKey, errors::AuthenticatorError}; +use base64::Engine; +use serde::ser::{Error as SerError, SerializeMap, Serializer}; +use serde::{ + de::{Error as SerdeError, Unexpected, Visitor}, + Deserialize, Deserializer, Serialize, +}; +use serde_cbor; +use std::fmt; +use std::io::{Cursor, Read}; + +#[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), +} + +impl Serialize for HmacSecretResponse { + fn serialize(&self, serializer: S) -> Result + 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(deserializer: D) -> Result + 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(self, v: &[u8]) -> Result + where + E: SerdeError, + { + Ok(HmacSecretResponse::Secret(v.to_vec())) + } + + fn visit_bool(self, v: bool) -> Result + where + E: SerdeError, + { + Ok(HmacSecretResponse::Confirmed(v)) + } + } + deserializer.deserialize_any(HmacSecretResponseVisitor) + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Default)] +pub struct Extension { + #[serde(rename = "credProtect", skip_serializing_if = "Option::is_none")] + pub cred_protect: Option, + #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")] + pub hmac_secret: Option, + #[serde(rename = "minPinLength", skip_serializing_if = "Option::is_none")] + pub min_pin_length: Option, +} + +impl Extension { + pub fn has_some(&self) -> bool { + self.min_pin_length.is_some() || self.hmac_secret.is_some() || self.cred_protect.is_some() + } +} + +#[derive(Serialize, PartialEq, Default, Eq, Clone)] +pub struct AAGuid(pub [u8; 16]); + +impl AAGuid { + pub fn from(src: &[u8]) -> Result { + let mut payload = [0u8; 16]; + if src.len() != payload.len() { + Err(AuthenticatorError::InternalError(String::from( + "Failed to parse AAGuid", + ))) + } 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(deserializer: D) -> Result + 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(self, v: &[u8]) -> Result + 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, + pub credential_public_key: COSEKey, +} + +fn parse_attested_cred_data( + data: &mut R, +) -> Result { + let mut aaguid_raw = [0u8; 16]; + data.read_exact(&mut aaguid_raw) + .map_err(|_| serde_parse_err("AAGuid"))?; + let aaguid = AAGuid(aaguid_raw); + let cred_len = read_be_u16(data)?; + let mut credential_id = vec![0u8; cred_len as usize]; + data.read_exact(&mut credential_id) + .map_err(|_| serde_parse_err("CredentialId"))?; + let credential_public_key = from_slice_stream(data)?; + Ok(AttestedCredentialData { + aaguid, + credential_id, + credential_public_key, + }) +} + +bitflags! { + // Defining an exhaustive list of flags here ensures that `from_bits_truncate` is lossless and + // that `from_bits` never returns None. + pub struct AuthenticatorDataFlags: u8 { + const USER_PRESENT = 0x01; + const RESERVED_1 = 0x02; + const USER_VERIFIED = 0x04; + const RESERVED_3 = 0x08; + const RESERVED_4 = 0x10; + const RESERVED_5 = 0x20; + 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, + pub extensions: Extension, +} + +impl AuthenticatorData { + pub fn to_vec(&self) -> Vec { + match serde_cbor::value::to_value(self) { + Ok(serde_cbor::value::Value::Bytes(out)) => out, + _ => unreachable!(), // Serialize is guaranteed to produce bytes + } + } +} + +impl<'de> Deserialize<'de> for AuthenticatorData { + fn deserialize(deserializer: D) -> Result + 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(self, input: &[u8]) -> Result + where + E: SerdeError, + { + let mut cursor = Cursor::new(input); + let mut rp_id_hash_raw = [0u8; 32]; + cursor + .read_exact(&mut rp_id_hash_raw) + .map_err(|_| serde_parse_err("32 bytes"))?; + let rp_id_hash = RpIdHash(rp_id_hash_raw); + + // preserve the flags, even if some reserved values are set. + let flags = AuthenticatorDataFlags::from_bits_truncate(read_byte(&mut cursor)?); + let counter = read_be_u32(&mut cursor)?; + let mut credential_data = None; + if flags.contains(AuthenticatorDataFlags::ATTESTED) { + credential_data = Some(parse_attested_cred_data(&mut cursor)?); + } + + let extensions = if flags.contains(AuthenticatorDataFlags::EXTENSION_DATA) { + from_slice_stream(&mut cursor)? + } else { + Default::default() + }; + + // TODO(baloo): we should check for end of buffer and raise a parse + // parse error if data is still in the buffer + Ok(AuthenticatorData { + rp_id_hash, + flags, + counter, + credential_data, + extensions, + }) + } + } + + deserializer.deserialize_bytes(AuthenticatorDataVisitor) + } +} + +impl Serialize for AuthenticatorData { + // see https://www.w3.org/TR/webauthn-2/#sctn-authenticator-data + // Authenticator Data + // Name Length (in bytes) + // rpIdHash 32 + // flags 1 + // signCount 4 + // attestedCredentialData variable (if present) + // extensions variable (if present) + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut data = Vec::new(); + data.extend(self.rp_id_hash.0); // (1) "rpIDHash", len=32 + data.extend([self.flags.bits()]); // (2) "flags", len=1 (u8) + data.extend(self.counter.to_be_bytes()); // (3) "signCount", len=4, 32-bit unsigned big-endian integer. + + if let Some(cred) = &self.credential_data { + // see https://www.w3.org/TR/webauthn-2/#sctn-attested-credential-data + // Attested Credential Data + // Name Length (in bytes) + // aaguid 16 + // credentialIdLength 2 + // credentialId L + // credentialPublicKey variable + data.extend(cred.aaguid.0); // (1) "aaguid", len=16 + data.extend((cred.credential_id.len() as u16).to_be_bytes()); // (2) "credentialIdLength", len=2, 16-bit unsigned big-endian integer + data.extend(&cred.credential_id); // (3) "credentialId", len= see (2) + data.extend( + // (4) "credentialPublicKey", len=variable + &serde_cbor::to_vec(&cred.credential_public_key) + .map_err(|_| SerError::custom("Failed to serialize auth_data"))?, + ); + } + // If we have parsed extension data, then we should serialize it even if the authenticator + // failed to set the extension data flag. + // If we don't have parsed extension data, then what we output depends on the flag. + // If the flag is set, we output the empty CBOR map. If it is not set, we output nothing. + if self.extensions.has_some() || self.flags.contains(AuthenticatorDataFlags::EXTENSION_DATA) + { + data.extend( + // (5) "extensions", len=variable + &serde_cbor::to_vec(&self.extensions) + .map_err(|_| SerError::custom("Failed to serialize auth_data"))?, + ); + } + + serializer.serialize_bytes(&data) + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +/// x509 encoded attestation certificate +pub struct AttestationCertificate(#[serde(with = "serde_bytes")] pub Vec); + +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 Vec); + +impl fmt::Debug for Signature { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let value = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&self.0); + write!(f, "Signature({value})") + } +} + +impl AsRef<[u8]> for Signature { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl From<&[u8]> for Signature { + fn from(sig: &[u8]) -> Signature { + Signature(sig.to_vec()) + } +} + +#[derive(Debug, PartialEq, Eq, Deserialize)] +// The tag and content attributes here are really for AttestationObject, which contains an +// "internally tagged" AttestationStatement. +#[serde(tag = "fmt", content = "attStmt", rename_all = "lowercase")] +pub enum AttestationStatement { + #[serde(deserialize_with = "deserialize_none_att_stmt")] + None, + Packed(AttestationStatementPacked), + #[serde(rename = "fido-u2f")] + FidoU2F(AttestationStatementFidoU2F), + // The remaining attestation statement formats are deserialized as serde_cbor::Values---we do + // not perform any validation of their contents. These are expected to be used primarily when + // anonymizing attestation objects that contain attestation statements in these formats. + #[serde(rename = "android-key")] + AndroidKey(serde_cbor::Value), + #[serde(rename = "android-safetynet")] + AndroidSafetyNet(serde_cbor::Value), + Apple(serde_cbor::Value), + Tpm(serde_cbor::Value), +} + +// AttestationStatement::None is serialized as the empty map. We need to enforce +// the emptyness condition manually while deserializing. +fn deserialize_none_att_stmt<'de, D>(deserializer: D) -> Result<(), D::Error> +where + D: Deserializer<'de>, +{ + let map = >::deserialize(deserializer)?; + + if !map.is_empty() { + return Err(D::Error::invalid_value(Unexpected::Map, &"the empty map")); + } + + Ok(()) +} + +// 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 { +// 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-2/#sctn-fido-u2f-attestation +// u2fStmtFormat = { +// x5c: [ attestnCert: bytes ], +// sig: bytes +// } +pub struct AttestationStatementFidoU2F { + /// Certificate chain in x509 format + #[serde(rename = "x5c")] + pub attestation_cert: Vec, // (1) "x5c" + pub sig: Signature, // (2) "sig" +} + +impl AttestationStatementFidoU2F { + pub fn new(cert: &[u8], signature: &[u8]) -> Self { + AttestationStatementFidoU2F { + attestation_cert: vec![AttestationCertificate(Vec::from(cert))], + sig: Signature::from(signature), + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +// https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation +// packedStmtFormat = { +// alg: COSEAlgorithmIdentifier, +// sig: bytes, +// x5c: [ attestnCert: bytes, * (caCert: bytes) ] +// } // +// { +// alg: COSEAlgorithmIdentifier +// sig: bytes, +// } +pub struct AttestationStatementPacked { + pub alg: COSEAlgorithm, // (1) "alg" + pub sig: Signature, // (2) "sig" + /// Certificate chain in x509 format + #[serde(rename = "x5c", skip_serializing_if = "Vec::is_empty", default)] + pub attestation_cert: Vec, // (3) "x5c" +} + +// A WebAuthn attestation object is a CBOR map with keys "fmt", "attStmt", and "authData". The +// "fmt" field determines the type of "attStmt". The flatten attribute here turns the tag and +// content attributes on AttestationStatement (defined above) into expected keys for +// AttestationObject, which allows us to derive Deserialize. Like many of our other structs, the +// derived Deserialize implementation is permissive: it does not enforce CTAP2 canonical CBOR +// encoding and it allows repeated keys (the last one wins). +#[derive(Debug, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AttestationObject { + pub auth_data: AuthenticatorData, + #[serde(flatten)] + pub att_stmt: AttestationStatement, +} + +impl AttestationObject { + pub fn anonymize(&mut self) { + // Remove the attestation statement and the AAGUID from the authenticator data. + self.att_stmt = AttestationStatement::None; + if let Some(credential_data) = self.auth_data.credential_data.as_mut() { + credential_data.aaguid = AAGuid::default(); + } + } +} + +impl Serialize for AttestationObject { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let map_len = 3; + let mut map = serializer.serialize_map(Some(map_len))?; + + // CTAP2 canonical CBOR order for these entries is ("fmt", "attStmt", "authData") + // as strings are sorted by length and then lexically. + // see https://www.w3.org/TR/webauthn-2/#attestation-object + match self.att_stmt { + AttestationStatement::None => { + map.serialize_entry(&"fmt", &"none")?; // (1) "fmt" + let v = std::collections::BTreeMap::<(), ()>::new(); + map.serialize_entry(&"attStmt", &v)?; // (2) "attStmt" + } + AttestationStatement::Packed(ref v) => { + map.serialize_entry(&"fmt", &"packed")?; // (1) "fmt" + map.serialize_entry(&"attStmt", v)?; // (2) "attStmt" + } + AttestationStatement::FidoU2F(ref v) => { + map.serialize_entry(&"fmt", &"fido-u2f")?; // (1) "fmt" + map.serialize_entry(&"attStmt", v)?; // (2) "attStmt" + } + AttestationStatement::AndroidKey(ref v) => { + map.serialize_entry(&"fmt", &"android-key")?; // (1) "fmt" + map.serialize_entry(&"attStmt", v)?; // (2) "attStmt" + } + AttestationStatement::AndroidSafetyNet(ref v) => { + map.serialize_entry(&"fmt", &"android-safetynet")?; // (1) "fmt" + map.serialize_entry(&"attStmt", v)?; // (2) "attStmt" + } + AttestationStatement::Apple(ref v) => { + map.serialize_entry(&"fmt", &"apple")?; // (1) "fmt" + map.serialize_entry(&"attStmt", v)?; // (2) "attStmt" + } + AttestationStatement::Tpm(ref v) => { + map.serialize_entry(&"fmt", &"tpm")?; // (1) "fmt" + map.serialize_entry(&"attStmt", v)?; // (2) "attStmt" + } + } + map.serialize_entry(&"authData", &self.auth_data)?; // (3) "authData" + map.end() + } +} + +#[cfg(test)] +pub mod test { + use super::super::utils::from_slice_stream; + use super::*; + use crate::crypto::{COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve}; + use serde_cbor::{from_slice, to_vec}; + + const SAMPLE_ATTESTATION_STMT_NONE: [u8; 19] = [ + 0xa2, // map(2) + 0x63, // text(3) + 0x66, 0x6d, 0x74, // "fmt" + 0x64, // text(4) + 0x6e, 0x6f, 0x6e, 0x65, // "none" + 0x67, // text(7) + 0x61, 0x74, 0x74, 0x53, 0x74, 0x6d, 0x74, // "attStmt" + 0xa0, // map(0) + ]; + + const SAMPLE_ATTESTATION_STMT_FIDO_U2F: [u8; 840] = [ + 0xa2, // map(2) + 0x63, // text(3) + 0x66, 0x6d, 0x74, // "fmt" + 0x68, // text(8) + 0x66, 0x69, 0x64, 0x6f, 0x2d, 0x75, 0x32, 0x66, // "fido-u2f" + 0x67, // text(7) + 0x61, 0x74, 0x74, 0x53, 0x74, 0x6d, 0x74, // "attStmt" + 0xa2, // map(2) + 0x63, // text(3) + 0x78, 0x35, 0x63, // "x5c" + 0x81, // array(1) + 0x59, 0x02, 0xdd, // bytes(733) + 0x30, 0x82, 0x02, 0xd9, 0x30, 0x82, 0x01, 0xc1, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x09, + 0x00, 0xdf, 0x92, 0xd9, 0xc4, 0xe2, 0xed, 0x66, 0x0a, 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, 0x6f, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x53, + 0x45, 0x31, 0x12, 0x30, 0x10, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x09, 0x59, 0x75, 0x62, + 0x69, 0x63, 0x6f, 0x20, 0x41, 0x42, 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, 0x31, 0x28, 0x30, + 0x26, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x1f, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, + 0x55, 0x32, 0x46, 0x20, 0x45, 0x45, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x20, 0x31, + 0x31, 0x35, 0x35, 0x31, 0x30, 0x39, 0x35, 0x39, 0x39, 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, 0x0a, 0x18, 0x6c, 0x6e, 0x4d, 0x0a, 0x6a, 0x52, 0x8a, + 0x44, 0x90, 0x9a, 0x7a, 0x24, 0x23, 0x68, 0x70, 0x28, 0xd4, 0xc5, 0x7e, 0xcc, 0xb7, 0x17, + 0xba, 0x12, 0x80, 0xb8, 0x5c, 0x2f, 0xc1, 0xe4, 0xe0, 0x61, 0x66, 0x8c, 0x3c, 0x20, 0xae, + 0xf3, 0x33, 0x50, 0xd1, 0x96, 0x45, 0x23, 0x8a, 0x2c, 0x39, 0x0b, 0xf5, 0xdf, 0xfa, 0x34, + 0xff, 0x25, 0x50, 0x2f, 0x47, 0x0f, 0x3d, 0x40, 0xb8, 0x88, 0xa3, 0x81, 0x81, 0x30, 0x7f, + 0x30, 0x13, 0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xc4, 0x0a, 0x0d, 0x01, 0x04, + 0x05, 0x04, 0x03, 0x05, 0x04, 0x03, 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, 0x37, 0x30, 0x13, 0x06, + 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xe5, 0x1c, 0x02, 0x01, 0x01, 0x04, 0x04, 0x03, + 0x02, 0x04, 0x30, 0x30, 0x21, 0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xe5, 0x1c, + 0x01, 0x01, 0x04, 0x04, 0x12, 0x04, 0x10, 0x2f, 0xc0, 0x57, 0x9f, 0x81, 0x13, 0x47, 0xea, + 0xb1, 0x16, 0xbb, 0x5a, 0x8d, 0xb9, 0x20, 0x2a, 0x30, 0x0c, 0x06, 0x03, 0x55, 0x1d, 0x13, + 0x01, 0x01, 0xff, 0x04, 0x02, 0x30, 0x00, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, + 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, 0x00, 0x82, 0xac, 0xaf, + 0x11, 0x30, 0xa9, 0x9b, 0xd1, 0x43, 0x27, 0xd2, 0xf8, 0xf9, 0xb0, 0x41, 0xa2, 0xa0, 0x4a, + 0x66, 0x85, 0x27, 0x24, 0x22, 0xe5, 0x7b, 0x14, 0xb0, 0xb8, 0xf8, 0x3b, 0x6f, 0x15, 0x45, + 0x66, 0x4b, 0xbf, 0x55, 0x68, 0x1e, 0xaf, 0x01, 0x58, 0x72, 0x2a, 0xbf, 0xce, 0xd2, 0xe4, + 0xac, 0x63, 0x3c, 0xec, 0x09, 0x59, 0x56, 0x45, 0x24, 0xb0, 0xf2, 0xe5, 0x17, 0xdd, 0x97, + 0x10, 0x98, 0xb9, 0x89, 0x15, 0x17, 0xec, 0xd0, 0xc5, 0x53, 0xa2, 0xe4, 0x73, 0x9f, 0x9d, + 0xe1, 0x3d, 0xaf, 0xd0, 0xd5, 0xd7, 0xb8, 0xac, 0x4a, 0x37, 0xf4, 0xf2, 0xcc, 0x30, 0xef, + 0x25, 0xcb, 0x00, 0x65, 0x2d, 0x19, 0xdb, 0x69, 0xd7, 0xda, 0x57, 0xbd, 0x1a, 0x9c, 0x1d, + 0x8e, 0xd8, 0x7d, 0x46, 0xd8, 0x0d, 0x2b, 0x3b, 0xdf, 0xd1, 0xd9, 0xef, 0x9d, 0x2b, 0x68, + 0x32, 0xd4, 0xad, 0x5b, 0xcd, 0x74, 0x21, 0x4c, 0xe6, 0xa6, 0x14, 0x1d, 0x16, 0xb2, 0xe9, + 0x3a, 0xcb, 0x2c, 0x88, 0xf6, 0x0a, 0x3e, 0xb6, 0xd5, 0xf6, 0x14, 0x71, 0x97, 0x59, 0x09, + 0x37, 0x3b, 0xc6, 0x77, 0x90, 0x23, 0x24, 0x57, 0x1a, 0x57, 0x3f, 0x60, 0xf0, 0x7b, 0xbe, + 0xd1, 0x7b, 0x92, 0xc8, 0xb5, 0x9f, 0xa2, 0x82, 0x10, 0xbf, 0xa8, 0xc6, 0x01, 0x22, 0x93, + 0x00, 0x1b, 0x39, 0xef, 0xe5, 0x7b, 0xf9, 0xcb, 0x1e, 0x3a, 0xca, 0x8a, 0x41, 0x30, 0xf8, + 0x3a, 0xf8, 0x66, 0x8f, 0x73, 0xde, 0xf2, 0x71, 0x1b, 0x20, 0xdc, 0x99, 0xe8, 0xa8, 0x04, + 0xee, 0xa3, 0xf7, 0x42, 0x71, 0x97, 0xb6, 0xb4, 0x51, 0xb3, 0x73, 0x5c, 0x23, 0xbc, 0x9b, + 0x1b, 0xe2, 0x74, 0xc2, 0x6d, 0x3b, 0xf9, 0x19, 0x6f, 0x8c, 0x4a, 0x4b, 0x71, 0x5f, 0x4b, + 0x95, 0xc4, 0xdb, 0x7b, 0x97, 0xe7, 0x59, 0x4e, 0xb4, 0x65, 0x64, 0x8c, 0x1c, 0x63, 0x73, + 0x69, 0x67, // "sig" + 0x58, 0x46, // bytes(70) + 0x30, 0x44, 0x02, 0x20, 0x48, 0x5a, 0x72, 0x40, 0xdf, 0x2c, 0x1e, 0x31, 0xa5, 0xb3, 0x0b, + 0x3b, 0x2c, 0xd1, 0xad, 0xd0, 0x8d, 0xae, 0x8d, 0x7a, 0x25, 0x3e, 0xf5, 0xa6, 0x25, 0xdb, + 0x2e, 0x22, 0x1b, 0x71, 0xe5, 0x78, 0x02, 0x20, 0x45, 0xbd, 0xdc, 0x30, 0xde, 0xf4, 0x05, + 0x97, 0x5c, 0xac, 0x72, 0x58, 0x96, 0xa6, 0x00, 0x94, 0x57, 0x3a, 0xa5, 0xe8, 0x1e, 0xf4, + 0xfd, 0x30, 0xd3, 0x88, 0x11, 0x8b, 0x49, 0x97, 0xdf, 0x34, + ]; + + const SAMPLE_ATTESTATION_OBJ_PACKED: [u8; 677] = [ + 0xa3, // map(3) + 0x63, // text(3) + 0x66, 0x6D, 0x74, // "fmt" + 0x66, // text(6) + 0x70, 0x61, 0x63, 0x6b, 0x65, 0x64, // "packed" + 0x67, // text(7) + 0x61, 0x74, 0x74, 0x53, 0x74, 0x6D, 0x74, // "attStmt" + 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, 0x68, // text(8) + 0x61, 0x75, 0x74, 0x68, 0x44, 0x61, 0x74, 0x61, // "authData" + 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, + ]; + + 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, + ]; + + pub fn create_attestation_obj() -> AttestationObject { + 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: Curve::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_stmt: AttestationStatement::Packed(AttestationStatementPacked { + alg: COSEAlgorithm::ES256, + sig: Signature(vec![ + 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, + ])], + }), + } + } + + #[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 = from_slice(&SAMPLE_CERT_CHAIN).unwrap(); + } + + #[test] + fn parse_attestation_statement() { + let actual: AttestationStatement = from_slice(&SAMPLE_ATTESTATION_STMT_NONE).unwrap(); + let expected = AttestationStatement::None; + assert_eq!(expected, actual); + + let actual: AttestationStatement = from_slice(&SAMPLE_ATTESTATION_STMT_FIDO_U2F).unwrap(); + let expected = AttestationStatement::FidoU2F(AttestationStatementFidoU2F { + attestation_cert: vec![AttestationCertificate(vec![ + 0x30, 0x82, 0x02, 0xd9, 0x30, 0x82, 0x01, 0xc1, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, + 0x09, 0x00, 0xdf, 0x92, 0xd9, 0xc4, 0xe2, 0xed, 0x66, 0x0a, 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, 0x6f, 0x31, 0x0b, 0x30, + 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x53, 0x45, 0x31, 0x12, 0x30, 0x10, + 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x09, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, + 0x41, 0x42, 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, 0x31, 0x28, 0x30, 0x26, + 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x1f, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, + 0x55, 0x32, 0x46, 0x20, 0x45, 0x45, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x20, + 0x31, 0x31, 0x35, 0x35, 0x31, 0x30, 0x39, 0x35, 0x39, 0x39, 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, 0x0a, 0x18, 0x6c, 0x6e, 0x4d, + 0x0a, 0x6a, 0x52, 0x8a, 0x44, 0x90, 0x9a, 0x7a, 0x24, 0x23, 0x68, 0x70, 0x28, 0xd4, + 0xc5, 0x7e, 0xcc, 0xb7, 0x17, 0xba, 0x12, 0x80, 0xb8, 0x5c, 0x2f, 0xc1, 0xe4, 0xe0, + 0x61, 0x66, 0x8c, 0x3c, 0x20, 0xae, 0xf3, 0x33, 0x50, 0xd1, 0x96, 0x45, 0x23, 0x8a, + 0x2c, 0x39, 0x0b, 0xf5, 0xdf, 0xfa, 0x34, 0xff, 0x25, 0x50, 0x2f, 0x47, 0x0f, 0x3d, + 0x40, 0xb8, 0x88, 0xa3, 0x81, 0x81, 0x30, 0x7f, 0x30, 0x13, 0x06, 0x0a, 0x2b, 0x06, + 0x01, 0x04, 0x01, 0x82, 0xc4, 0x0a, 0x0d, 0x01, 0x04, 0x05, 0x04, 0x03, 0x05, 0x04, + 0x03, 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, 0x37, 0x30, 0x13, 0x06, 0x0b, 0x2b, + 0x06, 0x01, 0x04, 0x01, 0x82, 0xe5, 0x1c, 0x02, 0x01, 0x01, 0x04, 0x04, 0x03, 0x02, + 0x04, 0x30, 0x30, 0x21, 0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xe5, 0x1c, + 0x01, 0x01, 0x04, 0x04, 0x12, 0x04, 0x10, 0x2f, 0xc0, 0x57, 0x9f, 0x81, 0x13, 0x47, + 0xea, 0xb1, 0x16, 0xbb, 0x5a, 0x8d, 0xb9, 0x20, 0x2a, 0x30, 0x0c, 0x06, 0x03, 0x55, + 0x1d, 0x13, 0x01, 0x01, 0xff, 0x04, 0x02, 0x30, 0x00, 0x30, 0x0d, 0x06, 0x09, 0x2a, + 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, + 0x00, 0x82, 0xac, 0xaf, 0x11, 0x30, 0xa9, 0x9b, 0xd1, 0x43, 0x27, 0xd2, 0xf8, 0xf9, + 0xb0, 0x41, 0xa2, 0xa0, 0x4a, 0x66, 0x85, 0x27, 0x24, 0x22, 0xe5, 0x7b, 0x14, 0xb0, + 0xb8, 0xf8, 0x3b, 0x6f, 0x15, 0x45, 0x66, 0x4b, 0xbf, 0x55, 0x68, 0x1e, 0xaf, 0x01, + 0x58, 0x72, 0x2a, 0xbf, 0xce, 0xd2, 0xe4, 0xac, 0x63, 0x3c, 0xec, 0x09, 0x59, 0x56, + 0x45, 0x24, 0xb0, 0xf2, 0xe5, 0x17, 0xdd, 0x97, 0x10, 0x98, 0xb9, 0x89, 0x15, 0x17, + 0xec, 0xd0, 0xc5, 0x53, 0xa2, 0xe4, 0x73, 0x9f, 0x9d, 0xe1, 0x3d, 0xaf, 0xd0, 0xd5, + 0xd7, 0xb8, 0xac, 0x4a, 0x37, 0xf4, 0xf2, 0xcc, 0x30, 0xef, 0x25, 0xcb, 0x00, 0x65, + 0x2d, 0x19, 0xdb, 0x69, 0xd7, 0xda, 0x57, 0xbd, 0x1a, 0x9c, 0x1d, 0x8e, 0xd8, 0x7d, + 0x46, 0xd8, 0x0d, 0x2b, 0x3b, 0xdf, 0xd1, 0xd9, 0xef, 0x9d, 0x2b, 0x68, 0x32, 0xd4, + 0xad, 0x5b, 0xcd, 0x74, 0x21, 0x4c, 0xe6, 0xa6, 0x14, 0x1d, 0x16, 0xb2, 0xe9, 0x3a, + 0xcb, 0x2c, 0x88, 0xf6, 0x0a, 0x3e, 0xb6, 0xd5, 0xf6, 0x14, 0x71, 0x97, 0x59, 0x09, + 0x37, 0x3b, 0xc6, 0x77, 0x90, 0x23, 0x24, 0x57, 0x1a, 0x57, 0x3f, 0x60, 0xf0, 0x7b, + 0xbe, 0xd1, 0x7b, 0x92, 0xc8, 0xb5, 0x9f, 0xa2, 0x82, 0x10, 0xbf, 0xa8, 0xc6, 0x01, + 0x22, 0x93, 0x00, 0x1b, 0x39, 0xef, 0xe5, 0x7b, 0xf9, 0xcb, 0x1e, 0x3a, 0xca, 0x8a, + 0x41, 0x30, 0xf8, 0x3a, 0xf8, 0x66, 0x8f, 0x73, 0xde, 0xf2, 0x71, 0x1b, 0x20, 0xdc, + 0x99, 0xe8, 0xa8, 0x04, 0xee, 0xa3, 0xf7, 0x42, 0x71, 0x97, 0xb6, 0xb4, 0x51, 0xb3, + 0x73, 0x5c, 0x23, 0xbc, 0x9b, 0x1b, 0xe2, 0x74, 0xc2, 0x6d, 0x3b, 0xf9, 0x19, 0x6f, + 0x8c, 0x4a, 0x4b, 0x71, 0x5f, 0x4b, 0x95, 0xc4, 0xdb, 0x7b, 0x97, 0xe7, 0x59, 0x4e, + 0xb4, 0x65, 0x64, 0x8c, 0x1c, + ])], + sig: Signature(vec![ + 0x30, 0x44, 0x02, 0x20, 0x48, 0x5a, 0x72, 0x40, 0xdf, 0x2c, 0x1e, 0x31, 0xa5, 0xb3, + 0x0b, 0x3b, 0x2c, 0xd1, 0xad, 0xd0, 0x8d, 0xae, 0x8d, 0x7a, 0x25, 0x3e, 0xf5, 0xa6, + 0x25, 0xdb, 0x2e, 0x22, 0x1b, 0x71, 0xe5, 0x78, 0x02, 0x20, 0x45, 0xbd, 0xdc, 0x30, + 0xde, 0xf4, 0x05, 0x97, 0x5c, 0xac, 0x72, 0x58, 0x96, 0xa6, 0x00, 0x94, 0x57, 0x3a, + 0xa5, 0xe8, 0x1e, 0xf4, 0xfd, 0x30, 0xd3, 0x88, 0x11, 0x8b, 0x49, 0x97, 0xdf, 0x34, + ]), + }); + assert_eq!(expected, actual); + } + + #[test] + fn parse_attestation_object() { + let actual: AttestationObject = from_slice(&SAMPLE_ATTESTATION_OBJ_PACKED).unwrap(); + let expected = create_attestation_obj(); + + assert_eq!(expected, actual); + } + + #[test] + fn test_anonymize_att_obj() { + // Anonymize should prevent identifying data in the attestation statement from being + // serialized. + let mut att_obj = create_attestation_obj(); + + // This test assumes that the sample attestation object contains identifying information + assert_ne!(att_obj.att_stmt, AttestationStatement::None); + assert_ne!( + att_obj + .auth_data + .credential_data + .as_ref() + .expect("credential_data should be Some") + .aaguid, + AAGuid::default() + ); + + att_obj.anonymize(); + + // Write the attestation object out to bytes and read it back. The result should not + // have an attestation statement, and it should have the default AAGUID. + let encoded_att_obj = to_vec(&att_obj).expect("could not serialize anonymized att_obj"); + let att_obj: AttestationObject = + from_slice(&encoded_att_obj).expect("could not deserialize anonymized att_obj"); + + assert_eq!(att_obj.att_stmt, AttestationStatement::None); + assert_eq!( + att_obj + .auth_data + .credential_data + .as_ref() + .expect("credential_data should be Some") + .aaguid, + AAGuid::default() + ); + } + + #[test] + fn parse_reader() { + let v: Vec = vec![ + 0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72, 0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72, + ]; + let mut data = Cursor::new(v); + let value: String = from_slice_stream::<_, _, serde_cbor::Error>(&mut data).unwrap(); + assert_eq!(value, "foobar"); + let mut remaining = Vec::new(); + data.read_to_end(&mut remaining).unwrap(); + assert_eq!( + remaining.as_slice(), + &[0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72] + ); + let mut data = Cursor::new(remaining); + let value: String = from_slice_stream::<_, _, serde_cbor::Error>(&mut data).unwrap(); + assert_eq!(value, "foobar"); + let mut remaining = Vec::new(); + data.read_to_end(&mut remaining).unwrap(); + assert!(remaining.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, + ])) + ); + } + + #[test] + fn test_empty_extension_data() { + let mut parsed_auth_data: AuthenticatorData = + from_slice(&SAMPLE_AUTH_DATA_MAKE_CREDENTIAL).unwrap(); + assert!(parsed_auth_data + .flags + .contains(AuthenticatorDataFlags::EXTENSION_DATA)); + + // Remove the extension data but keep the extension data flag set. + parsed_auth_data.extensions = Default::default(); + let with_flag = to_vec(&parsed_auth_data).expect("could not serialize auth data"); + // The serialized auth data should end with an empty map (CBOR 0xA0). + assert_eq!(with_flag[with_flag.len() - 1], 0xA0); + + // Remove the extension data flag. + parsed_auth_data + .flags + .remove(AuthenticatorDataFlags::EXTENSION_DATA); + let without_flag = to_vec(&parsed_auth_data).expect("could not serialize auth data"); + // The serialized auth data should be one byte shorter. + assert!(with_flag.len() == without_flag.len() + 1); + } + + /// 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); + } + + #[test] + fn test_ad_flags_from_bits() { + // Check that AuthenticatorDataFlags is defined on the entire u8 range and that + // `from_bits_truncate` is lossless + for x in 0..=u8::MAX { + assert_eq!( + AuthenticatorDataFlags::from_bits(x), + Some(AuthenticatorDataFlags::from_bits_truncate(x)) + ); + } + } +} 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..872a119e0c --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/client_data.rs @@ -0,0 +1,339 @@ +use super::commands::CommandError; +use crate::transport::errors::HIDError; +use base64::Engine; +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(&self, serializer: S) -> Result + 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(deserializer: D) -> Result + 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(self, mut map: M) -> Result + 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 => 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(&self, serializer: S) -> Result + 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(deserializer: D) -> Result + 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(self, v: &str) -> Result + 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) -> Self { + let value = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(input); + Challenge(value) + } +} + +impl From> for Challenge { + fn from(v: Vec) -> 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, +} + +impl CollectedClientData { + pub fn hash(&self) -> Result { + // 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 json = json::to_vec(&self).map_err(CommandError::Json)?; + let digest = Sha256::digest(json); + Ok(ClientDataHash(digest.into())) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ClientDataHash(pub [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(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_bytes(&self.0) + } +} + +#[cfg(test)] +mod test { + 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())), + }; + assert_eq!( + client_data.hash().expect("failed to serialize client data"), + // echo -n '{"type":"webauthn.create","challenge":"AAECAw","origin":"example.com","crossOrigin":false,"tokenBinding":{"status":"present","id":"AAECAw"}}' | sha256sum -t + 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 + ]) + ); + } +} diff --git a/third_party/rust/authenticator/src/ctap2/commands/authenticator_config.rs b/third_party/rust/authenticator/src/ctap2/commands/authenticator_config.rs new file mode 100644 index 0000000000..f72640d701 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/authenticator_config.rs @@ -0,0 +1,225 @@ +use super::{Command, CommandError, PinUvAuthCommand, RequestCtap2, StatusCode}; +use crate::{ + crypto::{PinUvAuthParam, PinUvAuthToken}, + ctap2::server::UserVerificationRequirement, + errors::AuthenticatorError, + transport::errors::HIDError, + AuthenticatorInfo, FidoDevice, +}; +use serde::{ser::SerializeMap, Deserialize, Serialize, Serializer}; +use serde_cbor::{de::from_slice, to_vec, Value}; + +#[derive(Debug, Clone, Deserialize)] +pub struct SetMinPINLength { + /// Minimum PIN length in code points + pub new_min_pin_length: Option, + /// RP IDs which are allowed to get this information via the minPinLength extension. + /// This parameter MUST NOT be used unless the minPinLength extension is supported. + pub min_pin_length_rpids: Option>, + /// The authenticator returns CTAP2_ERR_PIN_POLICY_VIOLATION until changePIN is successful. + pub force_change_pin: Option, +} + +impl Serialize for SetMinPINLength { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map_len = 0; + if self.new_min_pin_length.is_some() { + map_len += 1; + } + if self.min_pin_length_rpids.is_some() { + map_len += 1; + } + if self.force_change_pin.is_some() { + map_len += 1; + } + + let mut map = serializer.serialize_map(Some(map_len))?; + if let Some(new_min_pin_length) = self.new_min_pin_length { + map.serialize_entry(&0x01, &new_min_pin_length)?; + } + if let Some(min_pin_length_rpids) = &self.min_pin_length_rpids { + map.serialize_entry(&0x02, &min_pin_length_rpids)?; + } + if let Some(force_change_pin) = self.force_change_pin { + map.serialize_entry(&0x03, &force_change_pin)?; + } + map.end() + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub enum AuthConfigCommand { + EnableEnterpriseAttestation, + ToggleAlwaysUv, + SetMinPINLength(SetMinPINLength), +} + +impl AuthConfigCommand { + fn has_params(&self) -> bool { + match self { + AuthConfigCommand::EnableEnterpriseAttestation => false, + AuthConfigCommand::ToggleAlwaysUv => false, + AuthConfigCommand::SetMinPINLength(..) => true, + } + } +} + +#[derive(Debug, Serialize)] +pub enum AuthConfigResult { + Success(AuthenticatorInfo), +} + +#[derive(Debug)] +pub struct AuthenticatorConfig { + subcommand: AuthConfigCommand, // subCommand currently being requested + pin_uv_auth_param: Option, // First 16 bytes of HMAC-SHA-256 of contents using pinUvAuthToken. +} + +impl AuthenticatorConfig { + pub(crate) fn new(subcommand: AuthConfigCommand) -> Self { + Self { + subcommand, + pin_uv_auth_param: None, + } + } +} + +impl Serialize for AuthenticatorConfig { + fn serialize(&self, serializer: S) -> Result + 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_uv_auth_param.is_some() { + map_len += 2; + } + if self.subcommand.has_params() { + map_len += 1; + } + + let mut map = serializer.serialize_map(Some(map_len))?; + + match &self.subcommand { + AuthConfigCommand::EnableEnterpriseAttestation => { + map.serialize_entry(&0x01, &0x01)?; + } + AuthConfigCommand::ToggleAlwaysUv => { + map.serialize_entry(&0x01, &0x02)?; + } + AuthConfigCommand::SetMinPINLength(params) => { + map.serialize_entry(&0x01, &0x03)?; + map.serialize_entry(&0x02, ¶ms)?; + } + } + + if let Some(ref pin_uv_auth_param) = self.pin_uv_auth_param { + map.serialize_entry(&0x03, &pin_uv_auth_param.pin_protocol.id())?; + map.serialize_entry(&0x04, pin_uv_auth_param)?; + } + + map.end() + } +} + +impl RequestCtap2 for AuthenticatorConfig { + type Output = (); + + fn command(&self) -> Command { + Command::AuthenticatorConfig + } + + fn wire_format(&self) -> Result, HIDError> { + let output = to_vec(&self).map_err(CommandError::Serializing)?; + trace!("client subcommmand: {:04X?}", &output); + Ok(output) + } + + fn handle_response_ctap2( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result + where + Dev: FidoDevice, + { + 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()) + } + } + + fn send_to_virtual_device( + &self, + _dev: &mut Dev, + ) -> Result { + unimplemented!() + } +} + +impl PinUvAuthCommand for AuthenticatorConfig { + fn set_pin_uv_auth_param( + &mut self, + pin_uv_auth_token: Option, + ) -> Result<(), AuthenticatorError> { + let mut param = None; + if let Some(token) = pin_uv_auth_token { + // pinUvAuthParam (0x04): the result of calling + // authenticate(pinUvAuthToken, 32×0xff || 0x0d || uint8(subCommand) || subCommandParams). + let mut data = vec![0xff; 32]; + data.push(0x0D); + match &self.subcommand { + AuthConfigCommand::EnableEnterpriseAttestation => { + data.push(0x01); + } + AuthConfigCommand::ToggleAlwaysUv => { + data.push(0x02); + } + AuthConfigCommand::SetMinPINLength(params) => { + data.push(0x03); + data.extend(to_vec(params).map_err(CommandError::Serializing)?); + } + } + param = Some(token.derive(&data).map_err(CommandError::Crypto)?); + } + self.pin_uv_auth_param = param; + Ok(()) + } + + fn can_skip_user_verification( + &mut self, + authinfo: &AuthenticatorInfo, + _uv_req: UserVerificationRequirement, + ) -> bool { + !authinfo.device_is_protected() + } + + fn set_uv_option(&mut self, _uv: Option) { + /* No-op */ + } + + fn get_pin_uv_auth_param(&self) -> Option<&PinUvAuthParam> { + self.pin_uv_auth_param.as_ref() + } + + fn get_rp_id(&self) -> Option<&String> { + None + } +} diff --git a/third_party/rust/authenticator/src/ctap2/commands/bio_enrollment.rs b/third_party/rust/authenticator/src/ctap2/commands/bio_enrollment.rs new file mode 100644 index 0000000000..817bb5ac51 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/bio_enrollment.rs @@ -0,0 +1,660 @@ +use crate::{ + crypto::{PinUvAuthParam, PinUvAuthToken}, + ctap2::server::UserVerificationRequirement, + errors::AuthenticatorError, + transport::errors::HIDError, + AuthenticatorInfo, FidoDevice, +}; +use serde::{ + de::{Error as SerdeError, IgnoredAny, MapAccess, Visitor}, + ser::SerializeMap, + Deserialize, Deserializer, Serialize, Serializer, +}; +use serde_bytes::ByteBuf; +use serde_cbor::{from_slice, to_vec, Value}; +use std::fmt; + +use super::{Command, CommandError, CtapResponse, PinUvAuthCommand, RequestCtap2, StatusCode}; + +#[derive(Debug, Clone, Copy)] +pub enum BioEnrollmentModality { + Fingerprint, + Other(u8), +} + +impl From for BioEnrollmentModality { + fn from(value: u8) -> Self { + match value { + 0x01 => BioEnrollmentModality::Fingerprint, + x => BioEnrollmentModality::Other(x), + } + } +} + +impl From for u8 { + fn from(value: BioEnrollmentModality) -> Self { + match value { + BioEnrollmentModality::Fingerprint => 0x01, + BioEnrollmentModality::Other(x) => x, + } + } +} + +impl Serialize for BioEnrollmentModality { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_u8((*self).into()) + } +} + +impl<'de> Deserialize<'de> for BioEnrollmentModality { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct BioEnrollmentModalityVisitor; + + impl<'de> Visitor<'de> for BioEnrollmentModalityVisitor { + type Value = BioEnrollmentModality; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an integer") + } + + fn visit_u8(self, v: u8) -> Result + where + E: SerdeError, + { + Ok(BioEnrollmentModality::from(v)) + } + } + + deserializer.deserialize_u8(BioEnrollmentModalityVisitor) + } +} + +pub type BioTemplateId = Vec; +#[derive(Debug, Clone, Deserialize, Default)] +struct BioEnrollmentParams { + template_id: Option, // Template Identifier. + template_friendly_name: Option, // Template Friendly Name. + timeout_milliseconds: Option, // Timeout in milliSeconds. +} + +impl BioEnrollmentParams { + fn has_some(&self) -> bool { + self.template_id.is_some() + || self.template_friendly_name.is_some() + || self.timeout_milliseconds.is_some() + } +} + +impl Serialize for BioEnrollmentParams { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map_len = 0; + if self.template_id.is_some() { + map_len += 1; + } + if self.template_friendly_name.is_some() { + map_len += 1; + } + if self.timeout_milliseconds.is_some() { + map_len += 1; + } + + let mut map = serializer.serialize_map(Some(map_len))?; + if let Some(template_id) = &self.template_id { + map.serialize_entry(&0x01, &ByteBuf::from(template_id.as_slice()))?; + } + if let Some(template_friendly_name) = &self.template_friendly_name { + map.serialize_entry(&0x02, template_friendly_name)?; + } + if let Some(timeout_milliseconds) = &self.timeout_milliseconds { + map.serialize_entry(&0x03, timeout_milliseconds)?; + } + map.end() + } +} + +#[derive(Debug)] +pub enum BioEnrollmentCommand { + EnrollBegin(Option), + EnrollCaptureNextSample((BioTemplateId, Option)), + CancelCurrentEnrollment, + EnumerateEnrollments, + SetFriendlyName((BioTemplateId, String)), + RemoveEnrollment(BioTemplateId), + GetFingerprintSensorInfo, +} + +impl BioEnrollmentCommand { + fn to_id_and_param(&self) -> (u8, BioEnrollmentParams) { + let mut params = BioEnrollmentParams::default(); + match &self { + BioEnrollmentCommand::EnrollBegin(timeout) => { + params.timeout_milliseconds = *timeout; + (0x01, params) + } + BioEnrollmentCommand::EnrollCaptureNextSample((id, timeout)) => { + params.template_id = Some(id.clone()); + params.timeout_milliseconds = *timeout; + (0x02, params) + } + BioEnrollmentCommand::CancelCurrentEnrollment => (0x03, params), + BioEnrollmentCommand::EnumerateEnrollments => (0x04, params), + BioEnrollmentCommand::SetFriendlyName((id, name)) => { + params.template_id = Some(id.clone()); + params.template_friendly_name = Some(name.clone()); + (0x05, params) + } + BioEnrollmentCommand::RemoveEnrollment(id) => { + params.template_id = Some(id.clone()); + (0x06, params) + } + BioEnrollmentCommand::GetFingerprintSensorInfo => (0x07, params), + } + } +} + +#[derive(Debug)] +pub struct BioEnrollment { + /// The user verification modality being requested + modality: BioEnrollmentModality, + /// The authenticator user verification sub command currently being requested + pub(crate) subcommand: BioEnrollmentCommand, + /// First 16 bytes of HMAC-SHA-256 of contents using pinUvAuthToken. + pin_uv_auth_param: Option, + use_legacy_preview: bool, +} + +impl BioEnrollment { + pub(crate) fn new(subcommand: BioEnrollmentCommand, use_legacy_preview: bool) -> Self { + Self { + modality: BioEnrollmentModality::Fingerprint, // As per spec: Currently always "Fingerprint" + subcommand, + pin_uv_auth_param: None, + use_legacy_preview, + } + } +} + +impl Serialize for BioEnrollment { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Need to define how many elements are going to be in the map + // beforehand + let mut map_len = 2; + let (id, params) = self.subcommand.to_id_and_param(); + if params.has_some() { + map_len += 1; + } + if self.pin_uv_auth_param.is_some() { + map_len += 2; + } + + let mut map = serializer.serialize_map(Some(map_len))?; + + map.serialize_entry(&0x01, &self.modality)?; // Per spec currently always Fingerprint + map.serialize_entry(&0x02, &id)?; + if params.has_some() { + map.serialize_entry(&0x03, ¶ms)?; + } + + if let Some(ref pin_uv_auth_param) = self.pin_uv_auth_param { + map.serialize_entry(&0x04, &pin_uv_auth_param.pin_protocol.id())?; + map.serialize_entry(&0x05, pin_uv_auth_param)?; + } + + map.end() + } +} + +impl PinUvAuthCommand for BioEnrollment { + fn get_rp_id(&self) -> Option<&String> { + None + } + + fn set_pin_uv_auth_param( + &mut self, + pin_uv_auth_token: Option, + ) -> Result<(), AuthenticatorError> { + let mut param = None; + if let Some(token) = pin_uv_auth_token { + // pinUvAuthParam (0x04): the result of calling + // authenticate(pinUvAuthToken, fingerprint (0x01) || uint8(subCommand) || subCommandParams). + let (id, params) = self.subcommand.to_id_and_param(); + let modality = self.modality.into(); + let mut data = vec![modality, id]; + if params.has_some() { + data.extend(to_vec(¶ms).map_err(CommandError::Serializing)?); + } + param = Some(token.derive(&data).map_err(CommandError::Crypto)?); + } + self.pin_uv_auth_param = param; + Ok(()) + } + + fn can_skip_user_verification( + &mut self, + _info: &crate::AuthenticatorInfo, + _uv: UserVerificationRequirement, + ) -> bool { + // "discouraged" does not exist for BioEnrollment + false + } + + fn set_uv_option(&mut self, _uv: Option) { + /* No-op */ + } + + fn get_pin_uv_auth_param(&self) -> Option<&PinUvAuthParam> { + self.pin_uv_auth_param.as_ref() + } +} + +impl RequestCtap2 for BioEnrollment { + type Output = BioEnrollmentResponse; + + fn command(&self) -> Command { + if self.use_legacy_preview { + Command::BioEnrollmentPreview + } else { + Command::BioEnrollment + } + } + + fn wire_format(&self) -> Result, HIDError> { + let output = to_vec(&self).map_err(CommandError::Serializing)?; + trace!("client subcommmand: {:04X?}", &output); + Ok(output) + } + + fn handle_response_ctap2( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result + where + Dev: FidoDevice, + { + if input.is_empty() { + return Err(CommandError::InputTooSmall.into()); + } + + let status: StatusCode = input[0].into(); + if status.is_ok() { + if input.len() > 1 { + trace!("parsing bio enrollment response data: {:#04X?}", &input); + let bio_enrollment = + from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + Ok(bio_enrollment) + } else { + // Some subcommands return only an OK-status without any data + Ok(BioEnrollmentResponse::default()) + } + } else { + let data: Option = if input.len() > 1 { + Some(from_slice(&input[1..]).map_err(CommandError::Deserializing)?) + } else { + None + }; + Err(CommandError::StatusCode(status, data).into()) + } + } + + fn send_to_virtual_device( + &self, + _dev: &mut Dev, + ) -> Result { + unimplemented!() + } +} + +#[derive(Debug, Copy, Clone, Serialize)] +pub enum LastEnrollmentSampleStatus { + /// Good fingerprint capture. + Ctap2EnrollFeedbackFpGood, + /// Fingerprint was too high. + Ctap2EnrollFeedbackFpTooHigh, + /// Fingerprint was too low. + Ctap2EnrollFeedbackFpTooLow, + /// Fingerprint was too left. + Ctap2EnrollFeedbackFpTooLeft, + /// Fingerprint was too right. + Ctap2EnrollFeedbackFpTooRight, + /// Fingerprint was too fast. + Ctap2EnrollFeedbackFpTooFast, + /// Fingerprint was too slow. + Ctap2EnrollFeedbackFpTooSlow, + /// Fingerprint was of poor quality. + Ctap2EnrollFeedbackFpPoorQuality, + /// Fingerprint was too skewed. + Ctap2EnrollFeedbackFpTooSkewed, + /// Fingerprint was too short. + Ctap2EnrollFeedbackFpTooShort, + /// Merge failure of the capture. + Ctap2EnrollFeedbackFpMergeFailure, + /// Fingerprint already exists. + Ctap2EnrollFeedbackFpExists, + /// (this error number is available) + Unused, + /// User did not touch/swipe the authenticator. + Ctap2EnrollFeedbackNoUserActivity, + /// User did not lift the finger off the sensor. + Ctap2EnrollFeedbackNoUserPresenceTransition, + /// Other possible failure cases that are not (yet) defined by the spec + Ctap2EnrollFeedbackOther(u8), +} + +impl<'de> Deserialize<'de> for LastEnrollmentSampleStatus { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct LastEnrollmentSampleStatusVisitor; + + impl<'de> Visitor<'de> for LastEnrollmentSampleStatusVisitor { + type Value = LastEnrollmentSampleStatus; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an integer") + } + + fn visit_u8(self, v: u8) -> Result + where + E: SerdeError, + { + match v { + 0x00 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpGood), + 0x01 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooHigh), + 0x02 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooLow), + 0x03 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooLeft), + 0x04 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooRight), + 0x05 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooFast), + 0x06 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooSlow), + 0x07 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpPoorQuality), + 0x08 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooSkewed), + 0x09 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooShort), + 0x0A => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpMergeFailure), + 0x0B => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpExists), + 0x0C => Ok(LastEnrollmentSampleStatus::Unused), + 0x0D => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackNoUserActivity), + 0x0E => { + Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackNoUserPresenceTransition) + } + x => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackOther(x)), + } + } + } + + deserializer.deserialize_u8(LastEnrollmentSampleStatusVisitor) + } +} + +#[derive(Debug, Copy, Clone, Serialize)] +pub enum FingerprintKind { + TouchSensor, + SwipeSensor, + // Not (yet) defined by the spec + Other(u8), +} + +impl<'de> Deserialize<'de> for FingerprintKind { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct FingerprintKindVisitor; + + impl<'de> Visitor<'de> for FingerprintKindVisitor { + type Value = FingerprintKind; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an integer") + } + + fn visit_u8(self, v: u8) -> Result + where + E: SerdeError, + { + match v { + 0x01 => Ok(FingerprintKind::TouchSensor), + 0x02 => Ok(FingerprintKind::SwipeSensor), + x => Ok(FingerprintKind::Other(x)), + } + } + } + + deserializer.deserialize_u8(FingerprintKindVisitor) + } +} + +#[derive(Debug, Serialize)] +pub(crate) struct BioTemplateInfo { + template_id: BioTemplateId, + template_friendly_name: Option, +} + +impl<'de> Deserialize<'de> for BioTemplateInfo { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct BioTemplateInfoResponseVisitor; + + impl<'de> Visitor<'de> for BioTemplateInfoResponseVisitor { + type Value = BioTemplateInfo; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'de>, + { + let mut template_id = None; // (0x01) + let mut template_friendly_name = None; // (0x02) + while let Some(key) = map.next_key()? { + match key { + 0x01 => { + if template_id.is_some() { + return Err(SerdeError::duplicate_field("template_id")); + } + template_id = Some(map.next_value::()?.into_vec()); + } + 0x02 => { + if template_friendly_name.is_some() { + return Err(SerdeError::duplicate_field("template_friendly_name")); + } + template_friendly_name = Some(map.next_value()?); + } + k => { + warn!("BioTemplateInfo: unexpected key: {:?}", k); + let _ = map.next_value::()?; + continue; + } + } + } + + if let Some(template_id) = template_id { + Ok(BioTemplateInfo { + template_id, + template_friendly_name, + }) + } else { + Err(SerdeError::missing_field("template_id")) + } + } + } + deserializer.deserialize_bytes(BioTemplateInfoResponseVisitor) + } +} + +#[derive(Default, Debug)] +pub struct BioEnrollmentResponse { + /// The user verification modality. + pub(crate) modality: Option, + /// Indicates the type of fingerprint sensor. For touch type sensor, its value is 1. For swipe type sensor its value is 2. + pub(crate) fingerprint_kind: Option, + /// Indicates the maximum good samples required for enrollment. + pub(crate) max_capture_samples_required_for_enroll: Option, + /// Template Identifier. + pub(crate) template_id: Option, + /// Last enrollment sample status. + pub(crate) last_enroll_sample_status: Option, + /// Number of more sample required for enrollment to complete + pub(crate) remaining_samples: Option, + /// Array of templateInfo’s + pub(crate) template_infos: Vec, + /// Indicates the maximum number of bytes the authenticator will accept as a templateFriendlyName. + pub(crate) max_template_friendly_name: Option, +} + +impl CtapResponse for BioEnrollmentResponse {} + +impl<'de> Deserialize<'de> for BioEnrollmentResponse { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct BioEnrollmentResponseVisitor; + + impl<'de> Visitor<'de> for BioEnrollmentResponseVisitor { + type Value = BioEnrollmentResponse; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'de>, + { + let mut modality = None; // (0x01) + let mut fingerprint_kind = None; // (0x02) + let mut max_capture_samples_required_for_enroll = None; // (0x03) + let mut template_id = None; // (0x04) + let mut last_enroll_sample_status = None; // (0x05) + let mut remaining_samples = None; // (0x06) + let mut template_infos = None; // (0x07) + let mut max_template_friendly_name = None; // (0x08) + + while let Some(key) = map.next_key()? { + match key { + 0x01 => { + if modality.is_some() { + return Err(SerdeError::duplicate_field("modality")); + } + modality = Some(map.next_value()?); + } + 0x02 => { + if fingerprint_kind.is_some() { + return Err(SerdeError::duplicate_field("fingerprint_kind")); + } + fingerprint_kind = Some(map.next_value()?); + } + 0x03 => { + if max_capture_samples_required_for_enroll.is_some() { + return Err(SerdeError::duplicate_field( + "max_capture_samples_required_for_enroll", + )); + } + max_capture_samples_required_for_enroll = Some(map.next_value()?); + } + 0x04 => { + if template_id.is_some() { + return Err(SerdeError::duplicate_field("template_id")); + } + template_id = Some(map.next_value::()?.into_vec()); + } + 0x05 => { + if last_enroll_sample_status.is_some() { + return Err(SerdeError::duplicate_field( + "last_enroll_sample_status", + )); + } + last_enroll_sample_status = Some(map.next_value()?); + } + 0x06 => { + if remaining_samples.is_some() { + return Err(SerdeError::duplicate_field("remaining_samples")); + } + remaining_samples = Some(map.next_value()?); + } + 0x07 => { + if template_infos.is_some() { + return Err(SerdeError::duplicate_field("template_infos")); + } + template_infos = Some(map.next_value()?); + } + 0x08 => { + if max_template_friendly_name.is_some() { + return Err(SerdeError::duplicate_field( + "max_template_friendly_name", + )); + } + max_template_friendly_name = Some(map.next_value()?); + } + k => { + warn!("BioEnrollmentResponse: unexpected key: {:?}", k); + let _ = map.next_value::()?; + continue; + } + } + } + + Ok(BioEnrollmentResponse { + modality, + fingerprint_kind, + max_capture_samples_required_for_enroll, + template_id, + last_enroll_sample_status, + remaining_samples, + template_infos: template_infos.unwrap_or_default(), + max_template_friendly_name, + }) + } + } + deserializer.deserialize_bytes(BioEnrollmentResponseVisitor) + } +} + +#[derive(Debug, Serialize)] +pub struct EnrollmentInfo { + pub template_id: Vec, + pub template_friendly_name: Option, +} + +impl From<&BioTemplateInfo> for EnrollmentInfo { + fn from(value: &BioTemplateInfo) -> Self { + Self { + template_id: value.template_id.to_vec(), + template_friendly_name: value.template_friendly_name.clone(), + } + } +} + +#[derive(Debug, Serialize)] +pub struct FingerprintSensorInfo { + pub fingerprint_kind: FingerprintKind, + pub max_capture_samples_required_for_enroll: u64, + pub max_template_friendly_name: Option, +} + +#[derive(Debug, Serialize)] +pub enum BioEnrollmentResult { + EnrollmentList(Vec), + DeleteSuccess(AuthenticatorInfo), + UpdateSuccess, + AddSuccess(AuthenticatorInfo), + FingerprintSensorInfo(FingerprintSensorInfo), + SampleStatus(LastEnrollmentSampleStatus, u64), +} 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..eb3b713655 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/client_pin.rs @@ -0,0 +1,851 @@ +#![allow(non_upper_case_globals)] +use super::CtapResponse; +// Note: Needed for PinUvAuthTokenPermission +// The current version of `bitflags` doesn't seem to allow +// to set this for an individual bitflag-struct. +use super::{get_info::AuthenticatorInfo, Command, CommandError, RequestCtap2, StatusCode}; +use crate::crypto::{COSEKey, CryptoError, PinUvAuthProtocol, SharedSecret}; +use crate::transport::errors::HIDError; +use crate::transport::{FidoDevice, VirtualFidoDevice}; +use serde::{ + de::{Error as SerdeError, IgnoredAny, MapAccess, Visitor}, + ser::SerializeMap, + Deserialize, Deserializer, Serialize, Serializer, +}; +use serde_bytes::{ByteBuf, Bytes}; +use serde_cbor::de::from_slice; +use serde_cbor::ser::to_vec; +use serde_cbor::Value; +use sha2::{Digest, Sha256}; +use std::convert::TryFrom; +use std::error::Error as StdErrorT; +use std::fmt; + +#[derive(Debug, Copy, Clone)] +#[repr(u8)] +pub enum PINSubcommand { + GetPinRetries = 0x01, + GetKeyAgreement = 0x02, + SetPIN = 0x03, + ChangePIN = 0x04, + GetPINToken = 0x05, // superseded by GetPinUvAuth* + GetPinUvAuthTokenUsingUvWithPermissions = 0x06, + GetUvRetries = 0x07, + GetPinUvAuthTokenUsingPinWithPermissions = 0x09, // Yes, 0x08 is missing +} + +bitflags! { + pub struct PinUvAuthTokenPermission: u8 { + const MakeCredential = 0x01; // rp_id required + const GetAssertion = 0x02; // rp_id required + const CredentialManagement = 0x04; // rp_id optional + const BioEnrollment = 0x08; // rp_id ignored + const LargeBlobWrite = 0x10; // rp_id ignored + const AuthenticatorConfiguration = 0x20; // rp_id ignored + } +} + +impl Default for PinUvAuthTokenPermission { + fn default() -> Self { + // CTAP 2.1 spec: + // If authenticatorClientPIN's getPinToken subcommand is invoked, default permissions + // of `mc` and `ga` (value 0x03) are granted for the returned pinUvAuthToken. + PinUvAuthTokenPermission::MakeCredential | PinUvAuthTokenPermission::GetAssertion + } +} + +#[derive(Debug)] +pub struct ClientPIN { + pub pin_protocol: Option, + pub subcommand: PINSubcommand, + pub key_agreement: Option, + pub pin_auth: Option>, + pub new_pin_enc: Option>, + pub pin_hash_enc: Option>, + pub permissions: Option, + pub rp_id: Option, +} + +impl Default for ClientPIN { + fn default() -> Self { + ClientPIN { + pin_protocol: None, + subcommand: PINSubcommand::GetPinRetries, + 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(&self, serializer: S) -> Result + 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.id())?; + } + 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, Bytes::new(pin_auth))?; + } + if let Some(ref new_pin_enc) = self.new_pin_enc { + map.serialize_entry(&5, Bytes::new(new_pin_enc))?; + } + if let Some(ref pin_hash_enc) = self.pin_hash_enc { + map.serialize_entry(&6, Bytes::new(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; + fn parse_response_payload(&self, input: &[u8]) -> Result; +} + +#[derive(Default, Debug, PartialEq, Eq)] +pub struct ClientPinResponse { + pub key_agreement: Option, + pub pin_token: Option>, + /// Number of PIN attempts remaining before lockout. + pub pin_retries: Option, + pub power_cycle_state: Option, + pub uv_retries: Option, +} + +impl CtapResponse for ClientPinResponse {} + +impl<'de> Deserialize<'de> for ClientPinResponse { + fn deserialize(deserializer: D) -> Result + 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(self, mut map: M) -> Result + 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")); + } + let value: ByteBuf = map.next_value()?; + pin_token = Some(value.into_vec()); + } + 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::()?; + 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: PinUvAuthProtocol, +} + +impl GetKeyAgreement { + pub fn new(pin_protocol: PinUvAuthProtocol) -> Self { + GetKeyAgreement { pin_protocol } + } +} + +impl ClientPINSubCommand for GetKeyAgreement { + type Output = ClientPinResponse; + + fn as_client_pin(&self) -> Result { + Ok(ClientPIN { + pin_protocol: Some(self.pin_protocol.clone()), + subcommand: PINSubcommand::GetKeyAgreement, + ..ClientPIN::default() + }) + } + + fn parse_response_payload(&self, input: &[u8]) -> Result { + let get_pin_response: ClientPinResponse = + from_slice(input).map_err(CommandError::Deserializing)?; + if get_pin_response.key_agreement.is_none() { + return Err(CommandError::MissingRequiredField("key_agreement")); + } + Ok(get_pin_response) + } +} + +#[derive(Debug)] +/// Superseded by GetPinUvAuthTokenUsingUvWithPermissions or +/// GetPinUvAuthTokenUsingPinWithPermissions, thus for backwards compatibility only +pub struct GetPinToken<'sc, 'pin> { + shared_secret: &'sc SharedSecret, + pin: &'pin Pin, +} + +impl<'sc, 'pin> GetPinToken<'sc, 'pin> { + pub fn new(shared_secret: &'sc SharedSecret, pin: &'pin Pin) -> Self { + GetPinToken { shared_secret, pin } + } +} + +impl<'sc, 'pin> ClientPINSubCommand for GetPinToken<'sc, 'pin> { + type Output = ClientPinResponse; + + fn as_client_pin(&self) -> Result { + let input = self.pin.for_pin_token(); + trace!("pin_hash = {:#04X?}", &input); + let pin_hash_enc = self.shared_secret.encrypt(&input)?; + trace!("pin_hash_enc = {:#04X?}", &pin_hash_enc); + + Ok(ClientPIN { + pin_protocol: Some(self.shared_secret.pin_protocol.clone()), + subcommand: PINSubcommand::GetPINToken, + key_agreement: Some(self.shared_secret.client_input().clone()), + pin_hash_enc: Some(pin_hash_enc), + ..ClientPIN::default() + }) + } + + fn parse_response_payload(&self, input: &[u8]) -> Result { + let get_pin_response: ClientPinResponse = + from_slice(input).map_err(CommandError::Deserializing)?; + if get_pin_response.pin_token.is_none() { + return Err(CommandError::MissingRequiredField("pin_token")); + } + Ok(get_pin_response) + } +} + +#[derive(Debug)] +pub struct GetPinUvAuthTokenUsingPinWithPermissions<'sc, 'pin> { + shared_secret: &'sc SharedSecret, + pin: &'pin Pin, + permissions: PinUvAuthTokenPermission, + rp_id: Option, +} + +impl<'sc, 'pin> GetPinUvAuthTokenUsingPinWithPermissions<'sc, 'pin> { + pub fn new( + shared_secret: &'sc SharedSecret, + pin: &'pin Pin, + permissions: PinUvAuthTokenPermission, + rp_id: Option, + ) -> Self { + GetPinUvAuthTokenUsingPinWithPermissions { + shared_secret, + pin, + permissions, + rp_id, + } + } +} + +impl<'sc, 'pin> ClientPINSubCommand for GetPinUvAuthTokenUsingPinWithPermissions<'sc, 'pin> { + type Output = ClientPinResponse; + + fn as_client_pin(&self) -> Result { + let input = self.pin.for_pin_token(); + let pin_hash_enc = self.shared_secret.encrypt(&input)?; + + Ok(ClientPIN { + pin_protocol: Some(self.shared_secret.pin_protocol.clone()), + subcommand: PINSubcommand::GetPinUvAuthTokenUsingPinWithPermissions, + key_agreement: Some(self.shared_secret.client_input().clone()), + pin_hash_enc: Some(pin_hash_enc), + permissions: Some(self.permissions.bits()), + 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 { + let get_pin_response: ClientPinResponse = + from_slice(input).map_err(CommandError::Deserializing)?; + if get_pin_response.pin_token.is_none() { + return Err(CommandError::MissingRequiredField("pin_token")); + } + Ok(get_pin_response) + } +} + +macro_rules! implementRetries { + ($name:ident, $getter:ident) => { + #[derive(Debug, Default)] + pub struct $name {} + + impl $name { + pub fn new() -> Self { + Self {} + } + } + + impl ClientPINSubCommand for $name { + type Output = ClientPinResponse; + + fn as_client_pin(&self) -> Result { + Ok(ClientPIN { + subcommand: PINSubcommand::$name, + ..ClientPIN::default() + }) + } + + fn parse_response_payload(&self, input: &[u8]) -> Result { + let get_pin_response: ClientPinResponse = + from_slice(input).map_err(CommandError::Deserializing)?; + if get_pin_response.$getter.is_none() { + return Err(CommandError::MissingRequiredField(stringify!($getter))); + } + Ok(get_pin_response) + } + } + }; +} + +implementRetries!(GetPinRetries, pin_retries); +implementRetries!(GetUvRetries, uv_retries); + +#[derive(Debug)] +pub struct GetPinUvAuthTokenUsingUvWithPermissions<'sc> { + shared_secret: &'sc SharedSecret, + permissions: PinUvAuthTokenPermission, + rp_id: Option, +} + +impl<'sc> GetPinUvAuthTokenUsingUvWithPermissions<'sc> { + pub fn new( + shared_secret: &'sc SharedSecret, + permissions: PinUvAuthTokenPermission, + rp_id: Option, + ) -> Self { + GetPinUvAuthTokenUsingUvWithPermissions { + shared_secret, + permissions, + rp_id, + } + } +} + +impl<'sc> ClientPINSubCommand for GetPinUvAuthTokenUsingUvWithPermissions<'sc> { + type Output = ClientPinResponse; + + fn as_client_pin(&self) -> Result { + Ok(ClientPIN { + pin_protocol: Some(self.shared_secret.pin_protocol.clone()), + subcommand: PINSubcommand::GetPinUvAuthTokenUsingUvWithPermissions, + key_agreement: Some(self.shared_secret.client_input().clone()), + permissions: Some(self.permissions.bits()), + 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 { + let get_pin_response: ClientPinResponse = + from_slice(input).map_err(CommandError::Deserializing)?; + if get_pin_response.pin_token.is_none() { + return Err(CommandError::MissingRequiredField("pin_token")); + } + Ok(get_pin_response) + } +} + +#[derive(Debug)] +pub struct SetNewPin<'sc, 'pin> { + shared_secret: &'sc SharedSecret, + new_pin: &'pin Pin, +} + +impl<'sc, 'pin> SetNewPin<'sc, 'pin> { + pub fn new(shared_secret: &'sc SharedSecret, new_pin: &'pin Pin) -> Self { + SetNewPin { + shared_secret, + new_pin, + } + } +} + +impl<'sc, 'pin> ClientPINSubCommand for SetNewPin<'sc, 'pin> { + type Output = ClientPinResponse; + + fn as_client_pin(&self) -> Result { + if self.new_pin.as_bytes().len() > 63 { + return Err(CommandError::StatusCode( + StatusCode::PinPolicyViolation, + None, + )); + } + + // newPinEnc: the result of calling encrypt(shared secret, paddedPin) where paddedPin is + // newPin padded on the right with 0x00 bytes to make it 64 bytes long. (Since the maximum + // length of newPin is 63 bytes, there is always at least one byte of padding.) + let new_pin_padded = self.new_pin.padded(); + let new_pin_enc = self.shared_secret.encrypt(&new_pin_padded)?; + + // pinUvAuthParam: the result of calling authenticate(shared secret, newPinEnc). + let pin_auth = self.shared_secret.authenticate(&new_pin_enc)?; + + Ok(ClientPIN { + pin_protocol: Some(self.shared_secret.pin_protocol.clone()), + subcommand: PINSubcommand::SetPIN, + key_agreement: Some(self.shared_secret.client_input().clone()), + new_pin_enc: Some(new_pin_enc), + pin_auth: Some(pin_auth), + ..ClientPIN::default() + }) + } + + fn parse_response_payload(&self, input: &[u8]) -> Result { + // Should be an empty response or a valid cbor-value (which we ignore) + if !input.is_empty() { + let _: Value = from_slice(input).map_err(CommandError::Deserializing)?; + } + Ok(ClientPinResponse::default()) + } +} + +#[derive(Debug)] +pub struct ChangeExistingPin<'sc, 'pin> { + pin_protocol: PinUvAuthProtocol, + shared_secret: &'sc SharedSecret, + current_pin: &'pin Pin, + new_pin: &'pin Pin, +} + +impl<'sc, 'pin> ChangeExistingPin<'sc, 'pin> { + pub fn new( + info: &AuthenticatorInfo, + shared_secret: &'sc SharedSecret, + current_pin: &'pin Pin, + new_pin: &'pin Pin, + ) -> Result { + Ok(ChangeExistingPin { + pin_protocol: PinUvAuthProtocol::try_from(info)?, + shared_secret, + current_pin, + new_pin, + }) + } +} + +impl<'sc, 'pin> ClientPINSubCommand for ChangeExistingPin<'sc, 'pin> { + type Output = ClientPinResponse; + + fn as_client_pin(&self) -> Result { + if self.new_pin.as_bytes().len() > 63 { + return Err(CommandError::StatusCode( + StatusCode::PinPolicyViolation, + None, + )); + } + + // newPinEnc: the result of calling encrypt(shared secret, paddedPin) where paddedPin is + // newPin padded on the right with 0x00 bytes to make it 64 bytes long. (Since the maximum + // length of newPin is 63 bytes, there is always at least one byte of padding.) + let new_pin_padded = self.new_pin.padded(); + let new_pin_enc = self.shared_secret.encrypt(&new_pin_padded)?; + + let current_pin_hash = self.current_pin.for_pin_token(); + let pin_hash_enc = self.shared_secret.encrypt(current_pin_hash.as_ref())?; + + let pin_auth = self + .shared_secret + .authenticate(&[new_pin_enc.as_slice(), pin_hash_enc.as_slice()].concat())?; + + Ok(ClientPIN { + pin_protocol: Some(self.shared_secret.pin_protocol.clone()), + subcommand: PINSubcommand::ChangePIN, + key_agreement: Some(self.shared_secret.client_input().clone()), + new_pin_enc: Some(new_pin_enc), + pin_hash_enc: Some(pin_hash_enc), + pin_auth: Some(pin_auth), + permissions: None, + rp_id: None, + }) + } + + fn parse_response_payload(&self, input: &[u8]) -> Result { + // Should be an empty response or a valid cbor-value (which we ignore) + if !input.is_empty() { + let _: Value = from_slice(input).map_err(CommandError::Deserializing)?; + } + Ok(ClientPinResponse::default()) + } +} + +impl RequestCtap2 for T +where + T: ClientPINSubCommand, + T: fmt::Debug, +{ + type Output = ::Output; + + fn command(&self) -> Command { + Command::ClientPin + } + + fn wire_format(&self) -> Result, HIDError> { + 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( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result { + 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() { + ::parse_response_payload(self, &input[1..]) + .map_err(HIDError::Command) + } 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()) + } + } + + fn send_to_virtual_device( + &self, + dev: &mut Dev, + ) -> Result { + dev.client_pin(&self.as_client_pin()?) + } +} + +#[derive(Clone, Serialize, Deserialize)] +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) -> Vec { + 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]); + + output.to_vec() + } + + pub fn padded(&self) -> Vec { + let mut out = self.0.as_bytes().to_vec(); + out.resize(64, 0x00); + out + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[derive(Clone, Debug, Serialize)] +pub enum PinError { + PinRequired, + PinIsTooShort, + PinIsTooLong(usize), + InvalidPin(Option), + InvalidUv(Option), + PinAuthBlocked, + PinBlocked, + PinNotSet, + UvBlocked, + /// Used for CTAP2.0 UV (fingerprints) + PinAuthInvalid, + Crypto(CryptoError), +} + +impl fmt::Display for PinError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + PinError::PinRequired => write!(f, "Pin required."), + PinError::PinIsTooShort => write!(f, "pin is too short"), + PinError::PinIsTooLong(len) => write!(f, "pin is too long ({len})"), + PinError::InvalidPin(ref e) => { + let mut res = write!(f, "Invalid Pin."); + if let Some(pin_retries) = e { + res = write!(f, " Retries left: {pin_retries:?}") + } + res + } + PinError::InvalidUv(ref e) => { + let mut res = write!(f, "Invalid Uv."); + if let Some(uv_retries) = e { + res = write!(f, " Retries left: {uv_retries:?}") + } + res + } + PinError::PinAuthBlocked => { + write!(f, "Pin authentication blocked. Device needs power cycle.") + } + PinError::PinBlocked => write!(f, "No retries left. Pin blocked. Device needs reset."), + PinError::PinNotSet => write!(f, "Pin needed but not set on device."), + PinError::UvBlocked => write!(f, "No retries left. Uv blocked. Device needs reset."), + PinError::PinAuthInvalid => write!(f, "PinAuth invalid."), + PinError::Crypto(ref e) => write!(f, "Crypto backend error: {e:?}"), + } + } +} + +impl StdErrorT for PinError {} + +impl From for PinError { + fn from(e: CryptoError) -> Self { + PinError::Crypto(e) + } +} + +#[cfg(test)] +mod test { + use super::ClientPinResponse; + use crate::crypto::{COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve}; + use serde_cbor::de::from_slice; + + #[test] + fn test_get_key_agreement() { + let reference = [ + 161, 1, 165, 1, 2, 3, 56, 24, 32, 1, 33, 88, 32, 115, 222, 167, 5, 88, 238, 119, 202, + 121, 23, 241, 150, 9, 48, 197, 136, 174, 0, 17, 90, 190, 83, 65, 103, 237, 97, 41, 213, + 128, 111, 7, 106, 34, 88, 32, 248, 204, 9, 26, 82, 96, 25, 72, 5, 82, 251, 185, 22, 39, + 246, 149, 54, 246, 255, 225, 52, 102, 67, 221, 113, 194, 236, 213, 199, 147, 180, 81, + ]; + let expected = ClientPinResponse { + key_agreement: Some(COSEKey { + alg: COSEAlgorithm::ECDH_ES_HKDF256, + key: COSEKeyType::EC2(COSEEC2Key { + curve: Curve::SECP256R1, + x: vec![ + 115, 222, 167, 5, 88, 238, 119, 202, 121, 23, 241, 150, 9, 48, 197, 136, + 174, 0, 17, 90, 190, 83, 65, 103, 237, 97, 41, 213, 128, 111, 7, 106, + ], + y: vec![ + 248, 204, 9, 26, 82, 96, 25, 72, 5, 82, 251, 185, 22, 39, 246, 149, 54, + 246, 255, 225, 52, 102, 67, 221, 113, 194, 236, 213, 199, 147, 180, 81, + ], + }), + }), + pin_token: None, + pin_retries: None, + power_cycle_state: None, + uv_retries: None, + }; + let result: ClientPinResponse = + from_slice(&reference).expect("could not deserialize reference"); + assert_eq!(expected, result); + } + + #[test] + fn test_get_pin_retries() { + let reference = [161, 3, 7]; + let expected = ClientPinResponse { + key_agreement: None, + pin_token: None, + pin_retries: Some(7), + power_cycle_state: None, + uv_retries: None, + }; + let result: ClientPinResponse = + from_slice(&reference).expect("could not deserialize reference"); + assert_eq!(expected, result); + } + + #[test] + fn test_get_uv_retries() { + let reference = [161, 5, 2]; + let expected = ClientPinResponse { + key_agreement: None, + pin_token: None, + pin_retries: None, + power_cycle_state: None, + uv_retries: Some(2), + }; + let result: ClientPinResponse = + from_slice(&reference).expect("could not deserialize reference"); + assert_eq!(expected, result); + } + + #[test] + fn test_get_pin_token() { + let reference = [ + 161, 2, 88, 48, 173, 244, 214, 87, 128, 57, 25, 99, 142, 140, 41, 25, 94, 60, 75, 163, + 240, 187, 211, 138, 11, 208, 74, 117, 180, 181, 97, 31, 79, 252, 191, 244, 49, 13, 201, + 217, 204, 219, 122, 3, 101, 4, 70, 26, 14, 41, 150, 148, + ]; + let expected = ClientPinResponse { + key_agreement: None, + pin_token: Some(vec![ + 173, 244, 214, 87, 128, 57, 25, 99, 142, 140, 41, 25, 94, 60, 75, 163, 240, 187, + 211, 138, 11, 208, 74, 117, 180, 181, 97, 31, 79, 252, 191, 244, 49, 13, 201, 217, + 204, 219, 122, 3, 101, 4, 70, 26, 14, 41, 150, 148, + ]), + pin_retries: None, + power_cycle_state: None, + uv_retries: None, + }; + let result: ClientPinResponse = + from_slice(&reference).expect("could not deserialize reference"); + assert_eq!(expected, result); + } + + #[test] + fn test_get_puat_using_uv() { + let reference = [ + 161, 2, 88, 48, 94, 109, 192, 236, 90, 161, 77, 153, 23, 146, 179, 189, 133, 106, 76, + 150, 17, 238, 155, 102, 107, 201, 98, 232, 184, 33, 153, 224, 203, 87, 147, 10, 21, 20, + 85, 184, 109, 61, 240, 58, 236, 198, 171, 48, 242, 165, 221, 214, + ]; + let expected = ClientPinResponse { + key_agreement: None, + pin_token: Some(vec![ + 94, 109, 192, 236, 90, 161, 77, 153, 23, 146, 179, 189, 133, 106, 76, 150, 17, 238, + 155, 102, 107, 201, 98, 232, 184, 33, 153, 224, 203, 87, 147, 10, 21, 20, 85, 184, + 109, 61, 240, 58, 236, 198, 171, 48, 242, 165, 221, 214, + ]), + pin_retries: None, + power_cycle_state: None, + uv_retries: None, + }; + let result: ClientPinResponse = + from_slice(&reference).expect("could not deserialize reference"); + assert_eq!(expected, result); + } + + #[test] + fn test_get_puat_using_pin() { + let reference = [ + 161, 2, 88, 48, 143, 174, 68, 241, 186, 39, 106, 238, 129, 15, 181, 102, 112, 130, 239, + 96, 106, 235, 3, 10, 61, 173, 106, 252, 38, 236, 44, 112, 91, 34, 218, 136, 139, 118, + 162, 178, 172, 227, 82, 103, 136, 91, 136, 178, 170, 233, 156, 62, + ]; + let expected = ClientPinResponse { + key_agreement: None, + pin_token: Some(vec![ + 143, 174, 68, 241, 186, 39, 106, 238, 129, 15, 181, 102, 112, 130, 239, 96, 106, + 235, 3, 10, 61, 173, 106, 252, 38, 236, 44, 112, 91, 34, 218, 136, 139, 118, 162, + 178, 172, 227, 82, 103, 136, 91, 136, 178, 170, 233, 156, 62, + ]), + pin_retries: None, + power_cycle_state: None, + uv_retries: None, + }; + let result: ClientPinResponse = + from_slice(&reference).expect("could not deserialize reference"); + assert_eq!(expected, result); + } +} diff --git a/third_party/rust/authenticator/src/ctap2/commands/credential_management.rs b/third_party/rust/authenticator/src/ctap2/commands/credential_management.rs new file mode 100644 index 0000000000..2a58bb1791 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/credential_management.rs @@ -0,0 +1,457 @@ +use super::{Command, CommandError, CtapResponse, PinUvAuthCommand, RequestCtap2, StatusCode}; +use crate::{ + crypto::{COSEKey, PinUvAuthParam, PinUvAuthToken}, + ctap2::server::{ + PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity, RelyingParty, RpIdHash, + UserVerificationRequirement, + }, + errors::AuthenticatorError, + transport::errors::HIDError, + FidoDevice, +}; +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, to_vec, Value}; +use std::fmt; + +#[derive(Debug, Clone, Deserialize, Default)] +struct CredManagementParams { + rp_id_hash: Option, // RP ID SHA-256 hash + credential_id: Option, // Credential Identifier + user: Option, // User Entity +} + +impl CredManagementParams { + fn has_some(&self) -> bool { + self.rp_id_hash.is_some() || self.credential_id.is_some() || self.user.is_some() + } +} + +impl Serialize for CredManagementParams { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map_len = 0; + if self.rp_id_hash.is_some() { + map_len += 1; + } + if self.credential_id.is_some() { + map_len += 1; + } + if self.user.is_some() { + map_len += 1; + } + + let mut map = serializer.serialize_map(Some(map_len))?; + if let Some(rp_id_hash) = &self.rp_id_hash { + map.serialize_entry(&0x01, &ByteBuf::from(rp_id_hash.as_ref()))?; + } + if let Some(credential_id) = &self.credential_id { + map.serialize_entry(&0x02, credential_id)?; + } + if let Some(user) = &self.user { + map.serialize_entry(&0x03, user)?; + } + map.end() + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub(crate) enum CredManagementCommand { + GetCredsMetadata, + EnumerateRPsBegin, + EnumerateRPsGetNextRP, + EnumerateCredentialsBegin(RpIdHash), + EnumerateCredentialsGetNextCredential, + DeleteCredential(PublicKeyCredentialDescriptor), + UpdateUserInformation((PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity)), +} + +impl CredManagementCommand { + fn to_id_and_param(&self) -> (u8, CredManagementParams) { + let mut params = CredManagementParams::default(); + match &self { + CredManagementCommand::GetCredsMetadata => (0x01, params), + CredManagementCommand::EnumerateRPsBegin => (0x02, params), + CredManagementCommand::EnumerateRPsGetNextRP => (0x03, params), + CredManagementCommand::EnumerateCredentialsBegin(rp_id_hash) => { + params.rp_id_hash = Some(rp_id_hash.clone()); + (0x04, params) + } + CredManagementCommand::EnumerateCredentialsGetNextCredential => (0x05, params), + CredManagementCommand::DeleteCredential(cred_id) => { + params.credential_id = Some(cred_id.clone()); + (0x06, params) + } + CredManagementCommand::UpdateUserInformation((cred_id, user)) => { + params.credential_id = Some(cred_id.clone()); + params.user = Some(user.clone()); + (0x07, params) + } + } + } +} +#[derive(Debug)] +pub struct CredentialManagement { + pub(crate) subcommand: CredManagementCommand, // subCommand currently being requested + pin_uv_auth_param: Option, // First 16 bytes of HMAC-SHA-256 of contents using pinUvAuthToken. + use_legacy_preview: bool, +} + +impl CredentialManagement { + pub(crate) fn new(subcommand: CredManagementCommand, use_legacy_preview: bool) -> Self { + Self { + subcommand, + pin_uv_auth_param: None, + use_legacy_preview, + } + } +} + +impl Serialize for CredentialManagement { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Need to define how many elements are going to be in the map + // beforehand + let mut map_len = 1; + let (id, params) = self.subcommand.to_id_and_param(); + if params.has_some() { + map_len += 1; + } + if self.pin_uv_auth_param.is_some() { + map_len += 2; + } + + let mut map = serializer.serialize_map(Some(map_len))?; + + map.serialize_entry(&0x01, &id)?; + if params.has_some() { + map.serialize_entry(&0x02, ¶ms)?; + } + + if let Some(ref pin_uv_auth_param) = self.pin_uv_auth_param { + map.serialize_entry(&0x03, &pin_uv_auth_param.pin_protocol.id())?; + map.serialize_entry(&0x04, pin_uv_auth_param)?; + } + + map.end() + } +} + +#[derive(Debug, Default)] +pub struct CredentialManagementResponse { + /// Number of existing discoverable credentials present on the authenticator. + pub existing_resident_credentials_count: Option, + /// Number of maximum possible remaining discoverable credentials which can be created on the authenticator. + pub max_possible_remaining_resident_credentials_count: Option, + /// RP Information + pub rp: Option, + /// RP ID SHA-256 hash + pub rp_id_hash: Option, + /// Total number of RPs present on the authenticator + pub total_rps: Option, + /// User Information + pub user: Option, + /// Credential ID + pub credential_id: Option, + /// Public key of the credential. + pub public_key: Option, + /// Total number of credentials present on the authenticator for the RP in question + pub total_credentials: Option, + /// Credential protection policy. + pub cred_protect: Option, + /// Large blob encryption key. + pub large_blob_key: Option>, +} + +impl CtapResponse for CredentialManagementResponse {} + +#[derive(Debug, PartialEq, Eq, Serialize)] +pub struct CredentialRpListEntry { + /// RP Information + pub rp: RelyingParty, + /// RP ID SHA-256 hash + pub rp_id_hash: RpIdHash, + pub credentials: Vec, +} + +#[derive(Debug, PartialEq, Eq, Serialize)] +pub struct CredentialListEntry { + /// User Information + pub user: PublicKeyCredentialUserEntity, + /// Credential ID + pub credential_id: PublicKeyCredentialDescriptor, + /// Public key of the credential. + pub public_key: COSEKey, + /// Credential protection policy. + pub cred_protect: u64, + /// Large blob encryption key. + pub large_blob_key: Option>, +} + +#[derive(Debug, Serialize)] +pub enum CredentialManagementResult { + CredentialList(CredentialList), + DeleteSucess, + UpdateSuccess, +} + +#[derive(Debug, Default, Serialize)] +pub struct CredentialList { + /// Number of existing discoverable credentials present on the authenticator. + pub existing_resident_credentials_count: u64, + /// Number of maximum possible remaining discoverable credentials which can be created on the authenticator. + pub max_possible_remaining_resident_credentials_count: u64, + /// The found credentials + pub credential_list: Vec, +} + +impl CredentialList { + pub fn new() -> Self { + Default::default() + } +} + +impl<'de> Deserialize<'de> for CredentialManagementResponse { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct CredentialManagementResponseVisitor; + + impl<'de> Visitor<'de> for CredentialManagementResponseVisitor { + type Value = CredentialManagementResponse; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'de>, + { + let mut existing_resident_credentials_count = None; // (0x01) Unsigned Integer Number of existing discoverable credentials present on the authenticator. + let mut max_possible_remaining_resident_credentials_count = None; // (0x02) Unsigned Integer Number of maximum possible remaining discoverable credentials which can be created on the authenticator. + let mut rp = None; // (0x03) PublicKeyCredentialRpEntity RP Information + let mut rp_id_hash = None; // (0x04) Byte String RP ID SHA-256 hash + let mut total_rps = None; // (0x05) Unsigned Integer total number of RPs present on the authenticator + let mut user = None; // (0x06) PublicKeyCredentialUserEntity User Information + let mut credential_id = None; // (0x07) PublicKeyCredentialDescriptor PublicKeyCredentialDescriptor + let mut public_key = None; // (0x08) COSE_Key Public key of the credential. + let mut total_credentials = None; // (0x09) Unsigned Integer Total number of credentials present on the authenticator for the RP in question + let mut cred_protect = None; // (0x0A) Unsigned Integer Credential protection policy. + let mut large_blob_key = None; // (0x0B) Byte string Large blob encryption key. + + while let Some(key) = map.next_key()? { + match key { + 0x01 => { + if existing_resident_credentials_count.is_some() { + return Err(SerdeError::duplicate_field( + "existing_resident_credentials_count", + )); + } + existing_resident_credentials_count = Some(map.next_value()?); + } + 0x02 => { + if max_possible_remaining_resident_credentials_count.is_some() { + return Err(SerdeError::duplicate_field( + "max_possible_remaining_resident_credentials_count", + )); + } + max_possible_remaining_resident_credentials_count = + Some(map.next_value()?); + } + 0x03 => { + if rp.is_some() { + return Err(SerdeError::duplicate_field("rp")); + } + rp = Some(map.next_value()?); + } + 0x04 => { + if rp_id_hash.is_some() { + return Err(SerdeError::duplicate_field("rp_id_hash")); + } + let rp_raw = map.next_value::()?; + rp_id_hash = + Some(RpIdHash::from(rp_raw.as_slice()).map_err(|_| { + SerdeError::invalid_length(rp_raw.len(), &"32") + })?); + } + 0x05 => { + if total_rps.is_some() { + return Err(SerdeError::duplicate_field("total_rps")); + } + total_rps = Some(map.next_value()?); + } + 0x06 => { + if user.is_some() { + return Err(SerdeError::duplicate_field("user")); + } + user = Some(map.next_value()?); + } + 0x07 => { + if credential_id.is_some() { + return Err(SerdeError::duplicate_field("credential_id")); + } + credential_id = Some(map.next_value()?); + } + 0x08 => { + if public_key.is_some() { + return Err(SerdeError::duplicate_field("public_key")); + } + public_key = Some(map.next_value()?); + } + 0x09 => { + if total_credentials.is_some() { + return Err(SerdeError::duplicate_field("total_credentials")); + } + total_credentials = Some(map.next_value()?); + } + 0x0A => { + if cred_protect.is_some() { + return Err(SerdeError::duplicate_field("cred_protect")); + } + cred_protect = Some(map.next_value()?); + } + 0x0B => { + if large_blob_key.is_some() { + return Err(SerdeError::duplicate_field("large_blob_key")); + } + // Using into_vec, to avoid any copy of large_blob_key + large_blob_key = Some(map.next_value::()?.into_vec()); + } + + k => { + warn!("ClientPinResponse: unexpected key: {:?}", k); + let _ = map.next_value::()?; + continue; + } + } + } + + Ok(CredentialManagementResponse { + existing_resident_credentials_count, + max_possible_remaining_resident_credentials_count, + rp, + rp_id_hash, + total_rps, + user, + credential_id, + public_key, + total_credentials, + cred_protect, + large_blob_key, + }) + } + } + deserializer.deserialize_bytes(CredentialManagementResponseVisitor) + } +} + +impl RequestCtap2 for CredentialManagement { + type Output = CredentialManagementResponse; + + fn command(&self) -> Command { + if self.use_legacy_preview { + Command::CredentialManagementPreview + } else { + Command::CredentialManagement + } + } + + fn wire_format(&self) -> Result, HIDError> { + let output = to_vec(&self).map_err(CommandError::Serializing)?; + trace!("client subcommmand: {:04X?}", &output); + Ok(output) + } + + fn handle_response_ctap2( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result + where + Dev: FidoDevice, + { + if input.is_empty() { + return Err(CommandError::InputTooSmall.into()); + } + + let status: StatusCode = input[0].into(); + + if status.is_ok() { + if input.len() > 1 { + trace!("parsing credential management data: {:#04X?}", &input); + let credential_management = + from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + Ok(credential_management) + } else { + // Some subcommands return only an OK-status without any data + Ok(CredentialManagementResponse::default()) + } + } else { + let data: Option = if input.len() > 1 { + Some(from_slice(&input[1..]).map_err(CommandError::Deserializing)?) + } else { + None + }; + Err(CommandError::StatusCode(status, data).into()) + } + } + + fn send_to_virtual_device( + &self, + _dev: &mut Dev, + ) -> Result { + unimplemented!() + } +} + +impl PinUvAuthCommand for CredentialManagement { + fn get_rp_id(&self) -> Option<&String> { + None + } + + fn set_pin_uv_auth_param( + &mut self, + pin_uv_auth_token: Option, + ) -> Result<(), AuthenticatorError> { + let mut param = None; + if let Some(token) = pin_uv_auth_token { + // pinUvAuthParam (0x04): the result of calling + // authenticate(pinUvAuthToken, uint8(subCommand) || subCommandParams). + let (id, params) = self.subcommand.to_id_and_param(); + let mut data = vec![id]; + if params.has_some() { + data.extend(to_vec(¶ms).map_err(CommandError::Serializing)?); + } + param = Some(token.derive(&data).map_err(CommandError::Crypto)?); + } + self.pin_uv_auth_param = param; + Ok(()) + } + + fn can_skip_user_verification( + &mut self, + _info: &crate::AuthenticatorInfo, + _uv: UserVerificationRequirement, + ) -> bool { + // "discouraged" does not exist for AuthenticatorConfig + false + } + + fn set_uv_option(&mut self, _uv: Option) { + /* No-op */ + } + + fn get_pin_uv_auth_param(&self) -> Option<&PinUvAuthParam> { + self.pin_uv_auth_param.as_ref() + } +} 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..54d7d4919f --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/get_assertion.rs @@ -0,0 +1,1494 @@ +use super::get_info::AuthenticatorInfo; +use super::{ + Command, CommandError, CtapResponse, PinUvAuthCommand, RequestCtap1, RequestCtap2, Retryable, + StatusCode, +}; +use crate::consts::{ + PARAMETER_SIZE, U2F_AUTHENTICATE, U2F_DONT_ENFORCE_USER_PRESENCE_AND_SIGN, + U2F_REQUEST_USER_PRESENCE, +}; +use crate::crypto::{COSEKey, CryptoError, PinUvAuthParam, PinUvAuthToken, SharedSecret}; +use crate::ctap2::attestation::{AuthenticatorData, AuthenticatorDataFlags}; +use crate::ctap2::client_data::ClientDataHash; +use crate::ctap2::commands::get_next_assertion::GetNextAssertion; +use crate::ctap2::commands::make_credentials::UserVerification; +use crate::ctap2::server::{ + AuthenticationExtensionsClientInputs, AuthenticationExtensionsClientOutputs, + AuthenticatorAttachment, PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity, + RelyingParty, RpIdHash, UserVerificationRequirement, +}; +use crate::ctap2::utils::{read_be_u32, read_byte}; +use crate::errors::AuthenticatorError; +use crate::transport::errors::{ApduErrorStatus, HIDError}; +use crate::transport::{FidoDevice, VirtualFidoDevice}; +use crate::u2ftypes::CTAP1RequestAPDU; +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::fmt; +use std::io::Cursor; + +#[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, + #[serde(rename = "up", skip_serializing_if = "Option::is_none")] + pub user_presence: Option, +} + +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, + pub salt_auth: Vec, +} + +#[derive(Debug, Clone, Default)] +pub struct HmacSecretExtension { + pub salt1: Vec, + pub salt2: Option>, + calculated_hmac: Option, +} + +impl HmacSecretExtension { + pub fn new(salt1: Vec, salt2: Option>) -> Self { + HmacSecretExtension { + salt1, + salt2, + calculated_hmac: None, + } + } + + pub fn calculate(&mut self, secret: &SharedSecret) -> 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 + secret.encrypt(&salts) + } + None => secret.encrypt(&self.salt1[..32]), + }?; + let salt_auth = secret.authenticate(&salt_enc)?; + let public_key = secret.client_input().clone(); + self.calculated_hmac = Some(CalculatedHmacSecretExtension { + public_key, + salt_enc, + salt_auth, + }); + + Ok(()) + } +} + +impl Serialize for HmacSecretExtension { + fn serialize(&self, serializer: S) -> Result + 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, Default, Clone, Serialize)] +pub struct GetAssertionExtensions { + #[serde(skip_serializing)] + pub app_id: Option, + #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")] + pub hmac_secret: Option, +} + +impl From for GetAssertionExtensions { + fn from(input: AuthenticationExtensionsClientInputs) -> Self { + Self { + app_id: input.app_id, + ..Default::default() + } + } +} + +impl GetAssertionExtensions { + fn has_content(&self) -> bool { + self.hmac_secret.is_some() + } +} + +#[derive(Debug, Clone)] +pub struct GetAssertion { + pub client_data_hash: ClientDataHash, + pub rp: RelyingParty, + pub allow_list: Vec, + + // 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 extensions: GetAssertionExtensions, + pub options: GetAssertionOptions, + pub pin_uv_auth_param: Option, +} + +impl GetAssertion { + pub fn new( + client_data_hash: ClientDataHash, + rp: RelyingParty, + allow_list: Vec, + options: GetAssertionOptions, + extensions: GetAssertionExtensions, + ) -> Self { + Self { + client_data_hash, + rp, + allow_list, + extensions, + options, + pin_uv_auth_param: None, + } + } + + pub fn finalize_result(&self, dev: &Dev, result: &mut GetAssertionResult) { + result.attachment = match dev.get_authenticator_info() { + Some(info) if info.options.platform_device => AuthenticatorAttachment::Platform, + Some(_) => AuthenticatorAttachment::CrossPlatform, + None => AuthenticatorAttachment::Unknown, + }; + + // Handle extensions whose outputs are not encoded in the authenticator data. + // 1. appId + if let Some(app_id) = &self.extensions.app_id { + result.extensions.app_id = + Some(result.assertion.auth_data.rp_id_hash == RelyingParty::from(app_id).hash()); + } + } +} + +impl PinUvAuthCommand for GetAssertion { + fn set_pin_uv_auth_param( + &mut self, + pin_uv_auth_token: Option, + ) -> Result<(), AuthenticatorError> { + let mut param = None; + if let Some(token) = pin_uv_auth_token { + param = Some( + token + .derive(self.client_data_hash.as_ref()) + .map_err(CommandError::Crypto)?, + ); + } + self.pin_uv_auth_param = param; + Ok(()) + } + + fn set_uv_option(&mut self, uv: Option) { + self.options.user_verification = uv; + } + + fn get_rp_id(&self) -> Option<&String> { + Some(&self.rp.id) + } + + fn can_skip_user_verification( + &mut self, + info: &AuthenticatorInfo, + uv_req: UserVerificationRequirement, + ) -> bool { + let supports_uv = info.options.user_verification == Some(true); + let pin_configured = info.options.client_pin == Some(true); + let device_protected = supports_uv || pin_configured; + let uv_discouraged = uv_req == UserVerificationRequirement::Discouraged; + let always_uv = info.options.always_uv == Some(true); + + !always_uv && (!device_protected || uv_discouraged) + } + + fn get_pin_uv_auth_param(&self) -> Option<&PinUvAuthParam> { + self.pin_uv_auth_param.as_ref() + } +} + +impl Serialize for GetAssertion { + fn serialize(&self, serializer: S) -> Result + 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_content() { + map_len += 1; + } + if self.options.has_some() { + map_len += 1; + } + if self.pin_uv_auth_param.is_some() { + map_len += 2; + } + + let mut map = serializer.serialize_map(Some(map_len))?; + map.serialize_entry(&1, &self.rp.id)?; + map.serialize_entry(&2, &self.client_data_hash)?; + if !self.allow_list.is_empty() { + map.serialize_entry(&3, &self.allow_list)?; + } + if self.extensions.has_content() { + map.serialize_entry(&4, &self.extensions)?; + } + if self.options.has_some() { + map.serialize_entry(&5, &self.options)?; + } + if let Some(pin_uv_auth_param) = &self.pin_uv_auth_param { + map.serialize_entry(&6, &pin_uv_auth_param)?; + map.serialize_entry(&7, &pin_uv_auth_param.pin_protocol.id())?; + } + map.end() + } +} + +type GetAssertionOutput = Vec; +impl CtapResponse for GetAssertionOutput {} + +impl RequestCtap1 for GetAssertion { + type Output = Vec; + type AdditionalInfo = PublicKeyCredentialDescriptor; + + fn ctap1_format(&self) -> Result<(Vec, Self::AdditionalInfo), HIDError> { + // Pre-flighting should reduce the list to exactly one entry + let key_handle = match &self.allow_list[..] { + [key_handle] => key_handle, + [] => { + return Err(HIDError::Command(CommandError::StatusCode( + StatusCode::NoCredentials, + None, + ))); + } + _ => { + return Err(HIDError::UnsupportedCommand); + } + }; + + debug!("sending key_handle = {:?}", key_handle); + + let flags = if self.options.user_presence.unwrap_or(true) { + U2F_REQUEST_USER_PRESENCE + } else { + U2F_DONT_ENFORCE_USER_PRESENCE_AND_SIGN + }; + let mut auth_data = + Vec::with_capacity(2 * PARAMETER_SIZE + 1 /* key_handle_len */ + key_handle.id.len()); + + auth_data.extend_from_slice(self.client_data_hash.as_ref()); + auth_data.extend_from_slice(self.rp.hash().as_ref()); + auth_data.extend_from_slice(&[key_handle.id.len() as u8]); + auth_data.extend_from_slice(key_handle.id.as_ref()); + + let cmd = U2F_AUTHENTICATE; + let apdu = CTAP1RequestAPDU::serialize(cmd, flags, &auth_data)?; + Ok((apdu, key_handle.clone())) + } + + fn handle_response_ctap1( + &self, + dev: &mut Dev, + status: Result<(), ApduErrorStatus>, + input: &[u8], + add_info: &PublicKeyCredentialDescriptor, + ) -> Result> { + if Err(ApduErrorStatus::ConditionsNotSatisfied) == status { + return Err(Retryable::Retry); + } + if let Err(err) = status { + return Err(Retryable::Error(HIDError::ApduStatus(err))); + } + + let mut result = GetAssertionResult::from_ctap1(input, &self.rp.hash(), add_info) + .map_err(|e| Retryable::Error(HIDError::Command(e)))?; + self.finalize_result(dev, &mut result); + // Although there's only one result, we return a vector for consistency with CTAP2. + Ok(vec![result]) + } + + fn send_to_virtual_device( + &self, + dev: &mut Dev, + ) -> Result { + let mut results = dev.get_assertion(self)?; + for result in results.iter_mut() { + self.finalize_result(dev, result); + } + Ok(results) + } +} + +impl RequestCtap2 for GetAssertion { + type Output = Vec; + + fn command(&self) -> Command { + Command::GetAssertion + } + + fn wire_format(&self) -> Result, HIDError> { + Ok(ser::to_vec(&self).map_err(CommandError::Serializing)?) + } + + fn handle_response_ctap2( + &self, + dev: &mut Dev, + input: &[u8], + ) -> Result { + 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() { + return Err(CommandError::InputTooSmall.into()); + } + return Err(CommandError::StatusCode(status, None).into()); + } + + 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 results = Vec::with_capacity(number_of_credentials); + results.push(GetAssertionResult { + assertion: assertion.into(), + attachment: AuthenticatorAttachment::Unknown, + extensions: Default::default(), + }); + + let msg = GetNextAssertion; + // We already have one, so skipping 0 + for _ in 1..number_of_credentials { + let assertion = dev.send_cbor(&msg)?; + results.push(GetAssertionResult { + assertion: assertion.into(), + attachment: AuthenticatorAttachment::Unknown, + extensions: Default::default(), + }); + } + + for result in results.iter_mut() { + self.finalize_result(dev, result); + } + Ok(results) + } else { + let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + Err(CommandError::StatusCode(status, Some(data)).into()) + } + } + + fn send_to_virtual_device( + &self, + dev: &mut Dev, + ) -> Result { + let mut results = dev.get_assertion(self)?; + for result in results.iter_mut() { + self.finalize_result(dev, result); + } + Ok(results) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Assertion { + pub credentials: Option, /* Was optional in CTAP2.0, is + * mandatory in CTAP2.1 */ + pub auth_data: AuthenticatorData, + pub signature: Vec, + pub user: Option, +} + +impl From for Assertion { + fn from(r: GetAssertionResponse) -> Self { + Assertion { + credentials: r.credentials, + auth_data: r.auth_data, + signature: r.signature, + user: r.user, + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct GetAssertionResult { + pub assertion: Assertion, + pub attachment: AuthenticatorAttachment, + pub extensions: AuthenticationExtensionsClientOutputs, +} + +impl GetAssertionResult { + pub fn from_ctap1( + input: &[u8], + rp_id_hash: &RpIdHash, + key_handle: &PublicKeyCredentialDescriptor, + ) -> Result { + let mut data = Cursor::new(input); + let user_presence = read_byte(&mut data).map_err(CommandError::Deserializing)?; + let counter = read_be_u32(&mut data).map_err(CommandError::Deserializing)?; + // Remaining data is signature (Note: `data.remaining_slice()` is not yet stabilized) + let signature = Vec::from(&data.get_ref()[data.position() as usize..]); + + // Step 5 of Section 10.3 of CTAP2.1: "Copy bits 0 (the UP bit) and bit 1 from the + // CTAP2/U2F response user presence byte to bits 0 and 1 of the CTAP2 flags, respectively. + // Set all other bits of flags to zero." + let flag_mask = AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::RESERVED_1; + let flags = flag_mask & AuthenticatorDataFlags::from_bits_truncate(user_presence); + let auth_data = AuthenticatorData { + rp_id_hash: rp_id_hash.clone(), + flags, + counter, + credential_data: None, + extensions: Default::default(), + }; + let assertion = Assertion { + credentials: Some(key_handle.clone()), + signature, + user: None, + auth_data, + }; + + Ok(GetAssertionResult { + assertion, + attachment: AuthenticatorAttachment::Unknown, + extensions: Default::default(), + }) + } +} + +#[derive(Debug, PartialEq)] +pub struct GetAssertionResponse { + pub credentials: Option, + pub auth_data: AuthenticatorData, + pub signature: Vec, + pub user: Option, + pub number_of_credentials: Option, +} + +impl CtapResponse for GetAssertionResponse {} + +impl<'de> Deserialize<'de> for GetAssertionResponse { + fn deserialize(deserializer: D) -> Result + 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(self, mut map: M) -> Result + 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 = 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, CommandError, GetAssertion, GetAssertionOptions, GetAssertionResult, HIDError, + StatusCode, + }; + use crate::consts::{ + Capability, HIDCmd, SW_CONDITIONS_NOT_SATISFIED, SW_NO_ERROR, U2F_CHECK_IS_REGISTERED, + U2F_REQUEST_USER_PRESENCE, + }; + use crate::ctap2::attestation::{AAGuid, AuthenticatorData, AuthenticatorDataFlags}; + use crate::ctap2::client_data::{Challenge, CollectedClientData, TokenBinding, WebauthnType}; + use crate::ctap2::commands::get_info::tests::AAGUID_RAW; + use crate::ctap2::commands::get_info::{ + AuthenticatorInfo, AuthenticatorOptions, AuthenticatorVersion, + }; + use crate::ctap2::commands::RequestCtap1; + use crate::ctap2::preflight::{ + do_credential_list_filtering_ctap1, do_credential_list_filtering_ctap2, + }; + use crate::ctap2::server::{ + AuthenticatorAttachment, PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity, + RelyingParty, RpIdHash, Transport, + }; + use crate::transport::device_selector::Device; + use crate::transport::hid::HIDDevice; + use crate::transport::{FidoDevice, FidoDeviceIO, FidoProtocol}; + use crate::u2ftypes::U2FDeviceInfo; + 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.hash().expect("failed to serialize client data"), + RelyingParty::from("example.com"), + 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(), + ); + let mut device = Device::new("commands/get_assertion").unwrap(); + assert_eq!(device.get_protocol(), FidoProtocol::CTAP2); + 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) + 0x62, // text(2) + 0x69, 0x64, // id + 0x58, // bytes( + ]); + device.add_write(&msg, 0); + + msg = cid.to_vec(); + msg.extend([0x0]); //SEQ + msg.extend([0x40]); // 64) + msg.extend(&assertion.allow_list[0].id[..58]); + device.add_write(&msg, 0); + + msg = cid.to_vec(); + msg.extend([0x1]); //SEQ + msg.extend(&assertion.allow_list[0].id[58..64]); + msg.extend(vec![ + 0x64, // text(4), + 0x74, 0x79, 0x70, 0x65, // type + 0x6a, // text(10) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // public-key + 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, 0x2a]); // 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(PublicKeyCredentialUserEntity { + 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, + ], + name: Some("johnpsmith@example.com".to_string()), + display_name: Some("John P. Smith".to_string()), + }), + auth_data: expected_auth_data, + }; + + let expected = vec![GetAssertionResult { + assertion: expected_assertion, + attachment: AuthenticatorAttachment::Unknown, + extensions: Default::default(), + }]; + 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 allowed_key = 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], + }; + let mut assertion = GetAssertion::new( + client_data.hash().expect("failed to serialize client data"), + RelyingParty::from("example.com"), + vec![allowed_key.clone()], + GetAssertionOptions { + user_presence: Some(true), + user_verification: None, + }, + Default::default(), + ); + let mut device = Device::new("commands/get_assertion").unwrap(); // not really used (all functions ignore it) + // channel id + device.downgrade_to_ctap1(); + assert_eq!(device.get_protocol(), FidoProtocol::CTAP1); + 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 key_handle = do_credential_list_filtering_ctap1( + &mut device, + &assertion.allow_list, + &assertion.rp, + &assertion.client_data_hash, + ) + .expect("Did not find a key_handle, even though it should have"); + assertion.allow_list = vec![key_handle]; + let (ctap1_request, key_handle) = assertion.ctap1_format().unwrap(); + assert_eq!(key_handle, allowed_key); + // 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 + // Pre-flighting is not done automatically + 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: Some(allowed_key), + 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 = vec![GetAssertionResult { + assertion: expected_assertion, + attachment: AuthenticatorAttachment::Unknown, + extensions: Default::default(), + }]; + 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.hash().expect("failed to serialize client data"), + RelyingParty::from("example.com"), + vec![too_long_key_handle.clone()], + GetAssertionOptions { + user_presence: Some(true), + user_verification: None, + }, + Default::default(), + ); + + let mut device = Device::new("commands/get_assertion").unwrap(); // not really used (all functions ignore it) + // channel id + device.downgrade_to_ctap1(); + assert_eq!(device.get_protocol(), FidoProtocol::CTAP1); + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + + device.set_cid(cid); + + assert_matches!( + do_credential_list_filtering_ctap1( + &mut device, + &assertion.allow_list, + &assertion.rp, + &assertion.client_data_hash, + ), + None + ); + assertion.allow_list = vec![]; + // It should also fail when trying to format + assert_matches!( + assertion.ctap1_format(), + Err(HIDError::Command(CommandError::StatusCode( + StatusCode::NoCredentials, + .. + ))) + ); + + // Test also multiple too long keys and an empty allow list + for allow_list in [vec![], vec![too_long_key_handle.clone(); 5]] { + assertion.allow_list = allow_list; + + assert_matches!( + do_credential_list_filtering_ctap1( + &mut device, + &assertion.allow_list, + &assertion.rp, + &assertion.client_data_hash, + ), + None + ); + } + + 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.clone(), + too_long_key_handle, + ]; + + // ctap1 request + fill_device_ctap1( + &mut device, + cid, + U2F_CHECK_IS_REGISTERED, + SW_CONDITIONS_NOT_SATISFIED, + ); + let key_handle = do_credential_list_filtering_ctap1( + &mut device, + &assertion.allow_list, + &assertion.rp, + &assertion.client_data_hash, + ) + .expect("Did not find a key_handle, even though it should have"); + assertion.allow_list = vec![key_handle]; + let (ctap1_request, key_handle) = assertion.ctap1_format().unwrap(); + assert_eq!(key_handle, ok_key_handle); + // 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 + // Pre-flighting is not done automatically + 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: Some(ok_key_handle), + 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 = vec![GetAssertionResult { + assertion: expected_assertion, + attachment: AuthenticatorAttachment::Unknown, + extensions: Default::default(), + }]; + assert_eq!(response, expected); + } + + #[test] + fn test_get_assertion_ctap2_pre_flight() { + 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.hash().expect("failed to serialize client data"), + RelyingParty::from("example.com"), + vec![ + // This should never be tested, because it gets pre-filtered, since it is too long + // (see max_credential_id_length) + PublicKeyCredentialDescriptor { + id: vec![0x10; 100], + transports: vec![Transport::USB], + }, + // One we test and skip + PublicKeyCredentialDescriptor { + id: vec![ + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, + ], + transports: vec![Transport::USB], + }, + // This one is the 'right' one + 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], + }, + // We should never test this one + PublicKeyCredentialDescriptor { + id: vec![ + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, + ], + transports: vec![Transport::USB], + }, + ], + GetAssertionOptions { + user_presence: Some(true), + user_verification: None, + }, + Default::default(), + ); + let mut device = Device::new("commands/get_assertion").unwrap(); + assert_eq!(device.get_protocol(), FidoProtocol::CTAP2); + let mut cid = [0u8; 4]; + thread_rng().fill_bytes(&mut cid); + device.set_cid(cid); + device.set_device_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 | Capability::CBOR, + }); + device.set_authenticator_info(AuthenticatorInfo { + versions: vec![AuthenticatorVersion::U2F_V2, AuthenticatorVersion::FIDO_2_0], + 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, + ..Default::default() + }, + max_msg_size: Some(1200), + pin_protocols: Some(vec![1]), + max_credential_count_in_list: None, + max_credential_id_length: Some(80), + 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, + }); + + // Sending first GetAssertion with first allow_list-entry, that will return an error + 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) + 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, + 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, + 0x78, 0x52, 0xb8, 0x55, // empty hash + 0x3, //allowList + 0x81, // array(1) + 0xa2, // map(2) + 0x62, // text(2) + 0x69, 0x64, // id + 0x58, // bytes( + ]); + device.add_write(&msg, 0); + + msg = cid.to_vec(); + msg.extend([0x0]); //SEQ + msg.extend([0x40]); // 64) + msg.extend(&assertion.allow_list[1].id[..58]); + device.add_write(&msg, 0); + + msg = cid.to_vec(); + msg.extend([0x1]); //SEQ + msg.extend(&assertion.allow_list[1].id[58..64]); + msg.extend(vec![ + 0x64, // text(4), + 0x74, 0x79, 0x70, 0x65, // type + 0x6a, // text(10) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // public-key + 0x5, // options + 0xa1, // map(1) + 0x62, // text(2) + 0x75, 0x70, // up + 0xf4, // false + ]); + device.add_write(&msg, 0); + + // fido response + let len = 0x1; + let mut msg = cid.to_vec(); + msg.extend(vec![HIDCmd::Cbor.into(), 0x00, len]); // cmd + bcnt + msg.push(0x2e); // Status code: NoCredentials + device.add_read(&msg, 0); + + // Sending second GetAssertion with first allow_list-entry, that will return a success + 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) + 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, + 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, + 0x78, 0x52, 0xb8, 0x55, // empty hash + 0x3, //allowList + 0x81, // array(1) + 0xa2, // map(2) + 0x62, // text(2) + 0x69, 0x64, // id + 0x58, // bytes( + ]); + device.add_write(&msg, 0); + + msg = cid.to_vec(); + msg.extend([0x0]); //SEQ + msg.extend([0x40]); // 64) + msg.extend(&assertion.allow_list[2].id[..58]); + device.add_write(&msg, 0); + + msg = cid.to_vec(); + msg.extend([0x1]); //SEQ + msg.extend(&assertion.allow_list[2].id[58..64]); + msg.extend(vec![ + 0x64, // text(4), + 0x74, 0x79, 0x70, 0x65, // type + 0x6a, // text(10) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // public-key + 0x5, // options + 0xa1, // map(1) + 0x62, // text(2) + 0x75, 0x70, // up + 0xf4, // false + ]); + device.add_write(&msg, 0); + + let mut msg = cid.to_vec(); + msg.extend([HIDCmd::Cbor.into(), 0x1, 0x2a]); // 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); + + assert_matches!( + do_credential_list_filtering_ctap2( + &mut device, + &assertion.allow_list, + &assertion.rp, + None, + ), + Ok(..) + ); + } + + #[test] + fn test_get_assertion_ctap1_flags() { + // Ensure that only the two low bits of flags are preserved when repackaging a + // CTAP1 response. + let mut sample = GET_ASSERTION_SAMPLE_RESPONSE_CTAP1.to_vec(); + sample[0] = 0xff; // Set all 8 flag bits before repackaging + let add_info = PublicKeyCredentialDescriptor { + id: vec![], + transports: vec![], + }; + let rp_hash = RpIdHash([0u8; 32]); + let resp = GetAssertionResult::from_ctap1(&sample, &rp_hash, &add_info) + .expect("could not handle response"); + assert_eq!( + resp.assertion.auth_data.flags, + AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::RESERVED_1 + ); + } + + // 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; 298] = [ + 0x00, // status == success + 0xA5, // map(5) + 0x01, // unsigned(1) + 0xA2, // map(2) + 0x62, // text(2) + 0x69, 0x64, // "id" + 0x58, 0x40, // bytes(0x64, ) credential_id + 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, // end: credential_id + 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, ) auth_data + 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, // end: auth_data + 0x03, // unsigned(3) + 0x58, 0x47, // bytes(0x71, ) signature + 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, // end: signature + 0x04, // unsigned(4) + 0xA3, // map(3) + 0x62, // text(2) + 0x69, 0x64, // "id" + 0x58, 0x20, // bytes(0x32, ) user_id + 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, // end: user_id + 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..f676605a0b --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/get_info.rs @@ -0,0 +1,1054 @@ +use super::{Command, CommandError, CtapResponse, RequestCtap2, StatusCode}; +use crate::ctap2::attestation::AAGuid; +use crate::ctap2::server::PublicKeyCredentialParameters; +use crate::transport::errors::HIDError; +use crate::transport::{FidoDevice, VirtualFidoDevice}; +use serde::{ + de::{Error as SError, IgnoredAny, MapAccess, Visitor}, + Deserialize, Deserializer, Serialize, +}; +use serde_cbor::{de::from_slice, Value}; +use std::collections::BTreeMap; +use std::fmt; + +#[derive(Debug, Default)] +pub struct GetInfo {} + +impl RequestCtap2 for GetInfo { + type Output = AuthenticatorInfo; + + fn command(&self) -> Command { + Command::GetInfo + } + + fn wire_format(&self) -> Result, HIDError> { + Ok(Vec::new()) + } + + fn handle_response_ctap2( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result { + 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 send_to_virtual_device( + &self, + dev: &mut Dev, + ) -> Result { + dev.get_info() + } +} + +fn true_val() -> bool { + true +} + +#[derive(Debug, Deserialize, Clone, Eq, PartialEq, Serialize)] +pub 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 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 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 client_pin: Option, + + /// Indicates that the device is capable of testing user presence. + #[serde(rename = "up", default = "true_val")] + pub 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 user_verification: Option, + + // ---------------------------------------------------- + // CTAP 2.1 options + // ---------------------------------------------------- + /// If pinUvAuthToken is: + /// present and set to true + /// if the clientPin option id is present and set to true, then the + /// authenticator supports authenticatorClientPIN's getPinUvAuthTokenUsingPinWithPermissions + /// subcommand. If the uv option id is present and set to true, then + /// the authenticator supports authenticatorClientPIN's getPinUvAuthTokenUsingUvWithPermissions + /// subcommand. + /// present and set to false, or absent. + /// the authenticator does not support authenticatorClientPIN's + /// getPinUvAuthTokenUsingPinWithPermissions and getPinUvAuthTokenUsingUvWithPermissions + /// subcommands. + #[serde(rename = "pinUvAuthToken")] + pub pin_uv_auth_token: Option, + + /// If this noMcGaPermissionsWithClientPin is: + /// present and set to true + /// A pinUvAuthToken obtained via getPinUvAuthTokenUsingPinWithPermissions + /// (or getPinToken) cannot be used for authenticatorMakeCredential or + /// authenticatorGetAssertion commands, because it will lack the necessary + /// mc and ga permissions. In this situation, platforms SHOULD NOT attempt + /// to use getPinUvAuthTokenUsingPinWithPermissions if using + /// getPinUvAuthTokenUsingUvWithPermissions fails. + /// present and set to false, or absent. + /// A pinUvAuthToken obtained via getPinUvAuthTokenUsingPinWithPermissions + /// (or getPinToken) can be used for authenticatorMakeCredential or + /// authenticatorGetAssertion commands. + /// Note: noMcGaPermissionsWithClientPin MUST only be present if the + /// clientPin option ID is present. + #[serde(rename = "noMcGaPermissionsWithClientPin")] + pub no_mc_ga_permissions_with_client_pin: Option, + + /// If largeBlobs is: + /// present and set to true + /// the authenticator supports the authenticatorLargeBlobs command. + /// present and set to false, or absent. + /// The authenticatorLargeBlobs command is NOT supported. + #[serde(rename = "largeBlobs")] + pub large_blobs: Option, + + /// Enterprise Attestation feature support: + /// If ep is: + /// Present and set to true + /// The authenticator is enterprise attestation capable, and enterprise + /// attestation is enabled. + /// Present and set to false + /// The authenticator is enterprise attestation capable, and enterprise + /// attestation is disabled. + /// Absent + /// The Enterprise Attestation feature is NOT supported. + #[serde(rename = "ep")] + pub ep: Option, + + /// If bioEnroll is: + /// present and set to true + /// the authenticator supports the authenticatorBioEnrollment commands, + /// and has at least one bio enrollment presently provisioned. + /// present and set to false + /// the authenticator supports the authenticatorBioEnrollment commands, + /// and does not yet have any bio enrollments provisioned. + /// absent + /// the authenticatorBioEnrollment commands are NOT supported. + #[serde(rename = "bioEnroll")] + pub bio_enroll: Option, + + /// "FIDO_2_1_PRE" Prototype Credential management support: + /// If userVerificationMgmtPreview is: + /// present and set to true + /// the authenticator supports the Prototype authenticatorBioEnrollment (0x41) + /// commands, and has at least one bio enrollment presently provisioned. + /// present and set to false + /// the authenticator supports the Prototype authenticatorBioEnrollment (0x41) + /// commands, and does not yet have any bio enrollments provisioned. + /// absent + /// the Prototype authenticatorBioEnrollment (0x41) commands are not supported. + #[serde(rename = "userVerificationMgmtPreview")] + pub user_verification_mgmt_preview: Option, + + /// getPinUvAuthTokenUsingUvWithPermissions support for requesting the be permission: + /// This option ID MUST only be present if bioEnroll is also present. + /// If uvBioEnroll is: + /// present and set to true + /// requesting the be permission when invoking getPinUvAuthTokenUsingUvWithPermissions + /// is supported. + /// present and set to false, or absent. + /// requesting the be permission when invoking getPinUvAuthTokenUsingUvWithPermissions + /// is NOT supported. + #[serde(rename = "uvBioEnroll")] + pub uv_bio_enroll: Option, + + /// authenticatorConfig command support: + /// If authnrCfg is: + /// present and set to true + /// the authenticatorConfig command is supported. + /// present and set to false, or absent. + /// the authenticatorConfig command is NOT supported. + #[serde(rename = "authnrCfg")] + pub authnr_cfg: Option, + + /// getPinUvAuthTokenUsingUvWithPermissions support for requesting the acfg permission: + /// This option ID MUST only be present if authnrCfg is also present. + /// If uvAcfg is: + /// present and set to true + /// requesting the acfg permission when invoking getPinUvAuthTokenUsingUvWithPermissions + /// is supported. + /// present and set to false, or absent. + /// requesting the acfg permission when invoking getPinUvAuthTokenUsingUvWithPermissions + /// is NOT supported. + #[serde(rename = "uvAcfg")] + pub uv_acfg: Option, + + /// Credential management support: + /// If credMgmt is: + /// present and set to true + /// the authenticatorCredentialManagement command is supported. + /// present and set to false, or absent. + /// the authenticatorCredentialManagement command is NOT supported. + #[serde(rename = "credMgmt")] + pub cred_mgmt: Option, + + /// "FIDO_2_1_PRE" Prototype Credential management support: + /// If credentialMgmtPreview is: + /// present and set to true + /// the Prototype authenticatorCredentialManagement (0x41) command is supported. + /// present and set to false, or absent. + /// the Prototype authenticatorCredentialManagement (0x41) command is NOT supported. + #[serde(rename = "credentialMgmtPreview")] + pub credential_mgmt_preview: Option, + + /// Support for the Set Minimum PIN Length feature. + /// If setMinPINLength is: + /// present and set to true + /// the setMinPINLength subcommand is supported. + /// present and set to false, or absent. + /// the setMinPINLength subcommand is NOT supported. + /// Note: setMinPINLength MUST only be present if the clientPin option ID is present. + #[serde(rename = "setMinPINLength")] + pub set_min_pin_length: Option, + + /// Support for making non-discoverable credentials without requiring User Verification. + /// If makeCredUvNotRqd is: + /// present and set to true + /// the authenticator allows creation of non-discoverable credentials without + /// requiring any form of user verification, if the platform requests this behaviour. + /// present and set to false, or absent. + /// the authenticator requires some form of user verification for creating + /// non-discoverable credentials, regardless of the parameters the platform supplies + /// for the authenticatorMakeCredential command. + /// Authenticators SHOULD include this option with the value true. + #[serde(rename = "makeCredUvNotRqd")] + pub make_cred_uv_not_rqd: Option, + + /// Support for the Always Require User Verification feature: + /// If alwaysUv is + /// present and set to true + /// the authenticator supports the Always Require User Verification feature and it is enabled. + /// present and set to false + /// the authenticator supports the Always Require User Verification feature but it is disabled. + /// absent + /// the authenticator does not support the Always Require User Verification feature. + /// Note: If the alwaysUv option ID is present and true the authenticator MUST set the value + /// of makeCredUvNotRqd to false. + #[serde(rename = "alwaysUv")] + pub always_uv: Option, +} + +impl Default for AuthenticatorOptions { + fn default() -> Self { + AuthenticatorOptions { + platform_device: false, + resident_key: false, + client_pin: None, + user_presence: true, + user_verification: None, + pin_uv_auth_token: None, + no_mc_ga_permissions_with_client_pin: None, + large_blobs: None, + ep: None, + bio_enroll: None, + user_verification_mgmt_preview: None, + uv_bio_enroll: None, + authnr_cfg: None, + uv_acfg: None, + cred_mgmt: None, + credential_mgmt_preview: None, + set_min_pin_length: None, + make_cred_uv_not_rqd: None, + always_uv: None, + } + } +} + +#[allow(non_camel_case_types)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub enum AuthenticatorVersion { + U2F_V2, + FIDO_2_0, + FIDO_2_1_PRE, + FIDO_2_1, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)] +pub struct AuthenticatorInfo { + pub versions: Vec, + pub extensions: Vec, + pub aaguid: AAGuid, + pub options: AuthenticatorOptions, + pub max_msg_size: Option, + pub pin_protocols: Option>, + // CTAP 2.1 + pub max_credential_count_in_list: Option, + pub max_credential_id_length: Option, + pub transports: Option>, + pub algorithms: Option>, + pub max_ser_large_blob_array: Option, + pub force_pin_change: Option, + pub min_pin_length: Option, + pub firmware_version: Option, + pub max_cred_blob_length: Option, + pub max_rpids_for_set_min_pin_length: Option, + pub preferred_platform_uv_attempts: Option, + pub uv_modality: Option, + pub certifications: Option>, + pub remaining_discoverable_credentials: Option, + pub vendor_prototype_config_commands: Option>, +} + +impl AuthenticatorInfo { + pub fn supports_cred_protect(&self) -> bool { + self.extensions.contains(&"credProtect".to_string()) + } + + pub fn supports_hmac_secret(&self) -> bool { + self.extensions.contains(&"hmac-secret".to_string()) + } + + pub fn max_supported_version(&self) -> AuthenticatorVersion { + let versions = vec![ + AuthenticatorVersion::FIDO_2_1, + AuthenticatorVersion::FIDO_2_1_PRE, + AuthenticatorVersion::FIDO_2_0, + AuthenticatorVersion::U2F_V2, + ]; + for ver in versions { + if self.versions.contains(&ver) { + return ver; + } + } + AuthenticatorVersion::U2F_V2 + } + + pub fn device_is_protected(&self) -> bool { + self.options.client_pin == Some(true) || self.options.user_verification == Some(true) + } +} + +impl CtapResponse for AuthenticatorInfo {} + +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(deserializer: D) -> Result + 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(self, mut map: M) -> Result + 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: Option> = None; + 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 => { + parse_next_optional_value!(pin_protocols, map); + } + 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::()?; + continue; + } + } + } + + if versions.is_empty() { + return Err(M::Error::custom( + "expected at least one version, got none".to_string(), + )); + } + + if let Some(protocols) = &pin_protocols { + if protocols.is_empty() { + return Err(M::Error::custom( + "Token returned empty PIN protocol list, which is not allowed" + .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, FidoProtocol}; + 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![AuthenticatorVersion::U2F_V2, AuthenticatorVersion::FIDO_2_0], + 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, + ..Default::default() + }, + max_msg_size: Some(1200), + pin_protocols: Some(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![ + AuthenticatorVersion::U2F_V2, + AuthenticatorVersion::FIDO_2_0, + AuthenticatorVersion::FIDO_2_1_PRE, + AuthenticatorVersion::FIDO_2_1, + ], + 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), + pin_uv_auth_token: Some(true), + no_mc_ga_permissions_with_client_pin: None, + large_blobs: Some(true), + ep: None, + bio_enroll: Some(true), + user_verification_mgmt_preview: Some(true), + uv_bio_enroll: None, + authnr_cfg: Some(true), + uv_acfg: None, + cred_mgmt: Some(true), + credential_mgmt_preview: Some(true), + set_min_pin_length: Some(true), + make_cred_uv_not_rqd: Some(false), + always_uv: Some(true), + }, + max_msg_size: Some(1200), + pin_protocols: Some(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(); + assert_eq!(device.get_protocol(), FidoProtocol::CTAP2); + 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().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![AuthenticatorVersion::U2F_V2, AuthenticatorVersion::FIDO_2_0], + 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, + ..Default::default() + }, + max_msg_size: Some(1200), + pin_protocols: Some(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); + } + + #[test] + fn test_authenticator_info_max_version() { + let fido2_0 = AuthenticatorInfo { + versions: vec![AuthenticatorVersion::U2F_V2, AuthenticatorVersion::FIDO_2_0], + ..Default::default() + }; + assert_eq!( + fido2_0.max_supported_version(), + AuthenticatorVersion::FIDO_2_0 + ); + + let fido2_1_pre = AuthenticatorInfo { + versions: vec![ + AuthenticatorVersion::FIDO_2_1_PRE, + AuthenticatorVersion::U2F_V2, + ], + ..Default::default() + }; + assert_eq!( + fido2_1_pre.max_supported_version(), + AuthenticatorVersion::FIDO_2_1_PRE + ); + + let fido2_1 = AuthenticatorInfo { + versions: vec![ + AuthenticatorVersion::FIDO_2_1_PRE, + AuthenticatorVersion::FIDO_2_1, + AuthenticatorVersion::U2F_V2, + AuthenticatorVersion::FIDO_2_0, + ], + ..Default::default() + }; + assert_eq!( + fido2_1.max_supported_version(), + AuthenticatorVersion::FIDO_2_1 + ); + } + + #[test] + fn parse_authenticator_info_protocol_versions() { + let mut expected = AuthenticatorInfo { + versions: vec![AuthenticatorVersion::U2F_V2, AuthenticatorVersion::FIDO_2_0], + 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, + ..Default::default() + }, + max_msg_size: Some(1200), + pin_protocols: None, + ..Default::default() + }; + + let raw_data = AUTHENTICATOR_INFO_PAYLOAD.to_vec(); + // pin protocol entry (last 3 bytes in payload): + // 0x06, // unsigned(6) + // 0x81, // array(1) + // 0x01, // unsigned(1) + + let mut raw_empty_list = raw_data.clone(); + raw_empty_list.pop(); + let raw_list_len = raw_empty_list.len(); + raw_empty_list[raw_list_len - 1] = 0x80; // array(0) instead of array(1) + + // Empty protocols-array, that should produce an error + from_slice::(&raw_empty_list).unwrap_err(); + + // No protocols specified + let mut raw_no_list = raw_data.clone(); + raw_no_list.pop(); + raw_no_list.pop(); + raw_no_list.pop(); + raw_no_list[0] = 0xa5; // map(5) instead of map(6) + + let authenticator_info: AuthenticatorInfo = from_slice(&raw_no_list).unwrap(); + assert_eq!(authenticator_info, expected); + + // Both 1 and 2 + let mut raw_list = raw_data; + let raw_list_len = raw_list.len(); + raw_list[raw_list_len - 2] = 0x82; // array(2) instead of array(1) + raw_list.push(0x02); + + expected.pin_protocols = Some(vec![1, 2]); + let authenticator_info: AuthenticatorInfo = from_slice(&raw_list).unwrap(); + assert_eq!(authenticator_info, 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..b75bb51b08 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/get_next_assertion.rs @@ -0,0 +1,54 @@ +use super::{Command, CommandError, RequestCtap2, StatusCode}; +use crate::ctap2::commands::get_assertion::GetAssertionResponse; +use crate::transport::errors::HIDError; +use crate::transport::{FidoDevice, VirtualFidoDevice}; +use serde_cbor::{de::from_slice, Value}; + +#[derive(Debug)] +pub(crate) struct GetNextAssertion; + +impl RequestCtap2 for GetNextAssertion { + type Output = GetAssertionResponse; + + fn command(&self) -> Command { + Command::GetNextAssertion + } + + fn wire_format(&self) -> Result, HIDError> { + Ok(Vec::new()) + } + + fn handle_response_ctap2( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result { + 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()) + } + } + + fn send_to_virtual_device( + &self, + _dev: &mut Dev, + ) -> Result { + unimplemented!() + } +} 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..40019c8f1a --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/get_version.rs @@ -0,0 +1,121 @@ +use super::{CommandError, CtapResponse, RequestCtap1, Retryable}; +use crate::consts::U2F_VERSION; +use crate::transport::errors::{ApduErrorStatus, HIDError}; +use crate::transport::{FidoDevice, VirtualFidoDevice}; +use crate::u2ftypes::CTAP1RequestAPDU; + +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum U2FInfo { + U2F_V2, +} + +impl CtapResponse for U2FInfo {} + +#[derive(Debug, Default)] +// 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 RequestCtap1 for GetVersion { + type Output = U2FInfo; + type AdditionalInfo = (); + + fn handle_response_ctap1( + &self, + _dev: &mut Dev, + _status: Result<(), ApduErrorStatus>, + input: &[u8], + _add_info: &(), + ) -> Result> { + 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(&self) -> Result<(Vec, ()), HIDError> { + let flags = 0; + + let cmd = U2F_VERSION; + let data = CTAP1RequestAPDU::serialize(cmd, flags, &[])?; + Ok((data, ())) + } + + fn send_to_virtual_device( + &self, + dev: &mut Dev, + ) -> Result { + dev.get_version(self) + } +} + +#[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, FidoProtocol}; + use rand::{thread_rng, RngCore}; + + #[test] + fn test_get_version_ctap1_only() { + let mut device = Device::new("commands/get_version").unwrap(); + device.downgrade_to_ctap1(); + assert_eq!(device.get_protocol(), FidoProtocol::CTAP1); + 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().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..1de1048404 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/make_credentials.rs @@ -0,0 +1,1061 @@ +use super::get_info::{AuthenticatorInfo, AuthenticatorVersion}; +use super::{ + Command, CommandError, CtapResponse, PinUvAuthCommand, RequestCtap1, RequestCtap2, Retryable, + StatusCode, +}; +use crate::consts::{PARAMETER_SIZE, U2F_REGISTER, U2F_REQUEST_USER_PRESENCE}; +use crate::crypto::{ + parse_u2f_der_certificate, COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve, + PinUvAuthParam, PinUvAuthToken, +}; +use crate::ctap2::attestation::{ + AAGuid, AttestationObject, AttestationStatement, AttestationStatementFidoU2F, + AttestedCredentialData, AuthenticatorData, AuthenticatorDataFlags, HmacSecretResponse, +}; +use crate::ctap2::client_data::ClientDataHash; +use crate::ctap2::server::{ + AuthenticationExtensionsClientInputs, AuthenticationExtensionsClientOutputs, + AuthenticatorAttachment, CredentialProtectionPolicy, PublicKeyCredentialDescriptor, + PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, RelyingParty, RpIdHash, + UserVerificationRequirement, +}; +use crate::ctap2::utils::{read_byte, serde_parse_err}; +use crate::errors::AuthenticatorError; +use crate::transport::errors::{ApduErrorStatus, HIDError}; +use crate::transport::{FidoDevice, VirtualFidoDevice}; +use crate::u2ftypes::CTAP1RequestAPDU; +use serde::{ + de::{Error as DesError, MapAccess, Unexpected, Visitor}, + ser::SerializeMap, + Deserialize, Deserializer, Serialize, Serializer, +}; +use serde_cbor::{self, de::from_slice, ser, Value}; +use std::fmt; +use std::io::{Cursor, Read}; + +#[derive(Debug, PartialEq, Eq)] +pub struct MakeCredentialsResult { + pub att_obj: AttestationObject, + pub attachment: AuthenticatorAttachment, + pub extensions: AuthenticationExtensionsClientOutputs, +} + +impl MakeCredentialsResult { + pub fn from_ctap1(input: &[u8], rp_id_hash: &RpIdHash) -> Result { + let mut data = Cursor::new(input); + let magic_num = read_byte(&mut data).map_err(CommandError::Deserializing)?; + if magic_num != 0x05 { + error!("error while parsing registration: magic header not 0x05, but {magic_num}"); + return Err(CommandError::Deserializing(DesError::invalid_value( + serde::de::Unexpected::Unsigned(magic_num as u64), + &"0x05", + ))); + } + let mut public_key = [0u8; 65]; + data.read_exact(&mut public_key) + .map_err(|_| CommandError::Deserializing(serde_parse_err("PublicKey")))?; + + let credential_id_len = read_byte(&mut data).map_err(CommandError::Deserializing)?; + let mut credential_id = vec![0u8; credential_id_len as usize]; + data.read_exact(&mut credential_id) + .map_err(|_| CommandError::Deserializing(serde_parse_err("CredentialId")))?; + + let cert_and_sig = parse_u2f_der_certificate(&data.get_ref()[data.position() as usize..]) + .map_err(|err| { + CommandError::Deserializing(serde_parse_err(&format!( + "Certificate and Signature: {err:?}", + ))) + })?; + + let credential_ec2_key = COSEEC2Key::from_sec1_uncompressed(Curve::SECP256R1, &public_key) + .map_err(|err| { + CommandError::Deserializing(serde_parse_err(&format!("EC2 Key: {err:?}",))) + })?; + + let credential_public_key = COSEKey { + alg: COSEAlgorithm::ES256, + key: COSEKeyType::EC2(credential_ec2_key), + }; + + let auth_data = AuthenticatorData { + rp_id_hash: rp_id_hash.clone(), + // 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, + credential_public_key, + }), + extensions: Default::default(), + }; + + let att_stmt = AttestationStatement::FidoU2F(AttestationStatementFidoU2F::new( + cert_and_sig.certificate, + cert_and_sig.signature, + )); + + let att_obj = AttestationObject { + auth_data, + att_stmt, + }; + + Ok(Self { + att_obj, + attachment: AuthenticatorAttachment::Unknown, + extensions: Default::default(), + }) + } +} + +impl<'de> Deserialize<'de> for MakeCredentialsResult { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct MakeCredentialsResultVisitor; + + impl<'de> Visitor<'de> for MakeCredentialsResultVisitor { + type Value = MakeCredentialsResult; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a cbor map") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'de>, + { + let mut format: Option<&str> = None; + let mut auth_data: Option = None; + let mut att_stmt: Option = None; + + while let Some(key) = map.next_key()? { + match key { + 1 => { + if format.is_some() { + return Err(DesError::duplicate_field("fmt (0x01)")); + } + format = Some(map.next_value()?); + } + 2 => { + if auth_data.is_some() { + return Err(DesError::duplicate_field("authData (0x02)")); + } + auth_data = Some(map.next_value()?); + } + 3 => { + let format = + format.ok_or_else(|| DesError::missing_field("fmt (0x01)"))?; + if att_stmt.is_some() { + return Err(DesError::duplicate_field("attStmt (0x03)")); + } + att_stmt = match format { + "none" => { + let map: std::collections::BTreeMap<(), ()> = + map.next_value()?; + if !map.is_empty() { + return Err(DesError::invalid_value( + Unexpected::Map, + &"the empty map", + )); + } + Some(AttestationStatement::None) + } + "packed" => Some(AttestationStatement::Packed(map.next_value()?)), + "fido-u2f" => { + Some(AttestationStatement::FidoU2F(map.next_value()?)) + } + _ => { + return Err(DesError::custom( + "unknown attestation statement format", + )) + } + } + } + _ => continue, + } + } + + let auth_data = auth_data + .ok_or_else(|| M::Error::custom("found no authData (0x02)".to_string()))?; + let att_stmt = att_stmt + .ok_or_else(|| M::Error::custom("found no attStmt (0x03)".to_string()))?; + + Ok(MakeCredentialsResult { + att_obj: AttestationObject { + auth_data, + att_stmt, + }, + attachment: AuthenticatorAttachment::Unknown, + extensions: Default::default(), + }) + } + } + + deserializer.deserialize_bytes(MakeCredentialsResultVisitor) + } +} + +impl CtapResponse for MakeCredentialsResult {} + +#[derive(Copy, Clone, Debug, Default, Serialize)] +#[cfg_attr(test, derive(Deserialize))] +pub struct MakeCredentialsOptions { + #[serde(rename = "rk", skip_serializing_if = "Option::is_none")] + pub resident_key: Option, + #[serde(rename = "uv", skip_serializing_if = "Option::is_none")] + pub user_verification: Option, + // 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 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, Default, Clone, Serialize)] +pub struct MakeCredentialsExtensions { + #[serde(skip_serializing)] + pub cred_props: Option, + #[serde(rename = "credProtect", skip_serializing_if = "Option::is_none")] + pub cred_protect: Option, + #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")] + pub hmac_secret: Option, + #[serde(rename = "minPinLength", skip_serializing_if = "Option::is_none")] + pub min_pin_length: Option, +} + +impl MakeCredentialsExtensions { + fn has_content(&self) -> bool { + self.cred_protect.is_some() || self.hmac_secret.is_some() || self.min_pin_length.is_some() + } +} + +impl From for MakeCredentialsExtensions { + fn from(input: AuthenticationExtensionsClientInputs) -> Self { + Self { + cred_props: input.cred_props, + cred_protect: input.credential_protection_policy, + hmac_secret: input.hmac_create_secret, + min_pin_length: input.min_pin_length, + } + } +} + +#[derive(Debug, Clone)] +pub struct MakeCredentials { + pub client_data_hash: ClientDataHash, + pub rp: RelyingParty, + // Note(baloo): If none -> ctap1 + pub user: Option, + pub pub_cred_params: Vec, + pub exclude_list: Vec, + + // 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 extensions: MakeCredentialsExtensions, + pub options: MakeCredentialsOptions, + pub pin_uv_auth_param: Option, + pub enterprise_attestation: Option, +} + +impl MakeCredentials { + #[allow(clippy::too_many_arguments)] + pub fn new( + client_data_hash: ClientDataHash, + rp: RelyingParty, + user: Option, + pub_cred_params: Vec, + exclude_list: Vec, + options: MakeCredentialsOptions, + extensions: MakeCredentialsExtensions, + ) -> Self { + Self { + client_data_hash, + rp, + user, + pub_cred_params, + exclude_list, + extensions, + options, + pin_uv_auth_param: None, + enterprise_attestation: None, + } + } + + pub fn finalize_result(&self, dev: &Dev, result: &mut MakeCredentialsResult) { + let maybe_info = dev.get_authenticator_info(); + + result.attachment = match maybe_info { + Some(info) if info.options.platform_device => AuthenticatorAttachment::Platform, + Some(_) => AuthenticatorAttachment::CrossPlatform, + None => AuthenticatorAttachment::Unknown, + }; + + // Handle extensions whose outputs are not encoded in the authenticator data. + // 1. credProps + // "set clientExtensionResults["credProps"]["rk"] to the value of the + // requireResidentKey parameter that was used in the invocation of the + // authenticatorMakeCredential operation." + // Note: a CTAP 2.0 authenticator is allowed to create a discoverable credential even + // if one was not requested, so there is a case in which we cannot confidently + // return `rk=false` here. We omit the response entirely in this case. + let dev_supports_rk = maybe_info.map_or(false, |info| info.options.resident_key); + let requested_rk = self.options.resident_key.unwrap_or(false); + let max_supported_version = maybe_info.map_or(AuthenticatorVersion::U2F_V2, |info| { + info.max_supported_version() + }); + let rk_uncertain = max_supported_version == AuthenticatorVersion::FIDO_2_0 + && dev_supports_rk + && !requested_rk; + if self.extensions.cred_props == Some(true) && !rk_uncertain { + result + .extensions + .cred_props + .get_or_insert(Default::default()) + .rk = requested_rk; + } + + // 2. hmac-secret + // The extension returns a flag in the authenticator data which we need to mirror as a + // client output. + if self.extensions.hmac_secret == Some(true) { + if let Some(HmacSecretResponse::Confirmed(flag)) = + result.att_obj.auth_data.extensions.hmac_secret + { + result.extensions.hmac_create_secret = Some(flag); + } + } + } +} + +impl PinUvAuthCommand for MakeCredentials { + fn set_pin_uv_auth_param( + &mut self, + pin_uv_auth_token: Option, + ) -> Result<(), AuthenticatorError> { + let mut param = None; + if let Some(token) = pin_uv_auth_token { + param = Some( + token + .derive(self.client_data_hash.as_ref()) + .map_err(CommandError::Crypto)?, + ); + } + self.pin_uv_auth_param = param; + Ok(()) + } + + fn set_uv_option(&mut self, uv: Option) { + self.options.user_verification = uv; + } + + fn get_rp_id(&self) -> Option<&String> { + Some(&self.rp.id) + } + + fn can_skip_user_verification( + &mut self, + info: &AuthenticatorInfo, + uv_req: UserVerificationRequirement, + ) -> bool { + // TODO(MS): Handle here the case where we NEED a UV, the device supports PINs, but hasn't set a PIN. + // For this, the user has to be prompted to set a PIN first (see https://github.com/mozilla/authenticator-rs/issues/223) + + let supports_uv = info.options.user_verification == Some(true); + let pin_configured = info.options.client_pin == Some(true); + + // CTAP 2.0 authenticators require user verification if the device is protected + let device_protected = supports_uv || pin_configured; + + // CTAP 2.1 authenticators may allow the creation of non-discoverable credentials without + // user verification. This is only relevant if the relying party has not requested user + // verification. + let make_cred_uv_not_required = info.options.make_cred_uv_not_rqd == Some(true) + && self.options.resident_key != Some(true) + && uv_req == UserVerificationRequirement::Discouraged; + + // Alternatively, CTAP 2.1 authenticators may require user verification regardless of the + // RP's requirement. + let always_uv = info.options.always_uv == Some(true); + + !always_uv && (!device_protected || make_cred_uv_not_required) + } + + fn get_pin_uv_auth_param(&self) -> Option<&PinUvAuthParam> { + self.pin_uv_auth_param.as_ref() + } +} + +impl Serialize for MakeCredentials { + fn serialize(&self, serializer: S) -> Result + 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_content() { + map_len += 1; + } + if self.options.has_some() { + map_len += 1; + } + if self.pin_uv_auth_param.is_some() { + map_len += 2; + } + if self.enterprise_attestation.is_some() { + map_len += 1; + } + + let mut map = serializer.serialize_map(Some(map_len))?; + map.serialize_entry(&0x01, &self.client_data_hash)?; + map.serialize_entry(&0x02, &self.rp)?; + 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_content() { + map.serialize_entry(&0x06, &self.extensions)?; + } + if self.options.has_some() { + map.serialize_entry(&0x07, &self.options)?; + } + if let Some(pin_uv_auth_param) = &self.pin_uv_auth_param { + map.serialize_entry(&0x08, &pin_uv_auth_param)?; + map.serialize_entry(&0x09, &pin_uv_auth_param.pin_protocol.id())?; + } + if let Some(enterprise_attestation) = self.enterprise_attestation { + map.serialize_entry(&0x0a, &enterprise_attestation)?; + } + map.end() + } +} + +impl RequestCtap1 for MakeCredentials { + type Output = MakeCredentialsResult; + type AdditionalInfo = (); + + fn ctap1_format(&self) -> Result<(Vec, ()), HIDError> { + let flags = U2F_REQUEST_USER_PRESENCE; + + let mut register_data = Vec::with_capacity(2 * PARAMETER_SIZE); + register_data.extend_from_slice(self.client_data_hash.as_ref()); + 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, + dev: &mut Dev, + status: Result<(), ApduErrorStatus>, + input: &[u8], + _add_info: &(), + ) -> Result> { + if Err(ApduErrorStatus::ConditionsNotSatisfied) == status { + return Err(Retryable::Retry); + } + if let Err(err) = status { + return Err(Retryable::Error(HIDError::ApduStatus(err))); + } + + let mut output = MakeCredentialsResult::from_ctap1(input, &self.rp.hash()) + .map_err(|e| Retryable::Error(HIDError::Command(e)))?; + self.finalize_result(dev, &mut output); + Ok(output) + } + + fn send_to_virtual_device( + &self, + dev: &mut Dev, + ) -> Result { + let mut output = dev.make_credentials(self)?; + self.finalize_result(dev, &mut output); + Ok(output) + } +} + +impl RequestCtap2 for MakeCredentials { + type Output = MakeCredentialsResult; + + fn command(&self) -> Command { + Command::MakeCredentials + } + + fn wire_format(&self) -> Result, HIDError> { + Ok(ser::to_vec(&self).map_err(CommandError::Serializing)?) + } + + fn handle_response_ctap2( + &self, + dev: &mut Dev, + input: &[u8], + ) -> Result { + 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() { + return Err(HIDError::Command(CommandError::InputTooSmall)); + } + return Err(HIDError::Command(CommandError::StatusCode(status, None))); + } + + if status.is_ok() { + let mut output: MakeCredentialsResult = + from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + self.finalize_result(dev, &mut output); + Ok(output) + } else { + let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?; + Err(HIDError::Command(CommandError::StatusCode( + status, + Some(data), + ))) + } + } + + fn send_to_virtual_device( + &self, + dev: &mut Dev, + ) -> Result { + let mut output = dev.make_credentials(self)?; + self.finalize_result(dev, &mut output); + Ok(output) + } +} + +pub(crate) fn dummy_make_credentials_cmd() -> MakeCredentials { + let mut req = MakeCredentials::new( + // Hardcoded hash of: + // CollectedClientData { + // webauthn_type: WebauthnType::Create, + // challenge: Challenge::new(vec![0, 1, 2, 3, 4]), + // origin: String::new(), + // cross_origin: false, + // token_binding: None, + // } + ClientDataHash([ + 208, 206, 230, 252, 125, 191, 89, 154, 145, 157, 184, 251, 149, 19, 17, 38, 159, 14, + 183, 129, 247, 132, 28, 108, 192, 84, 74, 217, 218, 52, 21, 75, + ]), + RelyingParty::from("make.me.blink"), + Some(PublicKeyCredentialUserEntity { + id: vec![0], + name: Some(String::from("make.me.blink")), + ..Default::default() + }), + vec![PublicKeyCredentialParameters { + alg: COSEAlgorithm::ES256, + }], + vec![], + MakeCredentialsOptions::default(), + MakeCredentialsExtensions::default(), + ); + // 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. + req.pin_uv_auth_param = Some(PinUvAuthParam::create_empty()); + req +} + +#[cfg(test)] +pub mod test { + use super::{MakeCredentials, MakeCredentialsOptions, MakeCredentialsResult}; + use crate::crypto::{COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve}; + use crate::ctap2::attestation::test::create_attestation_obj; + use crate::ctap2::attestation::{ + AAGuid, AttestationCertificate, AttestationObject, AttestationStatement, + AttestationStatementFidoU2F, 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::{ + AuthenticatorAttachment, PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, + RelyingParty, + }; + use crate::transport::device_selector::Device; + use crate::transport::hid::HIDDevice; + use crate::transport::{FidoDevice, FidoProtocol}; + use base64::Engine; + + #[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"))), + } + .hash() + .expect("failed to serialize client data"), + RelyingParty { + id: String::from("example.com"), + name: Some(String::from("Acme")), + }, + Some(PublicKeyCredentialUserEntity { + id: base64::engine::general_purpose::URL_SAFE + .decode("MIIBkzCCATigAwIBAjCCAZMwggE4oAMCAQIwggGTMII=") + .unwrap(), + 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(), + ); + + let mut device = Device::new("commands/make_credentials").unwrap(); // not really used (all functions ignore it) + assert_eq!(device.get_protocol(), FidoProtocol::CTAP2); + let req_serialized = req + .wire_format() + .expect("Failed to serialize MakeCredentials request"); + assert_eq!(req_serialized, MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP2); + let make_cred_result = req + .handle_response_ctap2(&mut device, &MAKE_CREDENTIALS_SAMPLE_RESPONSE_CTAP2) + .expect("Failed to handle CTAP2 response"); + + let expected = MakeCredentialsResult { + att_obj: create_attestation_obj(), + attachment: AuthenticatorAttachment::Unknown, + extensions: Default::default(), + }; + + assert_eq!(make_cred_result, 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"))), + } + .hash() + .expect("failed to serialize client data"), + RelyingParty::from("example.com"), + Some(PublicKeyCredentialUserEntity { + id: base64::engine::general_purpose::URL_SAFE + .decode("MIIBkzCCATigAwIBAjCCAZMwggE4oAMCAQIwggGTMII=") + .unwrap(), + 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(), + ); + + let (req_serialized, _) = req + .ctap1_format() + .expect("Failed to serialize MakeCredentials request"); + assert_eq!( + req_serialized, MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP1, + "\nGot: {req_serialized:X?}\nExpected: {MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP1:X?}" + ); + let mut device = Device::new("commands/make_credentials").unwrap(); // not really used + let make_cred_result = req + .handle_response_ctap1( + &mut device, + Ok(()), + &MAKE_CREDENTIALS_SAMPLE_RESPONSE_CTAP1, + &(), + ) + .expect("Failed to handle CTAP1 response"); + + let att_obj = 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: Curve::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_stmt: AttestationStatement::FidoU2F(AttestationStatementFidoU2F { + sig: Signature(vec![ + 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, + ])], + }), + }; + + let expected = MakeCredentialsResult { + att_obj, + attachment: AuthenticatorAttachment::Unknown, + extensions: Default::default(), + }; + + assert_eq!(make_cred_result, expected); + } + + // This includes a CTAP2 encoded attestation object that is identical to + // the WebAuthn encoded attestation object in `ctap2::attestation::test::SAMPLE_ATTESTATION`. + // Both values decode to `ctap2::attestation::test::create_attestation_obj`. + #[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; 210] = [ + // 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 + 0xa3, // map(3) + 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) + 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 + 0x3, // 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..12990122d9 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/mod.rs @@ -0,0 +1,477 @@ +use crate::crypto::{CryptoError, PinUvAuthParam, PinUvAuthToken}; +use crate::ctap2::commands::client_pin::{GetPinRetries, GetUvRetries, PinError}; +use crate::ctap2::commands::get_info::AuthenticatorInfo; +use crate::ctap2::server::UserVerificationRequirement; +use crate::errors::AuthenticatorError; +use crate::transport::errors::{ApduErrorStatus, HIDError}; +use crate::transport::{FidoDevice, VirtualFidoDevice}; +use serde_cbor::{error::Error as CborError, Value}; +use serde_json as json; +use std::error::Error as StdErrorT; +use std::fmt; + +pub mod authenticator_config; +pub mod bio_enrollment; +pub mod client_pin; +pub mod credential_management; +pub mod get_assertion; +pub mod get_info; +pub mod get_next_assertion; +pub mod get_version; +pub mod make_credentials; +pub mod reset; +pub mod selection; + +/// 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 { + Retry, + Error(T), +} + +impl Retryable { + pub fn is_retry(&self) -> bool { + matches!(*self, Retryable::Retry) + } + + pub fn is_error(&self) -> bool { + !self.is_retry() + } +} + +impl From for Retryable { + fn from(e: T) -> Self { + Retryable::Error(e) + } +} + +pub trait RequestCtap1: fmt::Debug { + type Output: CtapResponse; + // E.g.: For GetAssertion, which key-handle is currently being tested + type AdditionalInfo; + + /// Serializes a request into FIDO v1.x / CTAP1 / U2F format. + /// + /// See [`crate::u2ftypes::CTAP1RequestAPDU::serialize()`] + fn ctap1_format(&self) -> Result<(Vec, Self::AdditionalInfo), HIDError>; + + /// Deserializes a response from FIDO v1.x / CTAP1 / U2Fv2 format. + fn handle_response_ctap1( + &self, + dev: &mut Dev, + status: Result<(), ApduErrorStatus>, + input: &[u8], + add_info: &Self::AdditionalInfo, + ) -> Result>; + + fn send_to_virtual_device( + &self, + dev: &mut Dev, + ) -> Result; +} + +pub trait RequestCtap2: fmt::Debug { + type Output: CtapResponse; + + fn command(&self) -> Command; + + fn wire_format(&self) -> Result, HIDError>; + + fn handle_response_ctap2( + &self, + dev: &mut Dev, + input: &[u8], + ) -> Result; + + fn send_to_virtual_device( + &self, + dev: &mut Dev, + ) -> Result; +} + +// Sadly, needs to be 'static to enable us in tests to collect them in a Vec +// but all of them are 'static, so this is currently no problem. +pub trait CtapResponse: std::fmt::Debug + 'static {} + +#[derive(Debug, Clone)] +pub enum PinUvAuthResult { + /// Request is CTAP1 and does not need PinUvAuth + RequestIsCtap1, + /// Device is not capable of CTAP2 + DeviceIsCtap1, + /// Device does not support UV or PINs + NoAuthTypeSupported, + /// Request doesn't want user verification (uv = "discouraged") + NoAuthRequired, + /// Device is CTAP2.0 and has internal UV capability + UsingInternalUv, + /// Successfully established PinUvAuthToken via GetPinToken (CTAP2.0) + SuccessGetPinToken(PinUvAuthToken), + /// Successfully established PinUvAuthToken via UV (CTAP2.1) + SuccessGetPinUvAuthTokenUsingUvWithPermissions(PinUvAuthToken), + /// Successfully established PinUvAuthToken via Pin (CTAP2.1) + SuccessGetPinUvAuthTokenUsingPinWithPermissions(PinUvAuthToken), +} + +impl PinUvAuthResult { + pub(crate) fn get_pin_uv_auth_token(&self) -> Option { + match self { + PinUvAuthResult::RequestIsCtap1 + | PinUvAuthResult::DeviceIsCtap1 + | PinUvAuthResult::NoAuthTypeSupported + | PinUvAuthResult::NoAuthRequired + | PinUvAuthResult::UsingInternalUv => None, + PinUvAuthResult::SuccessGetPinToken(token) => Some(token.clone()), + PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions(token) => { + Some(token.clone()) + } + PinUvAuthResult::SuccessGetPinUvAuthTokenUsingPinWithPermissions(token) => { + Some(token.clone()) + } + } + } +} + +/// Helper-trait to determine pin_uv_auth_param from PIN or UV. +pub(crate) trait PinUvAuthCommand: RequestCtap2 { + fn set_pin_uv_auth_param( + &mut self, + pin_uv_auth_token: Option, + ) -> Result<(), AuthenticatorError>; + fn get_pin_uv_auth_param(&self) -> Option<&PinUvAuthParam>; + fn set_uv_option(&mut self, uv: Option); + fn get_rp_id(&self) -> Option<&String>; + fn can_skip_user_verification( + &mut self, + info: &AuthenticatorInfo, + uv_req: UserVerificationRequirement, + ) -> bool; +} + +pub(crate) fn repackage_pin_errors( + dev: &mut D, + error: HIDError, +) -> AuthenticatorError { + match error { + HIDError::Command(CommandError::StatusCode(StatusCode::PinInvalid, _)) => { + // If the given PIN was wrong, determine no. of left retries + let cmd = GetPinRetries::new(); + // Treat any error as if the device returned a valid response without a pinRetries + // field. + let resp = dev.send_cbor(&cmd).unwrap_or_default(); + AuthenticatorError::PinError(PinError::InvalidPin(resp.pin_retries)) + } + HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthBlocked, _)) => { + AuthenticatorError::PinError(PinError::PinAuthBlocked) + } + HIDError::Command(CommandError::StatusCode(StatusCode::PinBlocked, _)) => { + AuthenticatorError::PinError(PinError::PinBlocked) + } + HIDError::Command(CommandError::StatusCode(StatusCode::PinRequired, _)) => { + AuthenticatorError::PinError(PinError::PinRequired) + } + HIDError::Command(CommandError::StatusCode(StatusCode::PinNotSet, _)) => { + AuthenticatorError::PinError(PinError::PinNotSet) + } + HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthInvalid, _)) => { + AuthenticatorError::PinError(PinError::PinAuthInvalid) + } + HIDError::Command(CommandError::StatusCode(StatusCode::UvInvalid, _)) => { + // If the internal UV failed, determine no. of left retries + let cmd = GetUvRetries::new(); + // Treat any error as if the device returned a valid response without a uvRetries + // field. + let resp = dev.send_cbor(&cmd).unwrap_or_default(); + AuthenticatorError::PinError(PinError::InvalidUv(resp.uv_retries)) + } + HIDError::Command(CommandError::StatusCode(StatusCode::UvBlocked, _)) => { + AuthenticatorError::PinError(PinError::UvBlocked) + } + // TODO(MS): Add "PinPolicyViolated" + err => AuthenticatorError::HIDError(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, PartialEq, Clone)] +pub enum Command { + MakeCredentials = 0x01, + GetAssertion = 0x02, + GetInfo = 0x04, + ClientPin = 0x06, + Reset = 0x07, + GetNextAssertion = 0x08, + BioEnrollment = 0x09, + CredentialManagement = 0x0A, + Selection = 0x0B, + AuthenticatorConfig = 0x0D, + BioEnrollmentPreview = 0x40, + CredentialManagementPreview = 0x41, +} + +#[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, + /// built-in user verification is disabled. + UvBlocked, + /// A checksum did not match. + IntegrityFailure, + /// The requested subcommand is either invalid or not implemented. + InvalidSubcommand, + /// built-in user verification unsuccessful. The platform SHOULD retry. + UvInvalid, + /// The permissions parameter contains an unauthorized permission. + UnauthorizedPermission, + /// Other unspecified error. + Other, + + /// 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 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, + 0x3C => StatusCode::UvBlocked, + 0x3D => StatusCode::IntegrityFailure, + 0x3E => StatusCode::InvalidSubcommand, + 0x3F => StatusCode::UvInvalid, + 0x40 => StatusCode::UnauthorizedPermission, + 0x7F => StatusCode::Other, + othr => StatusCode::Unknown(othr), + } + } +} + +#[cfg(test)] +impl From for u8 { + fn from(v: StatusCode) -> u8 { + match v { + 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::UvBlocked => 0x3C, + StatusCode::IntegrityFailure => 0x3D, + StatusCode::InvalidSubcommand => 0x3E, + StatusCode::UvInvalid => 0x3F, + StatusCode::UnauthorizedPermission => 0x40, + StatusCode::Other => 0x7F, + + StatusCode::Unknown(othr) => othr, + } + } +} + +#[derive(Debug)] +pub enum CommandError { + InputTooSmall, + MissingRequiredField(&'static str), + Deserializing(CborError), + Serializing(CborError), + StatusCode(StatusCode, Option), + Json(json::Error), + 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 {} 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..a1006800b5 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/reset.rs @@ -0,0 +1,123 @@ +use super::{Command, CommandError, RequestCtap2, StatusCode}; +use crate::transport::errors::HIDError; +use crate::transport::{FidoDevice, VirtualFidoDevice}; +use serde_cbor::{de::from_slice, Value}; + +#[derive(Debug, Default)] +pub struct Reset {} + +impl RequestCtap2 for Reset { + type Output = (); + + fn command(&self) -> Command { + Command::Reset + } + + fn wire_format(&self) -> Result, HIDError> { + Ok(Vec::new()) + } + + fn handle_response_ctap2( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result { + 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()) + } + } + + fn send_to_virtual_device( + &self, + dev: &mut Dev, + ) -> Result { + dev.reset(self) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::consts::HIDCmd; + use crate::transport::device_selector::Device; + use crate::transport::{hid::HIDDevice, FidoDevice, FidoDeviceIO, FidoProtocol}; + 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(); + assert_eq!(device.get_protocol(), FidoProtocol::CTAP2); + // 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() + issue_command_and_get_response(0, &[]).expect("Unexpected error"); + + // Denied by the user + let response = issue_command_and_get_response(0x27, &[]).expect_err("Not an error!"); + assert!(matches!( + response, + HIDError::Command(CommandError::StatusCode(StatusCode::OperationDenied, None)) + )); + + // Timeout + let response = issue_command_and_get_response(0x2F, &[]).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..4e9fc5213e --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/commands/selection.rs @@ -0,0 +1,123 @@ +use super::{Command, CommandError, RequestCtap2, StatusCode}; +use crate::transport::errors::HIDError; +use crate::transport::{FidoDevice, VirtualFidoDevice}; +use serde_cbor::{de::from_slice, Value}; + +#[derive(Debug, Default)] +pub struct Selection {} + +impl RequestCtap2 for Selection { + type Output = (); + + fn command(&self) -> Command { + Command::Selection + } + + fn wire_format(&self) -> Result, HIDError> { + Ok(Vec::new()) + } + + fn handle_response_ctap2( + &self, + _dev: &mut Dev, + input: &[u8], + ) -> Result { + 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()) + } + } + + fn send_to_virtual_device( + &self, + dev: &mut Dev, + ) -> Result { + dev.selection(self) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::consts::HIDCmd; + use crate::transport::device_selector::Device; + use crate::transport::{hid::HIDDevice, FidoDevice, FidoDeviceIO, FidoProtocol}; + 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(); + assert_eq!(device.get_protocol(), FidoProtocol::CTAP2); + // 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() + issue_command_and_get_response(0, &[]).expect("Unexpected error"); + + // Denied by the user + let response = issue_command_and_get_response(0x27, &[]).expect_err("Not an error!"); + assert!(matches!( + response, + HIDError::Command(CommandError::StatusCode(StatusCode::OperationDenied, None)) + )); + + // Timeout + let response = issue_command_and_get_response(0x2F, &[]).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..bc45ceb9eb --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/mod.rs @@ -0,0 +1,1518 @@ +pub mod attestation; +pub mod client_data; +#[allow(dead_code)] // TODO(MS): Remove me asap +pub mod commands; +pub mod preflight; +pub mod server; +pub(crate) mod utils; + +use crate::authenticatorservice::{RegisterArgs, SignArgs}; +use crate::crypto::COSEAlgorithm; +use crate::ctap2::client_data::ClientDataHash; +use crate::ctap2::commands::authenticator_config::{ + AuthConfigCommand, AuthConfigResult, AuthenticatorConfig, +}; +use crate::ctap2::commands::bio_enrollment::{ + BioEnrollment, BioEnrollmentCommand, BioEnrollmentResult, FingerprintSensorInfo, +}; +use crate::ctap2::commands::client_pin::{ + ChangeExistingPin, Pin, PinError, PinUvAuthTokenPermission, SetNewPin, +}; +use crate::ctap2::commands::credential_management::{ + CredManagementCommand, CredentialList, CredentialListEntry, CredentialManagement, + CredentialManagementResult, CredentialRpListEntry, +}; +use crate::ctap2::commands::get_assertion::{GetAssertion, GetAssertionOptions}; +use crate::ctap2::commands::make_credentials::{ + dummy_make_credentials_cmd, MakeCredentials, MakeCredentialsOptions, +}; +use crate::ctap2::commands::reset::Reset; +use crate::ctap2::commands::{ + repackage_pin_errors, CommandError, PinUvAuthCommand, PinUvAuthResult, RequestCtap2, StatusCode, +}; +use crate::ctap2::preflight::{ + do_credential_list_filtering_ctap1, do_credential_list_filtering_ctap2, + silently_discover_credentials, +}; +use crate::ctap2::server::{ + CredentialProtectionPolicy, RelyingParty, ResidentKeyRequirement, UserVerificationRequirement, +}; +use crate::errors::{AuthenticatorError, UnsupportedOption}; +use crate::statecallback::StateCallback; +use crate::status_update::{send_status, BioEnrollmentCmd, CredManagementCmd, InteractiveUpdate}; +use crate::transport::device_selector::{Device, DeviceSelectorEvent}; +use crate::transport::{errors::HIDError, hid::HIDDevice, FidoDevice, FidoDeviceIO, FidoProtocol}; +use crate::{ManageResult, ResetResult, StatusPinUv, StatusUpdate}; +use std::sync::mpsc::{channel, RecvError, Sender}; +use std::thread; +use std::time::Duration; + +use self::commands::get_info::AuthenticatorVersion; + +macro_rules! unwrap_option { + ($item: expr, $callback: expr) => { + match $item { + Some(r) => r, + None => { + $callback.call(Err(AuthenticatorError::Platform)); + return false; + } + } + }; +} + +macro_rules! unwrap_result { + ($item: expr, $callback: expr) => { + match $item { + Ok(r) => r, + Err(e) => { + $callback.call(Err(e.into())); + return false; + } + } + }; +} + +macro_rules! handle_errors { + ($error: expr, $status: expr, $callback: expr, $pin_uv_auth_result: expr, $skip_uv: expr) => { + let mut _dummy_skip_puap = false; + let mut _dummy_cached_puat = false; + handle_errors!( + $error, + $status, + $callback, + $pin_uv_auth_result, + $skip_uv, + _dummy_skip_puap, + _dummy_cached_puat + ) + }; + ($error: expr, $status: expr, $callback: expr, $pin_uv_auth_result: expr, $skip_uv: expr, $skip_puap: expr) => { + let mut _dummy_cached_puat = false; + handle_errors!( + $error, + $status, + $callback, + $pin_uv_auth_result, + $skip_uv, + $skip_puap, + _dummy_cached_puat + ) + }; + ($error: expr, $status: expr, $callback: expr, $pin_uv_auth_result: expr, $skip_uv: expr, $skip_puap: expr, $cached_puat: expr) => { + match $error { + HIDError::Command(CommandError::StatusCode(StatusCode::ChannelBusy, _)) => { + // Channel busy. Client SHOULD retry the request after a short delay. + thread::sleep(Duration::from_millis(100)); + continue; + } + HIDError::Command(CommandError::StatusCode(StatusCode::OperationDenied, _)) + | HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthInvalid, _)) + if matches!($pin_uv_auth_result, PinUvAuthResult::UsingInternalUv) => + { + // This should only happen for CTAP2.0 tokens that use internal UV and failed + // (e.g. wrong fingerprint used), while doing GetAssertion or MakeCredentials. + send_status( + &$status, + StatusUpdate::PinUvError(StatusPinUv::InvalidUv(None)), + ); + $skip_puap = false; + continue; + } + HIDError::Command(CommandError::StatusCode(StatusCode::PinRequired, _)) + if matches!($pin_uv_auth_result, PinUvAuthResult::UsingInternalUv) => + { + // This should only happen for CTAP2.0 tokens that use internal UV and failed + // repeatedly, so that we have to fall back to PINs + $skip_uv = true; + $skip_puap = false; + continue; + } + HIDError::Command(CommandError::StatusCode(StatusCode::UvBlocked, _)) + if matches!( + $pin_uv_auth_result, + PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions(..) + ) => + { + // This should only happen for CTAP2.1 tokens that use internal UV and failed + // repeatedly, so that we have to fall back to PINs + $skip_uv = true; + $skip_puap = false; + continue; + } + HIDError::Command(CommandError::StatusCode(StatusCode::CredentialExcluded, _)) => { + $callback.call(Err(AuthenticatorError::CredentialExcluded)); + break; + } + HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthInvalid, _)) + if $cached_puat => + { + // We used the cached PUAT, but it was invalid. So we just try again + // without the cached one and potentially trigger a new PIN/UV entry from + // the user. This happens e.g. if the PUAT expires, or we get an all-zeros + // PUAT from outside for whatever reason, etc. + $cached_puat = false; + $skip_puap = false; + continue; + } + e => { + warn!("error happened: {e}"); + $callback.call(Err(AuthenticatorError::HIDError(e))); + break; + } + } + }; +} + +fn ask_user_for_pin( + was_invalid: bool, + retries: Option, + status: &Sender, +) -> Result { + info!("PIN Error that requires user interaction detected. Sending it back and waiting for a reply"); + let (tx, rx) = channel(); + if was_invalid { + send_status( + status, + crate::StatusUpdate::PinUvError(StatusPinUv::InvalidPin(tx, retries)), + ); + } else { + send_status( + status, + crate::StatusUpdate::PinUvError(StatusPinUv::PinRequired(tx)), + ); + } + match rx.recv() { + Ok(pin) => Ok(pin), + Err(RecvError) => { + // recv() can only fail, if the other side is dropping the Sender. + info!("Callback dropped the channel. Aborting."); + Err(AuthenticatorError::CancelledByUser) + } + } +} + +/// Try to fetch PinUvAuthToken from the device and derive from it PinUvAuthParam. +/// Prefer UV, fallback to PIN. +/// Prefer newer pinUvAuth-methods, if supported by the device. +fn get_pin_uv_auth_param( + cmd: &mut T, + dev: &mut Dev, + permission: PinUvAuthTokenPermission, + skip_uv: bool, + uv_req: UserVerificationRequirement, + alive: &dyn Fn() -> bool, + pin: &Option, +) -> Result { + // CTAP 2.1 is very specific that the request should either include pinUvAuthParam + // OR uv=true, but not both at the same time. We now have to decide which (if either) + // to send. We may omit both values. Will never send an explicit uv=false, because + // a) this is the default, and + // b) some CTAP 2.0 authenticators return UnsupportedOption when uv=false. + + // We ensure both pinUvAuthParam and uv are not set to start. + cmd.set_pin_uv_auth_param(None)?; + cmd.set_uv_option(None); + + // Skip user verification if we're using CTAP1 or if the device does not support CTAP2. + let info = match (dev.get_protocol(), dev.get_authenticator_info()) { + (FidoProtocol::CTAP2, Some(info)) => info, + _ => return Ok(PinUvAuthResult::DeviceIsCtap1), + }; + + // Only use UV, if the device supports it and we don't skip it + // which happens as a fallback, if UV-usage failed too many times + // Note: In theory, we could also repeatedly query GetInfo here and check + // if uv is set to Some(true), as tokens should set it to Some(false) + // if UV is blocked (too many failed attempts). But the CTAP2.0-spec is + // vague and I don't trust all tokens to implement it that way. So we + // keep track of it ourselves, using `skip_uv`. + let supports_uv = info.options.user_verification == Some(true); + let supports_pin = info.options.client_pin.is_some(); + let pin_configured = info.options.client_pin == Some(true); + + // Check if the combination of device-protection and request-options + // are allowing for 'discouraged', meaning no auth required. + if cmd.can_skip_user_verification(info, uv_req) { + return Ok(PinUvAuthResult::NoAuthRequired); + } + + // Device does not support any (remaining) auth-method + if (skip_uv || !supports_uv) && !supports_pin { + if supports_uv && uv_req == UserVerificationRequirement::Required { + // We should always set the uv option in the Required case, but the CTAP 2.1 spec + // says 'Platforms MUST NOT include the "uv" option key if the authenticator does + // not support built-in user verification.' This is to work around some CTAP 2.0 + // authenticators which incorrectly error out with CTAP2_ERR_UNSUPPORTED_OPTION + // when the "uv" option is set. The RP that requested UV will (hopefully) reject our + // response in the !supports_uv case. + cmd.set_uv_option(Some(true)); + } + return Ok(PinUvAuthResult::NoAuthTypeSupported); + } + + // Device supports PINs, but a PIN is not configured. Signal that we + // can complete the operation if the user sets a PIN first. + if (skip_uv || !supports_uv) && !pin_configured { + return Err(AuthenticatorError::PinError(PinError::PinNotSet)); + } + + if info.options.pin_uv_auth_token == Some(true) { + if !skip_uv && supports_uv { + // CTAP 2.1 - UV + let pin_auth_token = dev + .get_pin_uv_auth_token_using_uv_with_permissions(permission, cmd.get_rp_id(), alive) + .map_err(|e| repackage_pin_errors(dev, e))?; + cmd.set_pin_uv_auth_param(Some(pin_auth_token.clone()))?; + Ok(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions(pin_auth_token)) + } else { + // CTAP 2.1 - PIN + // We did not take the `!skip_uv && supports_uv` branch, so we have + // `(skip_uv || !supports_uv)`. Moreover we did not exit early in the + // `(skip_uv || !supports_uv) && !pin_configured` case. So we have + // `pin_configured`. + let pin_auth_token = dev + .get_pin_uv_auth_token_using_pin_with_permissions( + pin, + permission, + cmd.get_rp_id(), + alive, + ) + .map_err(|e| repackage_pin_errors(dev, e))?; + cmd.set_pin_uv_auth_param(Some(pin_auth_token.clone()))?; + Ok(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingPinWithPermissions(pin_auth_token)) + } + } else { + // CTAP 2.0 fallback + if !skip_uv && supports_uv && pin.is_none() { + // If the device supports internal user-verification (e.g. fingerprints), + // skip PIN-stuff + + // We may need the shared secret for HMAC-extension, so we + // have to establish one + if info.supports_hmac_secret() { + let _shared_secret = dev.establish_shared_secret(alive)?; + } + // CTAP 2.1, Section 6.1.1, Step 1.1.2.1.2. + cmd.set_uv_option(Some(true)); + return Ok(PinUvAuthResult::UsingInternalUv); + } + + let pin_auth_token = dev + .get_pin_token(pin, alive) + .map_err(|e| repackage_pin_errors(dev, e))?; + cmd.set_pin_uv_auth_param(Some(pin_auth_token.clone()))?; + Ok(PinUvAuthResult::SuccessGetPinToken(pin_auth_token)) + } +} + +/// PUAP, as per spec: PinUvAuthParam +/// Determines, if we need to establish a PinUvAuthParam, based on the +/// capabilities of the device and the incoming request. +/// If it is needed, tries to establish one and save it inside the Request. +/// Returns Ok() if we can proceed with sending the actual Request to +/// the device, Err() otherwise. +/// Handles asking the user for a PIN, if needed and sending StatusUpdates +/// regarding PIN and UV usage. +#[allow(clippy::too_many_arguments)] +fn determine_puap_if_needed( + cmd: &mut T, + dev: &mut Dev, + mut skip_uv: bool, + permission: PinUvAuthTokenPermission, + uv_req: UserVerificationRequirement, + status: &Sender, + alive: &dyn Fn() -> bool, + pin: &mut Option, +) -> Result { + while alive() { + debug!("-----------------------------------------------------------------"); + debug!("Getting pinUvAuthParam"); + match get_pin_uv_auth_param(cmd, dev, permission, skip_uv, uv_req, alive, pin) { + Ok(r) => { + return Ok(r); + } + + Err(AuthenticatorError::PinError(PinError::PinRequired)) => { + let new_pin = ask_user_for_pin(false, None, status)?; + *pin = Some(new_pin); + skip_uv = true; + continue; + } + Err(AuthenticatorError::PinError(PinError::InvalidPin(retries))) => { + let new_pin = ask_user_for_pin(true, retries, status)?; + *pin = Some(new_pin); + continue; + } + Err(AuthenticatorError::PinError(PinError::InvalidUv(retries))) => { + if retries == Some(0) { + skip_uv = true; + } + send_status( + status, + StatusUpdate::PinUvError(StatusPinUv::InvalidUv(retries)), + ) + } + Err(e @ AuthenticatorError::PinError(PinError::PinAuthBlocked)) => { + send_status( + status, + StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked), + ); + error!("Error when determining pinAuth: {:?}", e); + return Err(e); + } + Err(e @ AuthenticatorError::PinError(PinError::PinBlocked)) => { + send_status(status, StatusUpdate::PinUvError(StatusPinUv::PinBlocked)); + error!("Error when determining pinAuth: {:?}", e); + return Err(e); + } + Err(e @ AuthenticatorError::PinError(PinError::PinNotSet)) => { + send_status(status, StatusUpdate::PinUvError(StatusPinUv::PinNotSet)); + error!("Error when determining pinAuth: {:?}", e); + return Err(e); + } + Err(AuthenticatorError::PinError(PinError::UvBlocked)) => { + skip_uv = true; + send_status(status, StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) + } + // Used for CTAP2.0 UV (fingerprints) + Err(AuthenticatorError::PinError(PinError::PinAuthInvalid)) => { + skip_uv = true; + send_status( + status, + StatusUpdate::PinUvError(StatusPinUv::InvalidUv(None)), + ) + } + Err(e) => { + error!("Error when determining pinAuth: {:?}", e); + return Err(e); + } + } + } + Err(AuthenticatorError::CancelledByUser) +} + +pub fn register( + dev: &mut Dev, + args: RegisterArgs, + status: Sender, + callback: StateCallback>, + alive: &dyn Fn() -> bool, +) -> bool { + let mut options = MakeCredentialsOptions::default(); + + if dev.get_protocol() == FidoProtocol::CTAP2 { + let info = match dev.get_authenticator_info() { + Some(info) => info, + None => { + callback.call(Err(HIDError::DeviceNotInitialized.into())); + return false; + } + }; + + // Set options based on the arguments and the device info. + // The user verification option will be set in `determine_puap_if_needed`. + options.resident_key = match args.resident_key_req { + ResidentKeyRequirement::Required => Some(true), + ResidentKeyRequirement::Preferred => { + // Use a resident key if the authenticator supports it + Some(info.options.resident_key) + } + ResidentKeyRequirement::Discouraged => Some(false), + } + } else { + // Check that the request can be processed by a CTAP1 device. + // See CTAP 2.1 Section 10.2. Some additional checks are performed in + // MakeCredentials::RequestCtap1 + if args.resident_key_req == ResidentKeyRequirement::Required { + callback.call(Err(AuthenticatorError::UnsupportedOption( + UnsupportedOption::ResidentKey, + ))); + return false; + } + if args.user_verification_req == UserVerificationRequirement::Required { + callback.call(Err(AuthenticatorError::UnsupportedOption( + UnsupportedOption::UserVerification, + ))); + return false; + } + if !args + .pub_cred_params + .iter() + .any(|x| x.alg == COSEAlgorithm::ES256) + { + callback.call(Err(AuthenticatorError::UnsupportedOption( + UnsupportedOption::PubCredParams, + ))); + return false; + } + } + + // Client extension processing for credProtect: + // "When enforceCredentialProtectionPolicy is true, and credentialProtectionPolicy's value is + // [not "Optional"], the platform SHOULD NOT create the credential in a way that does not + // implement the requested protection policy. (For example, by creating it on an authenticator + // that does not support this extension.)" + let dev_supports_cred_protect = dev + .get_authenticator_info() + .map_or(false, |info| info.supports_cred_protect()); + if args.extensions.enforce_credential_protection_policy == Some(true) + && args.extensions.credential_protection_policy + != Some(CredentialProtectionPolicy::UserVerificationOptional) + && !dev_supports_cred_protect + { + callback.call(Err(AuthenticatorError::UnsupportedOption( + UnsupportedOption::CredProtect, + ))); + return false; + } + + let mut makecred = MakeCredentials::new( + ClientDataHash(args.client_data_hash), + args.relying_party, + Some(args.user), + args.pub_cred_params, + args.exclude_list, + options, + args.extensions.into(), + ); + + let mut skip_uv = false; + let mut pin = args.pin; + while alive() { + // Requesting both because pre-flighting (credential list filtering) + // can potentially send GetAssertion-commands + let permissions = + PinUvAuthTokenPermission::MakeCredential | PinUvAuthTokenPermission::GetAssertion; + + let pin_uv_auth_result = unwrap_result!( + determine_puap_if_needed( + &mut makecred, + dev, + skip_uv, + permissions, + args.user_verification_req, + &status, + alive, + &mut pin, + ), + callback + ); + // Do "pre-flight": Filter the exclude-list + if dev.get_protocol() == FidoProtocol::CTAP2 { + makecred.exclude_list = unwrap_result!( + do_credential_list_filtering_ctap2( + dev, + &makecred.exclude_list, + &makecred.rp, + pin_uv_auth_result.get_pin_uv_auth_token(), + ), + callback + ); + } else { + let key_handle = do_credential_list_filtering_ctap1( + dev, + &makecred.exclude_list, + &makecred.rp, + &makecred.client_data_hash, + ); + // That handle was already registered with the token + if key_handle.is_some() { + // 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. + send_status(&status, crate::StatusUpdate::PresenceRequired); + let msg = dummy_make_credentials_cmd(); + let _ = dev.send_msg_cancellable(&msg, alive); // Ignore answer, return "CredentialExcluded" + callback.call(Err(AuthenticatorError::CredentialExcluded)); + return false; + } + } + + debug!("------------------------------------------------------------------"); + debug!("{makecred:?} using {pin_uv_auth_result:?}"); + debug!("------------------------------------------------------------------"); + send_status(&status, crate::StatusUpdate::PresenceRequired); + let resp = dev.send_msg_cancellable(&makecred, alive); + match resp { + Ok(result) => { + callback.call(Ok(result)); + return true; + } + Err(e) => { + handle_errors!(e, status, callback, pin_uv_auth_result, skip_uv); + } + } + } + false +} + +pub fn sign( + dev: &mut Dev, + args: SignArgs, + status: Sender, + callback: StateCallback>, + alive: &dyn Fn() -> bool, +) -> bool { + if dev.get_protocol() == FidoProtocol::CTAP1 { + // Check that the request can be processed by a CTAP1 device. + // See CTAP 2.1 Section 10.3. Some additional checks are performed in + // GetAssertion::RequestCtap1 + if args.user_verification_req == UserVerificationRequirement::Required { + callback.call(Err(AuthenticatorError::UnsupportedOption( + UnsupportedOption::UserVerification, + ))); + return false; + } + if args.allow_list.is_empty() { + callback.call(Err(AuthenticatorError::UnsupportedOption( + UnsupportedOption::EmptyAllowList, + ))); + return false; + } + } + + let mut allow_list = args.allow_list; + let mut rp_id = RelyingParty::from(args.relying_party_id); + let client_data_hash = ClientDataHash(args.client_data_hash); + if let Some(ref app_id) = args.extensions.app_id { + if !allow_list.is_empty() { + // Try to silently discover U2F credentials that require the FIDO App ID extension. If + // any are found, we should use the alternate RP ID instead of the provided RP ID. + let alt_rp_id = RelyingParty::from(app_id); + let silent_creds = + silently_discover_credentials(dev, &allow_list, &alt_rp_id, &client_data_hash); + if !silent_creds.is_empty() { + allow_list = silent_creds; + rp_id = alt_rp_id; + } + } + } + + let mut get_assertion = GetAssertion::new( + client_data_hash, + rp_id, + allow_list, + GetAssertionOptions { + user_presence: Some(args.user_presence_req), + user_verification: None, + }, + args.extensions.into(), + ); + + let mut skip_uv = false; + let mut pin = args.pin; + while alive() { + let pin_uv_auth_result = unwrap_result!( + determine_puap_if_needed( + &mut get_assertion, + dev, + skip_uv, + PinUvAuthTokenPermission::GetAssertion, + args.user_verification_req, + &status, + alive, + &mut pin, + ), + callback + ); + // Third, use the shared secret in the extensions, if requested + if let Some(extension) = get_assertion.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 false; + } + } + } + } + + // Do "pre-flight": Filter the allow-list + let original_allow_list_was_empty = get_assertion.allow_list.is_empty(); + if dev.get_protocol() == FidoProtocol::CTAP2 { + get_assertion.allow_list = unwrap_result!( + do_credential_list_filtering_ctap2( + dev, + &get_assertion.allow_list, + &get_assertion.rp, + pin_uv_auth_result.get_pin_uv_auth_token(), + ), + callback + ); + } else { + let key_handle = do_credential_list_filtering_ctap1( + dev, + &get_assertion.allow_list, + &get_assertion.rp, + &get_assertion.client_data_hash, + ); + match key_handle { + Some(key_handle) => { + get_assertion.allow_list = vec![key_handle]; + } + None => { + get_assertion.allow_list.clear(); + } + } + } + + // If the incoming list was not empty, but the filtered list is, we have to error out + if !original_allow_list_was_empty && get_assertion.allow_list.is_empty() { + // We have to collect a user interaction + send_status(&status, crate::StatusUpdate::PresenceRequired); + let msg = dummy_make_credentials_cmd(); + let _ = dev.send_msg_cancellable(&msg, alive); // Ignore answer, return "NoCredentials" + callback.call(Err(HIDError::Command(CommandError::StatusCode( + StatusCode::NoCredentials, + None, + )) + .into())); + return false; + } + + debug!("------------------------------------------------------------------"); + debug!("{get_assertion:?} using {pin_uv_auth_result:?}"); + debug!("------------------------------------------------------------------"); + send_status(&status, crate::StatusUpdate::PresenceRequired); + let mut results = match dev.send_msg_cancellable(&get_assertion, alive) { + Ok(results) => results, + Err(e) => { + handle_errors!(e, status, callback, pin_uv_auth_result, skip_uv); + } + }; + if results.len() == 1 { + callback.call(Ok(results.swap_remove(0))); + return true; + } + let (tx, rx) = channel(); + let user_entities = results + .iter() + .filter_map(|x| x.assertion.user.clone()) + .collect(); + send_status( + &status, + crate::StatusUpdate::SelectResultNotice(tx, user_entities), + ); + match rx.recv() { + Ok(Some(index)) if index < results.len() => { + callback.call(Ok(results.swap_remove(index))); + return true; + } + _ => { + callback.call(Err(AuthenticatorError::CancelledByUser)); + return true; + } + } + } + false +} + +pub(crate) fn reset_helper>( + dev: &mut Device, + selector: Sender, + status: Sender, + callback: StateCallback>, + keep_alive: &dyn Fn() -> bool, +) { + let reset = Reset {}; + info!("Device {:?} continues with the reset process", dev.id()); + + debug!("------------------------------------------------------------------"); + debug!("{:?}", reset); + debug!("------------------------------------------------------------------"); + send_status(&status, crate::StatusUpdate::PresenceRequired); + let resp = dev.send_cbor_cancellable(&reset, keep_alive); + if resp.is_ok() { + // 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(T::from(()))), + 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))); + } + } +} + +pub(crate) fn set_or_change_pin_helper>( + dev: &mut Device, + mut current_pin: Option, + new_pin: Pin, + status: Sender, + callback: StateCallback>, + alive: &dyn Fn() -> bool, +) { + let mut shared_secret = match dev.establish_shared_secret(alive) { + Ok(s) => s, + Err(e) => { + callback.call(Err(AuthenticatorError::HIDError(e))); + return; + } + }; + + let authinfo = match dev.get_authenticator_info() { + Some(i) => i.clone(), + None => { + callback.call(Err(HIDError::DeviceNotInitialized.into())); + return; + } + }; + + // If the device has a min PIN use that, otherwise default to 4 according to Spec + if new_pin.as_bytes().len() < authinfo.min_pin_length.unwrap_or(4) as usize { + callback.call(Err(AuthenticatorError::PinError(PinError::PinIsTooShort))); + return; + } + + // As per Spec: "Maximum PIN Length: UTF-8 representation MUST NOT exceed 63 bytes" + 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 Some(true) == authinfo.options.client_pin { + let mut res; + let mut was_invalid = false; + let mut retries = None; + loop { + // current_pin will only be Some() in the interactive mode (running `manage()`) + // In case that PIN is wrong, we want to avoid an endless-loop here with re-trying + // that wrong PIN all the time. So we `take()` it, and only test it once. + // If that PIN is wrong, we fall back to the "ask_user_for_pin"-method. + let curr_pin = match current_pin.take() { + None => match ask_user_for_pin(was_invalid, retries, &status) { + Ok(pin) => pin, + Err(e) => { + callback.call(Err(e)); + return; + } + }, + Some(pin) => pin, + }; + + res = ChangeExistingPin::new(&authinfo, &shared_secret, &curr_pin, &new_pin) + .map_err(HIDError::Command) + .and_then(|msg| dev.send_cbor_cancellable(&msg, alive)) + .map_err(|e| repackage_pin_errors(dev, e)); + + if let Err(AuthenticatorError::PinError(PinError::InvalidPin(r))) = res { + was_invalid = true; + retries = r; + // We need to re-establish the shared secret for the next round. + match dev.establish_shared_secret(alive) { + Ok(s) => { + shared_secret = s; + } + Err(e) => { + callback.call(Err(AuthenticatorError::HIDError(e))); + return; + } + }; + + continue; + } else { + break; + } + } + res + } else { + dev.send_cbor_cancellable(&SetNewPin::new(&shared_secret, &new_pin), alive) + .map_err(AuthenticatorError::HIDError) + }; + // the callback is expecting `Result<(), AuthenticatorError>`, but `ChangeExistingPin` + // and `SetNewPin` return the default `ClientPinResponse` on success. Just discard it. + callback.call(res.map(|_| T::from(()))); +} + +pub(crate) fn bio_enrollment( + dev: &mut Device, + puat_result: Option, + command: BioEnrollmentCmd, + status: Sender, + callback: StateCallback>, + alive: &dyn Fn() -> bool, +) -> bool { + let authinfo = match dev.get_authenticator_info() { + Some(i) => i, + None => { + callback.call(Err(HIDError::DeviceNotInitialized.into())); + return false; + } + }; + + if authinfo.options.bio_enroll.is_none() + && authinfo.options.user_verification_mgmt_preview.is_none() + { + callback.call(Err(AuthenticatorError::HIDError( + HIDError::UnsupportedCommand, + ))); + return false; + } + + let use_legacy_preview = authinfo.options.bio_enroll.is_none(); + + // We are not allowed to request the BE-permission using UV, so we have to skip UV + let mut skip_uv = authinfo.options.uv_bio_enroll != Some(true); + // Currently not used, but if we want, we can just set the value here. + let timeout = None; + + let mut bio_cmd = match &command { + BioEnrollmentCmd::StartNewEnrollment(_name) => BioEnrollment::new( + BioEnrollmentCommand::EnrollBegin(timeout), + use_legacy_preview, + ), + BioEnrollmentCmd::DeleteEnrollment(id) => BioEnrollment::new( + BioEnrollmentCommand::RemoveEnrollment(id.clone()), + use_legacy_preview, + ), + BioEnrollmentCmd::ChangeName(id, name) => BioEnrollment::new( + BioEnrollmentCommand::SetFriendlyName((id.clone(), name.clone())), + use_legacy_preview, + ), + BioEnrollmentCmd::GetEnrollments => BioEnrollment::new( + BioEnrollmentCommand::EnumerateEnrollments, + use_legacy_preview, + ), + BioEnrollmentCmd::GetFingerprintSensorInfo => BioEnrollment::new( + BioEnrollmentCommand::GetFingerprintSensorInfo, + use_legacy_preview, + ), + }; + + let mut skip_puap = false; + let mut cached_puat = false; // If we were provided with a cached puat from the outside + let mut pin_uv_auth_result = puat_result + .clone() + .unwrap_or(PinUvAuthResult::NoAuthRequired); + // See, if we have a cached PUAT with matching permissions. + match puat_result { + Some(PinUvAuthResult::SuccessGetPinToken(t)) + | Some(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions(t)) + | Some(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingPinWithPermissions(t)) + if !authinfo.versions.contains(&AuthenticatorVersion::FIDO_2_1) // Only 2.1 has a permission-system + || use_legacy_preview // Preview doesn't use permissions + || t.permissions + .contains(PinUvAuthTokenPermission::BioEnrollment) => + { + skip_puap = true; + cached_puat = true; + unwrap_result!(bio_cmd.set_pin_uv_auth_param(Some(t)), callback); + } + _ => {} + } + let mut pin = None; + while alive() { + if !skip_puap { + pin_uv_auth_result = unwrap_result!( + determine_puap_if_needed( + &mut bio_cmd, + dev, + skip_uv, + PinUvAuthTokenPermission::BioEnrollment, + UserVerificationRequirement::Preferred, + &status, + alive, + &mut pin, + ), + callback + ); + } + + debug!("------------------------------------------------------------------"); + debug!("{bio_cmd:?} using {pin_uv_auth_result:?}"); + debug!("------------------------------------------------------------------"); + + let resp = dev.send_cbor_cancellable(&bio_cmd, alive); + match resp { + Ok(result) => { + skip_puap = true; + match bio_cmd.subcommand { + BioEnrollmentCommand::EnrollBegin(..) + | BioEnrollmentCommand::EnrollCaptureNextSample(..) => { + let template_id = + if let BioEnrollmentCommand::EnrollCaptureNextSample((id, ..)) = + bio_cmd.subcommand + { + id + } else { + unwrap_option!(result.template_id, callback) + }; + let last_enroll_sample_status = + unwrap_option!(result.last_enroll_sample_status, callback); + let remaining_samples = unwrap_option!(result.remaining_samples, callback); + + send_status( + &status, + StatusUpdate::InteractiveManagement( + InteractiveUpdate::BioEnrollmentUpdate(( + BioEnrollmentResult::SampleStatus( + last_enroll_sample_status, + remaining_samples, + ), + Some(pin_uv_auth_result.clone()), + )), + ), + ); + + if remaining_samples == 0 { + if let BioEnrollmentCmd::StartNewEnrollment(Some(ref name)) = command { + bio_cmd.subcommand = BioEnrollmentCommand::SetFriendlyName(( + template_id.to_vec(), + name.clone(), + )); + // We have to regenerate PUAP here. PUAT hasn't changed, but the content + // of the command has changed, and that is part of the PUAP-calculation + unwrap_result!( + bio_cmd.set_pin_uv_auth_param( + pin_uv_auth_result.get_pin_uv_auth_token() + ), + callback + ); + continue; + } else { + let auth_info = + unwrap_option!(dev.refresh_authenticator_info(), callback); + send_status( + &status, + StatusUpdate::InteractiveManagement( + InteractiveUpdate::BioEnrollmentUpdate(( + BioEnrollmentResult::AddSuccess(auth_info.clone()), + Some(pin_uv_auth_result), + )), + ), + ); + return true; + } + } else { + bio_cmd.subcommand = BioEnrollmentCommand::EnrollCaptureNextSample(( + template_id, + timeout, + )); + // We have to regenerate PUAP here. PUAT hasn't changed, but the content + // of the command has changed, and that is part of the PUAP-calculation + unwrap_result!( + bio_cmd.set_pin_uv_auth_param( + pin_uv_auth_result.get_pin_uv_auth_token() + ), + callback + ); + continue; + } + } + BioEnrollmentCommand::EnumerateEnrollments => { + let list = result.template_infos.iter().map(|x| x.into()).collect(); + send_status( + &status, + StatusUpdate::InteractiveManagement( + InteractiveUpdate::BioEnrollmentUpdate(( + BioEnrollmentResult::EnrollmentList(list), + Some(pin_uv_auth_result), + )), + ), + ); + return true; + } + BioEnrollmentCommand::SetFriendlyName(_) => { + let res = match command { + BioEnrollmentCmd::StartNewEnrollment(..) => { + let auth_info = + unwrap_option!(dev.refresh_authenticator_info(), callback); + BioEnrollmentResult::AddSuccess(auth_info.clone()) + } + _ => BioEnrollmentResult::UpdateSuccess, + }; + send_status( + &status, + StatusUpdate::InteractiveManagement( + InteractiveUpdate::BioEnrollmentUpdate(( + res, + Some(pin_uv_auth_result), + )), + ), + ); + return true; + } + BioEnrollmentCommand::RemoveEnrollment(_) => { + let auth_info = unwrap_option!(dev.refresh_authenticator_info(), callback); + send_status( + &status, + StatusUpdate::InteractiveManagement( + InteractiveUpdate::BioEnrollmentUpdate(( + BioEnrollmentResult::DeleteSuccess(auth_info.clone()), + Some(pin_uv_auth_result), + )), + ), + ); + return true; + } + BioEnrollmentCommand::CancelCurrentEnrollment => { + callback.call(Ok(ManageResult::Success)); + return true; + } + BioEnrollmentCommand::GetFingerprintSensorInfo => { + let fingerprint_kind = unwrap_option!(result.fingerprint_kind, callback); + let max_capture_samples_required_for_enroll = unwrap_option!( + result.max_capture_samples_required_for_enroll, + callback + ); + // FIDO_2_1_PRE-devices do not report this field. So we leave it optional. + let max_template_friendly_name = result.max_template_friendly_name; + send_status( + &status, + StatusUpdate::InteractiveManagement( + InteractiveUpdate::BioEnrollmentUpdate(( + BioEnrollmentResult::FingerprintSensorInfo( + FingerprintSensorInfo { + fingerprint_kind, + max_capture_samples_required_for_enroll, + max_template_friendly_name, + }, + ), + Some(pin_uv_auth_result), + )), + ), + ); + return true; + } + }; + } + Err(e) => { + handle_errors!( + e, + status, + callback, + pin_uv_auth_result, + skip_uv, + skip_puap, + cached_puat + ); + } + } + } + false +} + +pub(crate) fn credential_management( + dev: &mut Device, + puat_result: Option, + command: CredManagementCmd, + status: Sender, + callback: StateCallback>, + alive: &dyn Fn() -> bool, +) -> bool { + let mut skip_uv = false; + let authinfo = match dev.get_authenticator_info() { + Some(i) => i.clone(), + None => { + callback.call(Err(HIDError::DeviceNotInitialized.into())); + return false; + } + }; + + if authinfo.options.cred_mgmt != Some(true) + && authinfo.options.credential_mgmt_preview != Some(true) + { + callback.call(Err(AuthenticatorError::HIDError( + HIDError::UnsupportedCommand, + ))); + return false; + } + + let use_legacy_preview = authinfo.options.cred_mgmt != Some(true); + + // FIDO_2_1_PRE-devices do not support UpdateUserInformation. + if use_legacy_preview + && !authinfo.versions.contains(&AuthenticatorVersion::FIDO_2_1) + && matches!(command, CredManagementCmd::UpdateUserInformation(..)) + { + callback.call(Err(AuthenticatorError::HIDError( + HIDError::UnsupportedCommand, + ))); + return false; + } + + // If puap is provided, we can skip puap-determination (i.e. PIN entry) + let mut cred_management = match command { + CredManagementCmd::GetCredentials => { + CredentialManagement::new(CredManagementCommand::GetCredsMetadata, use_legacy_preview) + } + CredManagementCmd::DeleteCredential(cred_id) => CredentialManagement::new( + CredManagementCommand::DeleteCredential(cred_id), + use_legacy_preview, + ), + CredManagementCmd::UpdateUserInformation(cred_id, user) => CredentialManagement::new( + CredManagementCommand::UpdateUserInformation((cred_id, user)), + use_legacy_preview, + ), + }; + let mut credential_result = CredentialList::new(); + let mut remaining_rps = 0; + let mut remaining_cred_ids = 0; + let mut current_rp = 0; + let mut skip_puap = false; + let mut cached_puat = false; // If we were provided with a cached puat from the outside + let mut pin_uv_auth_result = puat_result + .clone() + .unwrap_or(PinUvAuthResult::NoAuthRequired); + // See, if we have a cached PUAT with matching permissions. + match puat_result { + Some(PinUvAuthResult::SuccessGetPinToken(t)) + | Some(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions(t)) + | Some(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingPinWithPermissions(t)) + if !authinfo.versions.contains(&AuthenticatorVersion::FIDO_2_1) // Only 2.1 has a permission-system + || use_legacy_preview // Preview doesn't use permissions + || t.permissions + .contains(PinUvAuthTokenPermission::CredentialManagement) => + { + skip_puap = true; + cached_puat = true; + unwrap_result!(cred_management.set_pin_uv_auth_param(Some(t)), callback); + } + _ => {} + } + let mut pin = None; + while alive() { + if !skip_puap { + pin_uv_auth_result = unwrap_result!( + determine_puap_if_needed( + &mut cred_management, + dev, + skip_uv, + PinUvAuthTokenPermission::CredentialManagement, + UserVerificationRequirement::Preferred, + &status, + alive, + &mut pin, + ), + callback + ); + } + + debug!("------------------------------------------------------------------"); + debug!("{cred_management:?} using {pin_uv_auth_result:?}"); + debug!("------------------------------------------------------------------"); + + let resp = dev.send_cbor_cancellable(&cred_management, alive); + match resp { + Ok(result) => { + skip_puap = true; + match cred_management.subcommand { + CredManagementCommand::GetCredsMetadata => { + let existing_resident_credentials_count = + unwrap_option!(result.existing_resident_credentials_count, callback); + let max_possible_remaining_resident_credentials_count = unwrap_option!( + result.max_possible_remaining_resident_credentials_count, + callback + ); + credential_result.existing_resident_credentials_count = + existing_resident_credentials_count; + credential_result.max_possible_remaining_resident_credentials_count = + max_possible_remaining_resident_credentials_count; + if existing_resident_credentials_count > 0 { + cred_management.subcommand = CredManagementCommand::EnumerateRPsBegin; + // We have to regenerate PUAP here. PUAT hasn't changed, but the content + // of the command has changed, and that is part of the PUAP-calculation + unwrap_result!( + cred_management.set_pin_uv_auth_param( + pin_uv_auth_result.get_pin_uv_auth_token() + ), + callback + ); + continue; + } else { + // This token doesn't have any resident keys, but its not an error, + // so we send an empty list. + send_status( + &status, + StatusUpdate::InteractiveManagement( + InteractiveUpdate::CredentialManagementUpdate(( + CredentialManagementResult::CredentialList( + credential_result, + ), + Some(pin_uv_auth_result), + )), + ), + ); + return true; + } + } + CredManagementCommand::EnumerateRPsBegin + | CredManagementCommand::EnumerateRPsGetNextRP => { + if matches!( + cred_management.subcommand, + CredManagementCommand::EnumerateRPsBegin + ) { + let total_rps = unwrap_option!(result.total_rps, callback); + if total_rps == 0 { + // This token doesn't have any RPs, but its not an error, + // so we return an Ok with an empty list. + send_status( + &status, + StatusUpdate::InteractiveManagement( + InteractiveUpdate::CredentialManagementUpdate(( + CredentialManagementResult::CredentialList( + credential_result, + ), + Some(pin_uv_auth_result), + )), + ), + ); + return true; + } + remaining_rps = total_rps - 1; + } else { + remaining_rps -= 1; + } + + let rp = unwrap_option!(result.rp, callback); + let rp_id_hash = unwrap_option!(result.rp_id_hash, callback); + let rp_res = CredentialRpListEntry { + rp, + rp_id_hash, + credentials: vec![], + }; + credential_result.credential_list.push(rp_res); + if remaining_rps > 0 { + cred_management.subcommand = + CredManagementCommand::EnumerateRPsGetNextRP; + } else { + // We have queried all RPs, now start querying the corresponding credentials for each RP + cred_management.subcommand = + CredManagementCommand::EnumerateCredentialsBegin( + credential_result.credential_list[0].rp_id_hash.clone(), + ); + } + // We have to regenerate PUAP here. PUAT hasn't changed, but the content + // of the command has changed, and that is part of the PUAP-calculation + unwrap_result!( + cred_management + .set_pin_uv_auth_param(pin_uv_auth_result.get_pin_uv_auth_token()), + callback + ); + continue; + } + CredManagementCommand::EnumerateCredentialsBegin(..) + | CredManagementCommand::EnumerateCredentialsGetNextCredential => { + let user = unwrap_option!(result.user, callback); + let credential_id = unwrap_option!(result.credential_id, callback); + let public_key = unwrap_option!(result.public_key, callback); + let cred_protect = unwrap_option!(result.cred_protect, callback); + let large_blob_key = result.large_blob_key; + + if matches!( + cred_management.subcommand, + CredManagementCommand::EnumerateCredentialsBegin(..) + ) { + remaining_cred_ids = + unwrap_option!(result.total_credentials, callback) - 1; + } else { + remaining_cred_ids -= 1; + } + // We might have to change the global variable, but need the unmodified below + let current_rp_backup = current_rp; + let mut we_are_done = false; + if remaining_cred_ids > 0 { + cred_management.subcommand = + CredManagementCommand::EnumerateCredentialsGetNextCredential; + } else { + current_rp += 1; + // We have all credentials from this RP. Starting with the next RP. + if current_rp < credential_result.credential_list.len() { + cred_management.subcommand = + CredManagementCommand::EnumerateCredentialsBegin( + credential_result.credential_list[current_rp] + .rp_id_hash + .clone(), + ); + // We have to regenerate PUAP here. PUAT hasn't changed, but the content + // of the command has changed, and that is part of the PUAP-calculation + unwrap_result!( + cred_management.set_pin_uv_auth_param( + pin_uv_auth_result.get_pin_uv_auth_token() + ), + callback + ); + } else { + // Finally done iterating over all RPs and their Credentials + we_are_done = true; + } + } + let key = CredentialListEntry { + user, + credential_id, + public_key, + cred_protect, + large_blob_key, + }; + credential_result.credential_list[current_rp_backup] + .credentials + .push(key); + if we_are_done { + send_status( + &status, + StatusUpdate::InteractiveManagement( + InteractiveUpdate::CredentialManagementUpdate(( + CredentialManagementResult::CredentialList( + credential_result, + ), + Some(pin_uv_auth_result), + )), + ), + ); + return true; + } else { + continue; + } + } + CredManagementCommand::DeleteCredential(_) => { + send_status( + &status, + StatusUpdate::InteractiveManagement( + InteractiveUpdate::CredentialManagementUpdate(( + CredentialManagementResult::DeleteSucess, + Some(pin_uv_auth_result), + )), + ), + ); + return true; + } + CredManagementCommand::UpdateUserInformation(_) => { + send_status( + &status, + StatusUpdate::InteractiveManagement( + InteractiveUpdate::CredentialManagementUpdate(( + CredentialManagementResult::UpdateSuccess, + Some(pin_uv_auth_result), + )), + ), + ); + return true; + } + }; + } + Err(e) => { + handle_errors!( + e, + status, + callback, + pin_uv_auth_result, + skip_uv, + skip_puap, + cached_puat + ); + } + } + } + false +} + +pub(crate) fn configure_authenticator( + dev: &mut Device, + puat_result: Option, + cfg_subcommand: AuthConfigCommand, + status: Sender, + callback: StateCallback>, + alive: &dyn Fn() -> bool, +) -> bool { + let mut authcfg = AuthenticatorConfig::new(cfg_subcommand); + let mut skip_uv = false; + let authinfo = match dev.get_authenticator_info() { + Some(i) => i.clone(), + None => { + callback.call(Err(HIDError::DeviceNotInitialized.into())); + return false; + } + }; + + if authinfo.options.authnr_cfg != Some(true) { + callback.call(Err(AuthenticatorError::HIDError( + HIDError::UnsupportedCommand, + ))); + return false; + } + + let mut skip_puap = false; + let mut cached_puat = false; // If we were provided with a cached puat from the outside + let mut pin_uv_auth_result = puat_result + .clone() + .unwrap_or(PinUvAuthResult::NoAuthRequired); + match puat_result { + Some(PinUvAuthResult::SuccessGetPinToken(t)) + | Some(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions(t)) + | Some(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingPinWithPermissions(t)) + if t.permissions + .contains(PinUvAuthTokenPermission::AuthenticatorConfiguration) => + { + skip_puap = true; + cached_puat = true; + unwrap_result!(authcfg.set_pin_uv_auth_param(Some(t)), callback); + } + _ => {} + } + let mut pin = None; + while alive() { + // We can use the AuthenticatorConfiguration-command only in two cases: + // 1. The device also supports the uv_acfg-permission (otherwise we can't establish a PUAP) + // 2. The device is NOT protected by PIN/UV (yet). This allows organizations to configure + // the token, before handing them out. + // If authinfo.options.uv_acfg is not supported, this will return UnauthorizedPermission + if !skip_puap { + pin_uv_auth_result = unwrap_result!( + determine_puap_if_needed( + &mut authcfg, + dev, + skip_uv, + PinUvAuthTokenPermission::AuthenticatorConfiguration, + UserVerificationRequirement::Preferred, + &status, + alive, + &mut pin, + ), + callback + ); + } + + debug!("------------------------------------------------------------------"); + debug!("{authcfg:?} using {pin_uv_auth_result:?}"); + debug!("------------------------------------------------------------------"); + + let resp = dev.send_cbor_cancellable(&authcfg, alive); + match resp { + Ok(()) => { + let auth_info = unwrap_option!(dev.refresh_authenticator_info(), callback); + send_status( + &status, + StatusUpdate::InteractiveManagement(InteractiveUpdate::AuthConfigUpdate(( + AuthConfigResult::Success(auth_info.clone()), + Some(pin_uv_auth_result), + ))), + ); + return true; + } + Err(e) => { + handle_errors!( + e, + status, + callback, + pin_uv_auth_result, + skip_uv, + skip_puap, + cached_puat + ); + } + } + } + false +} diff --git a/third_party/rust/authenticator/src/ctap2/preflight.rs b/third_party/rust/authenticator/src/ctap2/preflight.rs new file mode 100644 index 0000000000..6574042136 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/preflight.rs @@ -0,0 +1,530 @@ +use super::client_data::ClientDataHash; +use super::commands::get_assertion::{GetAssertion, GetAssertionExtensions, GetAssertionOptions}; +use super::commands::{CtapResponse, PinUvAuthCommand, RequestCtap1, Retryable}; +use crate::consts::{PARAMETER_SIZE, U2F_AUTHENTICATE, U2F_CHECK_IS_REGISTERED}; +use crate::crypto::PinUvAuthToken; +use crate::ctap2::server::{PublicKeyCredentialDescriptor, RelyingParty}; +use crate::errors::AuthenticatorError; +use crate::transport::errors::{ApduErrorStatus, HIDError}; +use crate::transport::{FidoDevice, FidoProtocol, VirtualFidoDevice}; +use crate::u2ftypes::CTAP1RequestAPDU; +use sha2::{Digest, Sha256}; + +/// 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 struct CheckKeyHandle<'assertion> { + pub key_handle: &'assertion [u8], + pub client_data_hash: &'assertion [u8], + pub rp: &'assertion RelyingParty, +} + +type EmptyResponse = (); +impl CtapResponse for EmptyResponse {} + +impl<'assertion> RequestCtap1 for CheckKeyHandle<'assertion> { + type Output = EmptyResponse; + type AdditionalInfo = (); + + fn ctap1_format(&self) -> Result<(Vec, Self::AdditionalInfo), HIDError> { + // In theory, we only need to do this for up=true, for up=false, we could + // use U2F_DONT_ENFORCE_USER_PRESENCE_AND_SIGN instead and use the answer directly. + // But that would involve another major refactoring to implement, and so we accept + // that we will send the final request twice to the authenticator. Once with + // U2F_CHECK_IS_REGISTERED followed by U2F_DONT_ENFORCE_USER_PRESENCE_AND_SIGN. + let flags = U2F_CHECK_IS_REGISTERED; + let mut auth_data = Vec::with_capacity(2 * PARAMETER_SIZE + 1 + self.key_handle.len()); + + auth_data.extend_from_slice(self.client_data_hash); + 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, + _dev: &mut Dev, + status: Result<(), ApduErrorStatus>, + _input: &[u8], + _add_info: &Self::AdditionalInfo, + ) -> Result> { + // 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))), + } + } + + fn send_to_virtual_device( + &self, + dev: &mut Dev, + ) -> Result { + dev.check_key_handle(self) + } +} + +/// "pre-flight": In order to determine whether authenticatorMakeCredential's excludeList or +/// authenticatorGetAssertion's allowList contain credential IDs that are already +/// present on an authenticator, a platform typically invokes authenticatorGetAssertion +/// with the "up" option key set to false and optionally pinUvAuthParam one or more times. +/// For CTAP1, the resulting list will always be of length 1. +pub(crate) fn do_credential_list_filtering_ctap1( + dev: &mut Dev, + cred_list: &[PublicKeyCredentialDescriptor], + rp: &RelyingParty, + client_data_hash: &ClientDataHash, +) -> Option { + let key_handle = cred_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). + .filter(|key_handle| key_handle.id.len() < 256) + .find_map(|key_handle| { + let check_command = CheckKeyHandle { + key_handle: key_handle.id.as_ref(), + client_data_hash: client_data_hash.as_ref(), + rp, + }; + let res = dev.send_ctap1(&check_command); + match res { + Ok(_) => Some(key_handle.clone()), + _ => None, + } + }); + key_handle +} + +/// "pre-flight": In order to determine whether authenticatorMakeCredential's excludeList or +/// authenticatorGetAssertion's allowList contain credential IDs that are already +/// present on an authenticator, a platform typically invokes authenticatorGetAssertion +/// with the "up" option key set to false and optionally pinUvAuthParam one or more times. +pub(crate) fn do_credential_list_filtering_ctap2( + dev: &mut Dev, + cred_list: &[PublicKeyCredentialDescriptor], + rp: &RelyingParty, + pin_uv_auth_token: Option, +) -> Result, AuthenticatorError> { + let info = dev + .get_authenticator_info() + .ok_or(HIDError::DeviceNotInitialized)?; + let mut cred_list = cred_list.to_vec(); + // Step 1.0: Find out how long the exclude_list/allow_list is allowed to be + // If the token doesn't tell us, we assume a length of 1 + let mut chunk_size = match info.max_credential_count_in_list { + // Length 0 is not allowed by the spec, so we assume the device can't be trusted, which means + // falling back to a chunk size of 1 as the bare minimum. + None | Some(0) => 1, + Some(x) => x, + }; + + // Step 1.1: The device only supports keys up to a certain length. + // Filter out all keys that are longer, because they can't be + // from this device anyways. + match info.max_credential_id_length { + None => { /* no-op */ } + // Length 0 is not allowed by the spec, so we assume the device can't be trusted, which means + // falling back to a chunk size of 1 as the bare minimum. + Some(0) => { + chunk_size = 1; + } + Some(max_key_length) => { + cred_list.retain(|k| k.id.len() <= max_key_length); + } + } + + let chunked_list = cred_list.chunks(chunk_size); + + // Step 2: If we have more than one chunk: Loop over all, doing GetAssertion + // and if one of them comes back with a success, use only that chunk. + let mut final_list = Vec::new(); + for chunk in chunked_list { + let mut silent_assert = GetAssertion::new( + ClientDataHash(Sha256::digest("").into()), + rp.clone(), + chunk.to_vec(), + GetAssertionOptions { + user_verification: None, // defaults to Some(false) if puap is absent + user_presence: Some(false), + }, + GetAssertionExtensions::default(), + ); + silent_assert.set_pin_uv_auth_param(pin_uv_auth_token.clone())?; + match dev.send_msg(&silent_assert) { + Ok(mut response) => { + // This chunk contains a key_handle that is already known to the device. + // Filter out all credentials the device returned. Those are valid. + let credential_ids = response + .iter_mut() + .filter_map(|result| { + // CTAP 2.0 devices can omit the credentials in their response, + // if the given allowList was only 1 entry long. If so, we have + // to fill it in ourselfs. + if chunk.len() == 1 && result.assertion.credentials.is_none() { + Some(chunk[0].clone()) + } else { + result.assertion.credentials.take() + } + }) + .collect(); + // Replace credential_id_list with the valid credentials + final_list = credential_ids; + break; + } + Err(_) => { + // No-op: Go to next chunk. + // NOTE: while we expect a StatusCode::NoCredentials error here, some tokens return + // other values. + continue; + } + } + } + + // Step 3: Now ExcludeList/AllowList is either empty or has one batch with a 'known' credential. + // Send it as a normal Request and expect a "CredentialExcluded"-error in case of + // MakeCredential or a Success in case of GetAssertion + Ok(final_list) +} + +pub(crate) fn silently_discover_credentials( + dev: &mut Dev, + cred_list: &[PublicKeyCredentialDescriptor], + rp: &RelyingParty, + client_data_hash: &ClientDataHash, +) -> Vec { + if dev.get_protocol() == FidoProtocol::CTAP2 { + if let Ok(cred_list) = do_credential_list_filtering_ctap2(dev, cred_list, rp, None) { + return cred_list; + } + } else if let Some(key_handle) = + do_credential_list_filtering_ctap1(dev, cred_list, rp, client_data_hash) + { + return vec![key_handle]; + } + vec![] +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::{ + crypto::{COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve}, + ctap2::{ + attestation::{ + AAGuid, AttestedCredentialData, AuthenticatorData, AuthenticatorDataFlags, + Extension, + }, + commands::{CommandError, StatusCode}, + server::{AuthenticationExtensionsClientOutputs, AuthenticatorAttachment, Transport}, + }, + transport::{ + device_selector::tests::{make_device_simple_u2f, make_device_with_pin}, + hid::HIDDevice, + platform::device::Device, + }, + Assertion, GetAssertionResult, + }; + + fn new_relying_party(name: &str) -> RelyingParty { + RelyingParty { + id: String::from(name), + name: Some(String::from(name)), + } + } + + fn new_silent_assert( + rp: &RelyingParty, + allow_list: &[PublicKeyCredentialDescriptor], + ) -> GetAssertion { + GetAssertion::new( + ClientDataHash(Sha256::digest("").into()), + rp.clone(), + allow_list.to_vec(), + GetAssertionOptions { + user_verification: None, // defaults to Some(false) if puap is absent + user_presence: Some(false), + }, + GetAssertionExtensions::default(), + ) + } + + fn new_credential(fill: u8, repeat: usize) -> PublicKeyCredentialDescriptor { + PublicKeyCredentialDescriptor { + id: vec![fill; repeat], + transports: vec![Transport::USB], + } + } + + fn new_assertion_response( + rp: &RelyingParty, + cred: Option<&PublicKeyCredentialDescriptor>, + ) -> GetAssertionResult { + let credential_data = cred.map(|cred| AttestedCredentialData { + aaguid: AAGuid::default(), + credential_id: cred.id.clone(), + credential_public_key: COSEKey { + alg: COSEAlgorithm::RS256, + key: COSEKeyType::EC2(COSEEC2Key { + curve: Curve::SECP256R1, + x: vec![], + y: vec![], + }), + }, + }); + GetAssertionResult { + assertion: Assertion { + credentials: cred.cloned(), + auth_data: AuthenticatorData { + rp_id_hash: rp.hash(), + flags: AuthenticatorDataFlags::empty(), + counter: 0, + credential_data, + extensions: Extension::default(), + }, + signature: vec![], + user: None, + }, + attachment: AuthenticatorAttachment::Platform, + extensions: AuthenticationExtensionsClientOutputs::default(), + } + } + + fn new_check_key_handle<'a>( + rp: &'a RelyingParty, + client_data_hash: &'a ClientDataHash, + cred: &'a PublicKeyCredentialDescriptor, + ) -> CheckKeyHandle<'a> { + CheckKeyHandle { + key_handle: cred.id.as_ref(), + client_data_hash: client_data_hash.as_ref(), + rp, + } + } + + #[test] + fn test_preflight_ctap1_empty() { + let mut dev = Device::new("preflight").unwrap(); + make_device_simple_u2f(&mut dev); + let client_data_hash = ClientDataHash(Sha256::digest("").into()); + let rp = new_relying_party("preflight test"); + let res = silently_discover_credentials(&mut dev, &[], &rp, &client_data_hash); + assert!(res.is_empty()); + } + + #[test] + fn test_preflight_ctap1_multiple_replies() { + let mut dev = Device::new_skipping_serialization("preflight").unwrap(); + make_device_simple_u2f(&mut dev); + let rp = new_relying_party("preflight test"); + let cdh = ClientDataHash(Sha256::digest("").into()); + let allow_list = vec![ + new_credential(4, 4), + new_credential(3, 4), + new_credential(2, 4), + new_credential(1, 4), + ]; + dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[0])); + dev.add_upcoming_ctap_error(HIDError::ApduStatus( + ApduErrorStatus::WrongData, // Not a registered cred + )); + dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[1])); + dev.add_upcoming_ctap_error(HIDError::ApduStatus( + ApduErrorStatus::WrongData, // Not a registered cred + )); + dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[2])); + dev.add_upcoming_ctap_response(()); // Valid credential - the code exits here now and doesn't even look at the last one + + let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &cdh); + assert_eq!(res, vec![allow_list[2].clone()]); + } + + #[test] + fn test_preflight_ctap1_too_long_entries() { + let mut dev = Device::new_skipping_serialization("preflight").unwrap(); + make_device_simple_u2f(&mut dev); + let rp = new_relying_party("preflight test"); + let cdh = ClientDataHash(Sha256::digest("").into()); + let allow_list = vec![ + new_credential(4, 300), // ctap1 limit is 256 + new_credential(3, 4), + new_credential(2, 4), + new_credential(1, 4), + ]; + // allow_list[0] is filtered out due to its size + dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[1])); + dev.add_upcoming_ctap_error(HIDError::ApduStatus( + ApduErrorStatus::WrongData, // Not a registered cred + )); + dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[2])); + dev.add_upcoming_ctap_response(()); // Valid credential - the code exits here now and doesn't even look at the last one + + let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &cdh); + assert_eq!(res, vec![allow_list[2].clone()]); + } + + #[test] + fn test_preflight_ctap2_empty() { + let mut dev = Device::new("preflight").unwrap(); + make_device_with_pin(&mut dev); + let rp = new_relying_party("preflight test"); + let client_data_hash = ClientDataHash(Sha256::digest("").into()); + let res = silently_discover_credentials(&mut dev, &[], &rp, &client_data_hash); + assert!(res.is_empty()); + } + + #[test] + fn test_preflight_ctap20_no_cred_data() { + // CTAP2.0 tokens are allowed to not send any credential-data in their + // response, if the allow-list is of length one. See https://github.com/mozilla/authenticator-rs/issues/319 + let mut dev = Device::new_skipping_serialization("preflight").unwrap(); + make_device_with_pin(&mut dev); + let rp = new_relying_party("preflight test"); + let client_data_hash = ClientDataHash(Sha256::digest("").into()); + let allow_list = vec![new_credential(1, 4)]; + dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &allow_list)); + dev.add_upcoming_ctap_response(vec![new_assertion_response(&rp, None)]); + let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash); + assert_eq!(res, allow_list); + } + + #[test] + fn test_preflight_ctap2_one_valid_entry() { + let mut dev = Device::new_skipping_serialization("preflight").unwrap(); + make_device_with_pin(&mut dev); + let rp = new_relying_party("preflight test"); + let client_data_hash = ClientDataHash(Sha256::digest("").into()); + let allow_list = vec![new_credential(1, 4)]; + dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &allow_list)); + dev.add_upcoming_ctap_response(vec![new_assertion_response(&rp, Some(&allow_list[0]))]); + let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash); + assert_eq!(res, allow_list); + } + + #[test] + fn test_preflight_ctap2_multiple_entries() { + let mut dev = Device::new_skipping_serialization("preflight").unwrap(); + make_device_with_pin(&mut dev); + let rp = new_relying_party("preflight test"); + let client_data_hash = ClientDataHash(Sha256::digest("").into()); + let allow_list = vec![ + new_credential(3, 4), + new_credential(2, 4), + new_credential(1, 4), + new_credential(0, 4), + ]; + // Our test device doesn't say how many allow_list-entries it supports, so our code + // defaults to one. Thus three requests, with three answers. Only one of them + // valid. + dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &[allow_list[0].clone()])); + dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &[allow_list[1].clone()])); + dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &[allow_list[2].clone()])); + dev.add_upcoming_ctap_error(HIDError::Command(CommandError::StatusCode( + StatusCode::NoCredentials, + None, + ))); + dev.add_upcoming_ctap_error(HIDError::Command(CommandError::StatusCode( + StatusCode::NoCredentials, + None, + ))); + dev.add_upcoming_ctap_response(vec![new_assertion_response(&rp, Some(&allow_list[2]))]); + let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash); + assert_eq!(res, vec![allow_list[2].clone()]); + } + + #[test] + fn test_preflight_ctap2_multiple_replies() { + let mut dev = Device::new_skipping_serialization("preflight").unwrap(); + make_device_with_pin(&mut dev); + let rp = new_relying_party("preflight test"); + let client_data_hash = ClientDataHash(Sha256::digest("").into()); + let allow_list = vec![ + new_credential(4, 4), + new_credential(3, 4), + new_credential(2, 4), + new_credential(1, 4), + ]; + let mut info = dev.get_authenticator_info().unwrap().clone(); + info.max_credential_count_in_list = Some(5); + dev.set_authenticator_info(info); + // Our test device now says that it supports 5 allow_list-entries, + // so we can send all of them in one request + dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &allow_list)); + dev.add_upcoming_ctap_response(vec![ + new_assertion_response(&rp, Some(&allow_list[1])), + new_assertion_response(&rp, Some(&allow_list[2])), + new_assertion_response(&rp, Some(&allow_list[3])), + ]); + let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash); + assert_eq!(res, allow_list[1..].to_vec()); + } + + #[test] + fn test_preflight_ctap2_multiple_replies_some_invalid() { + let mut dev = Device::new_skipping_serialization("preflight").unwrap(); + make_device_with_pin(&mut dev); + let rp = new_relying_party("preflight test"); + let client_data_hash = ClientDataHash(Sha256::digest("").into()); + let allow_list = vec![ + new_credential(4, 4), + new_credential(3, 4), + new_credential(2, 4), + new_credential(1, 4), + ]; + let mut info = dev.get_authenticator_info().unwrap().clone(); + info.max_credential_count_in_list = Some(5); + dev.set_authenticator_info(info); + // Our test device now says that it supports 5 allow_list-entries, + // so we can send all of them in one request + dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &allow_list)); + dev.add_upcoming_ctap_response(vec![ + new_assertion_response(&rp, Some(&allow_list[1])), + new_assertion_response(&rp, None), // This will be ignored + new_assertion_response(&rp, Some(&allow_list[2])), + new_assertion_response(&rp, None), // This will be ignored + ]); + let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash); + assert_eq!(res, allow_list[1..=2].to_vec()); + } + + #[test] + fn test_preflight_ctap2_too_long_entries() { + let mut dev = Device::new_skipping_serialization("preflight").unwrap(); + make_device_with_pin(&mut dev); + let rp = new_relying_party("preflight test"); + let client_data_hash = ClientDataHash(Sha256::digest("").into()); + let allow_list = vec![ + new_credential(4, 50), // too long + new_credential(3, 4), + new_credential(2, 50), // too long + new_credential(1, 4), + ]; + let mut info = dev.get_authenticator_info().unwrap().clone(); + info.max_credential_count_in_list = Some(5); + info.max_credential_id_length = Some(20); + dev.set_authenticator_info(info); + // Our test device now says that it supports 5 allow_list-entries, + // so we can send all of them in one request, except for those + // that got pre-filtered, as they were too long. + dev.add_upcoming_ctap2_request(&new_silent_assert( + &rp, + &[allow_list[1].clone(), allow_list[3].clone()], + )); + dev.add_upcoming_ctap_response(vec![new_assertion_response(&rp, Some(&allow_list[1]))]); + let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash); + assert_eq!(res, vec![allow_list[1].clone()]); + } +} 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..fdf2637464 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/server.rs @@ -0,0 +1,629 @@ +use crate::crypto::COSEAlgorithm; +use crate::{errors::AuthenticatorError, AuthenticatorTransports, KeyHandle}; +use base64::Engine; +use serde::de::MapAccess; +use serde::{ + de::{Error as SerdeError, Unexpected, Visitor}, + ser::SerializeMap, + Deserialize, Deserializer, Serialize, Serializer, +}; +use serde_bytes::{ByteBuf, Bytes}; +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::engine::general_purpose::URL_SAFE_NO_PAD.encode(self.0); + 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 { + let mut payload = [0u8; 32]; + if src.len() != payload.len() { + Err(AuthenticatorError::InvalidRelyingPartyInput) + } else { + payload.copy_from_slice(src); + Ok(RpIdHash(payload)) + } + } +} + +// NOTE: WebAuthn requires all fields and CTAP2 does not. +#[derive(Debug, Serialize, Clone, Default, Deserialize, PartialEq, Eq)] +pub struct RelyingParty { + pub id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +impl RelyingParty { + pub fn from(id: S) -> Self + where + S: Into, + { + Self { + id: id.into(), + name: None, + } + } + + pub fn hash(&self) -> RpIdHash { + let mut hasher = Sha256::new(); + hasher.update(&self.id); + + let mut output = [0u8; 32]; + output.copy_from_slice(hasher.finalize().as_slice()); + + RpIdHash(output) + } +} + +// NOTE: WebAuthn requires all fields and CTAP2 does not. +#[derive(Debug, Serialize, Clone, Eq, PartialEq, Deserialize, Default)] +pub struct PublicKeyCredentialUserEntity { + #[serde(with = "serde_bytes")] + pub id: Vec, + pub name: Option, + #[serde(skip_serializing_if = "Option::is_none", rename = "displayName")] + pub display_name: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PublicKeyCredentialParameters { + pub alg: COSEAlgorithm, +} + +impl TryFrom for PublicKeyCredentialParameters { + type Error = AuthenticatorError; + fn try_from(arg: i32) -> Result { + let alg = COSEAlgorithm::try_from(arg as i64)?; + Ok(PublicKeyCredentialParameters { alg }) + } +} + +impl Serialize for PublicKeyCredentialParameters { + fn serialize(&self, serializer: S) -> Result + 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(deserializer: D) -> Result + 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(self, mut map: M) -> Result + 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_else(|| 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 for Vec { + 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 + } +} + +pub type PublicKeyCredentialId = Vec; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PublicKeyCredentialDescriptor { + pub id: PublicKeyCredentialId, + pub transports: Vec, +} + +impl Serialize for PublicKeyCredentialDescriptor { + fn serialize(&self, serializer: S) -> Result + 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("id", Bytes::new(&self.id))?; + map.serialize_entry("type", "public-key")?; + // map.serialize_entry("transports", &self.transports)?; + map.end() + } +} + +impl<'de> Deserialize<'de> for PublicKeyCredentialDescriptor { + fn deserialize(deserializer: D) -> Result + 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(self, mut map: M) -> Result + 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_else(|| SerdeError::missing_field("id"))?; + let transports = transports.unwrap_or_default(); + + Ok(PublicKeyCredentialDescriptor { id, transports }) + } + } + + deserializer.deserialize_any(PublicKeyCredentialDescriptorVisitor) + } +} + +impl From<&KeyHandle> for PublicKeyCredentialDescriptor { + fn from(kh: &KeyHandle) -> Self { + Self { + id: kh.credential.clone(), + transports: kh.transports.into(), + } + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum ResidentKeyRequirement { + Discouraged, + Preferred, + Required, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum UserVerificationRequirement { + Discouraged, + Preferred, + Required, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum CredentialProtectionPolicy { + UserVerificationOptional = 1, + UserVerificationOptionalWithCredentialIDList = 2, + UserVerificationRequired = 3, +} + +impl Serialize for CredentialProtectionPolicy { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_u64(*self as u64) + } +} + +impl<'de> Deserialize<'de> for CredentialProtectionPolicy { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct CredentialProtectionPolicyVisitor; + + impl<'de> Visitor<'de> for CredentialProtectionPolicyVisitor { + type Value = CredentialProtectionPolicy; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an integer") + } + + fn visit_u64(self, v: u64) -> Result + where + E: SerdeError, + { + match v { + 1 => Ok(CredentialProtectionPolicy::UserVerificationOptional), + 2 => Ok( + CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIDList, + ), + 3 => Ok(CredentialProtectionPolicy::UserVerificationRequired), + _ => Err(SerdeError::invalid_value( + Unexpected::Unsigned(v), + &"valid CredentialProtectionPolicy", + )), + } + } + } + + deserializer.deserialize_any(CredentialProtectionPolicyVisitor) + } +} + +#[derive(Clone, Debug, Default)] +pub struct AuthenticationExtensionsClientInputs { + pub app_id: Option, + pub cred_props: Option, + pub credential_protection_policy: Option, + pub enforce_credential_protection_policy: Option, + pub hmac_create_secret: Option, + pub min_pin_length: Option, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct CredentialProperties { + pub rk: bool, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct AuthenticationExtensionsClientOutputs { + pub app_id: Option, + pub cred_props: Option, + pub hmac_create_secret: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum AuthenticatorAttachment { + CrossPlatform, + Platform, + Unknown, +} + +#[cfg(test)] +mod test { + use super::{ + COSEAlgorithm, PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, + PublicKeyCredentialUserEntity, RelyingParty, Transport, + }; + use serde_cbor::from_slice; + + fn create_user() -> PublicKeyCredentialUserEntity { + PublicKeyCredentialUserEntity { + 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, + ], + name: Some(String::from("johnpsmith@example.com")), + display_name: Some(String::from("John P. Smith")), + } + } + #[test] + fn serialize_rp() { + let rp = RelyingParty { + id: String::from("Acme"), + name: 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 test_deserialize_user() { + // This includes an obsolete "icon" field to test that deserialization + // ignores it. + let input = 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, // ... + ]; + let expected = create_user(); + let actual: PublicKeyCredentialUserEntity = from_slice(&input).unwrap(); + assert_eq!(expected, actual); + } + + #[test] + fn serialize_user() { + let user = create_user(); + + let payload = ser::to_vec(&user).unwrap(); + println!("payload = {payload:?}"); + assert_eq!( + payload, + vec![ + 0xa3, // map(3) + 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, // ... + 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_nodisplayname() { + let user = PublicKeyCredentialUserEntity { + 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, + ], + 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) + 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, // ... + 0x64, // text(4) + 0x74, 0x79, 0x70, 0x65, // "type" + 0x6a, // text(10) + 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, // "public-key" + 0x2D, 0x6B, 0x65, + 0x79, // ... + + // 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..b5d84c7d34 --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/utils.rs @@ -0,0 +1,39 @@ +use serde::de; +use serde_cbor::Deserializer; +use std::io::Read; + +pub fn serde_parse_err(s: &str) -> E { + E::custom(format!("Failed to parse {s}")) +} + +pub fn from_slice_stream<'a, T, R: Read, E: de::Error>(data: &mut R) -> Result +where + T: de::Deserialize<'a>, +{ + let mut deserializer = Deserializer::from_reader(data); + de::Deserialize::deserialize(&mut deserializer) + .map_err(|x| serde_parse_err(&format!("{}: {}", stringify!(T), &x.to_string()))) +} + +// Parsing routines + +pub fn read_be_u32(data: &mut R) -> Result { + let mut buf = [0; 4]; + data.read_exact(&mut buf) + .map_err(|_| serde_parse_err("u32"))?; + Ok(u32::from_be_bytes(buf)) +} + +pub fn read_be_u16(data: &mut R) -> Result { + let mut buf = [0; 2]; + data.read_exact(&mut buf) + .map_err(|_| serde_parse_err("u16"))?; + Ok(u16::from_be_bytes(buf)) +} + +pub fn read_byte(data: &mut R) -> Result { + match data.bytes().next() { + Some(Ok(s)) => Ok(s), + _ => Err(serde_parse_err("u8")), + } +} diff --git a/third_party/rust/authenticator/src/errors.rs b/third_party/rust/authenticator/src/errors.rs new file mode 100644 index 0000000000..0e3b969bb3 --- /dev/null +++ b/third_party/rust/authenticator/src/errors.rs @@ -0,0 +1,135 @@ +/* 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 { + CredProtect, + EmptyAllowList, + MaxPinLength, + PubCredParams, + ResidentKey, + UserVerification, +} + +#[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), + CancelledByUser, + CredentialExcluded, +} + +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, "{manager} expected arguments of version CTAP{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:?}") + } + AuthenticatorError::CancelledByUser => { + write!(f, "Cancelled by user.") + } + AuthenticatorError::CredentialExcluded => { + write!(f, "Credential excluded.") + } + } + } +} + +impl From for AuthenticatorError { + fn from(err: io::Error) -> AuthenticatorError { + AuthenticatorError::Io(err) + } +} + +impl From for AuthenticatorError { + fn from(err: HIDError) -> AuthenticatorError { + AuthenticatorError::HIDError(err) + } +} + +impl From for AuthenticatorError { + fn from(err: CommandError) -> AuthenticatorError { + AuthenticatorError::HIDError(HIDError::Command(err)) + } +} + +impl From> for AuthenticatorError { + fn from(err: mpsc::SendError) -> 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..5dd4133b8f --- /dev/null +++ b/third_party/rust/authenticator/src/lib.rs @@ -0,0 +1,113 @@ +/* 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::large_enum_variant)] +#![allow(clippy::upper_case_acronyms)] +#![allow(clippy::bool_to_int_with_if)] + +#[macro_use] +mod util; + +#[cfg(target_os = "linux")] +extern crate libudev; + +#[cfg(target_os = "freebsd")] +extern crate devd_rs; + +#[cfg(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; + +mod consts; +mod manager; +mod statemachine; +mod status_update; +mod transport; +mod u2ftypes; + +pub mod authenticatorservice; +pub mod crypto; +pub mod ctap2; +pub mod errors; +pub mod statecallback; +pub use ctap2::attestation::AttestationObject; +pub use ctap2::commands::bio_enrollment::BioEnrollmentResult; +pub use ctap2::commands::client_pin::{Pin, PinError}; +pub use ctap2::commands::credential_management::CredentialManagementResult; +pub use ctap2::commands::get_assertion::{Assertion, GetAssertionResult}; +pub use ctap2::commands::get_info::AuthenticatorInfo; +pub use ctap2::commands::make_credentials::MakeCredentialsResult; +use serde::Serialize; +pub use statemachine::StateMachine; +pub use status_update::{ + BioEnrollmentCmd, CredManagementCmd, InteractiveRequest, InteractiveUpdate, StatusPinUv, + StatusUpdate, +}; +pub use transport::{FidoDevice, FidoDeviceIO, FidoProtocol, VirtualFidoDevice}; + +// 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, + pub transports: AuthenticatorTransports, +} + +pub type AppId = Vec; + +pub type RegisterResult = MakeCredentialsResult; +pub type SignResult = GetAssertionResult; + +#[derive(Debug, Serialize)] +pub enum ManageResult { + Success, + CredManagement(CredentialManagementResult), + BioEnrollment(BioEnrollmentResult), +} + +pub type ResetResult = (); + +impl From for ManageResult { + fn from(_value: ResetResult) -> Self { + ManageResult::Success + } +} + +pub type Result = std::result::Result; + +#[cfg(test)] +#[macro_use] +extern crate assert_matches; + +#[cfg(fuzzing)] +pub use consts::*; +#[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..47e3a06c10 --- /dev/null +++ b/third_party/rust/authenticator/src/manager.rs @@ -0,0 +1,218 @@ +/* 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, SignArgs}; +use crate::errors::*; +use crate::statecallback::StateCallback; +use crate::statemachine::StateMachine; +use crate::Pin; +use runloop::RunLoop; +use std::io; +use std::sync::mpsc::{channel, RecvTimeoutError, Sender}; +use std::time::Duration; + +enum QueueAction { + Register { + timeout: u64, + register_args: RegisterArgs, + status: Sender, + callback: StateCallback>, + }, + Sign { + timeout: u64, + sign_args: SignArgs, + status: Sender, + callback: StateCallback>, + }, + Cancel, + Reset { + timeout: u64, + status: Sender, + callback: StateCallback>, + }, + SetPin { + timeout: u64, + new_pin: Pin, + status: Sender, + callback: StateCallback>, + }, + InteractiveManagement { + timeout: u64, + status: Sender, + callback: StateCallback>, + }, +} + +pub struct Manager { + queue: RunLoop, + tx: Sender, +} + +impl Manager { + pub fn new() -> io::Result { + 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::Register { + timeout, + register_args, + status, + callback, + }) => { + // This must not block, otherwise we can't cancel. + sm.register(timeout, register_args, status, callback); + } + + Ok(QueueAction::Sign { + timeout, + sign_args, + status, + callback, + }) => { + // This must not block, otherwise we can't cancel. + sm.sign(timeout, sign_args, 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::InteractiveManagement { + timeout, + status, + callback, + }) => { + // Manage token interactively + sm.manage(timeout, status, callback); + } + + 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, + register_args: RegisterArgs, + status: Sender, + callback: StateCallback>, + ) -> Result<(), AuthenticatorError> { + let action = QueueAction::Register { + timeout, + register_args, + status, + callback, + }; + Ok(self.tx.send(action)?) + } + + fn sign( + &mut self, + timeout: u64, + sign_args: SignArgs, + status: Sender, + callback: StateCallback>, + ) -> crate::Result<()> { + let action = QueueAction::Sign { + timeout, + sign_args, + 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, + callback: StateCallback>, + ) -> Result<(), AuthenticatorError> { + Ok(self.tx.send(QueueAction::Reset { + timeout, + status, + callback, + })?) + } + + fn set_pin( + &mut self, + timeout: u64, + new_pin: Pin, + status: Sender, + callback: StateCallback>, + ) -> crate::Result<()> { + Ok(self.tx.send(QueueAction::SetPin { + timeout, + new_pin, + status, + callback, + })?) + } + + fn manage( + &mut self, + timeout: u64, + status: Sender, + callback: StateCallback>, + ) -> Result<(), AuthenticatorError> { + Ok(self.tx.send(QueueAction::InteractiveManagement { + timeout, + 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..17f3881571 --- /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 { + callback: Arc>>>, + observer: Arc>>>, + condition: Arc<(Mutex, Condvar)>, +} + +impl StateCallback { + // This is used for the Condvar, which requires this kind of construction + #[allow(clippy::mutex_atomic)] + pub fn new(cb: Box) -> 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) { + 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 Clone for StateCallback { + 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..71bda827f0 --- /dev/null +++ b/third_party/rust/authenticator/src/statemachine.rs @@ -0,0 +1,427 @@ +/* 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::{RegisterArgs, SignArgs}; + +use crate::ctap2; + +use crate::ctap2::commands::client_pin::Pin; + +use crate::errors::AuthenticatorError; +use crate::statecallback::StateCallback; +use crate::status_update::{send_status, InteractiveUpdate}; +use crate::transport::device_selector::{ + BlinkResult, Device, DeviceBuildParameters, DeviceCommand, DeviceSelectorEvent, +}; +use crate::transport::platform::transaction::Transaction; +use crate::transport::{hid::HIDDevice, FidoDevice, FidoProtocol}; +use crate::{InteractiveRequest, ManageResult}; +use std::sync::mpsc::{channel, RecvTimeoutError, Sender}; + +use std::time::Duration; + +#[derive(Default)] +pub struct StateMachine { + transaction: Option, +} + +impl StateMachine { + pub fn new() -> Self { + Default::default() + } + + fn init_device( + info: DeviceBuildParameters, + selector: &Sender, + ) -> Option { + // Create a new device. + let mut dev = match Device::new(info) { + Ok(dev) => dev, + Err((e, id)) => { + info!("error happened with device: {}", e); + let _ = selector.send(DeviceSelectorEvent::NotAToken(id)); + return None; + } + }; + + // Try initializing it. + if let Err(e) = dev.init() { + warn!("error while initializing device: {}", e); + let _ = selector.send(DeviceSelectorEvent::NotAToken(dev.id())); + return None; + } + + Some(dev) + } + + fn wait_for_device_selector( + dev: &mut Device, + selector: &Sender, + status: &Sender, + keep_alive: &dyn Fn() -> bool, + ) -> bool { + let (tx, rx) = channel(); + if selector + .send(DeviceSelectorEvent::ImAToken((dev.id(), tx))) + .is_err() + { + // If we fail to register with the device selector, then we're not going + // to be selected. + return false; + } + + // We can be cancelled from the user (through keep_alive()) or from the device selector + // (through a DeviceCommand::Cancel on rx). We'll combine those signals into a single + // predicate to pass to Device::block_and_blink. + let keep_blinking = || keep_alive() && !matches!(rx.try_recv(), Ok(DeviceCommand::Cancel)); + + // Blocking recv. DeviceSelector will tell us what to do + match rx.recv() { + Ok(DeviceCommand::Blink) => { + // The caller wants the user to choose a device. Send a status update and blink + // this device. NOTE: We send one status update per device, so the recipient should be + // prepared to receive the message multiple times. + send_status(status, crate::StatusUpdate::SelectDeviceNotice); + match dev.block_and_blink(&keep_blinking) { + BlinkResult::DeviceSelected => { + // User selected us. Let DeviceSelector know, so it can cancel all other + // outstanding open blink-requests. If we fail to send the SelectedToken + // message to the device selector, then don't consider this token as having + // been selected. + selector + .send(DeviceSelectorEvent::SelectedToken(dev.id())) + .is_ok() + } + BlinkResult::Cancelled => { + info!("Device {:?} was not selected", dev.id()); + false + } + } + } + Ok(DeviceCommand::Cancel) => { + info!("Device {:?} was not selected", dev.id()); + false + } + Ok(DeviceCommand::Removed) => { + info!("Device {:?} was removed", dev.id()); + false + } + Ok(DeviceCommand::Continue) => true, + Err(_) => { + warn!("Error when trying to receive messages from DeviceSelector! Exiting."); + false + } + } + } + + pub fn register( + &mut self, + timeout: u64, + args: RegisterArgs, + status: Sender, + callback: StateCallback>, + ) { + // 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_device(info, &selector) { + Some(dev) => dev, + None => return, + }; + if !Self::wait_for_device_selector(&mut dev, &selector, &status, alive) { + return; + }; + + if args.use_ctap1_fallback { + dev.downgrade_to_ctap1(); + } + + info!("Device {:?} continues with the register process", dev.id()); + if ctap2::register(&mut dev, args.clone(), status, callback.clone(), alive) { + // ctap2::register returns true if it called the callback with Ok(..). 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())); + } + }, + ); + + self.transaction = Some(try_or!(transaction, |e| cbc.call(Err(e)))); + } + + pub fn sign( + &mut self, + timeout: u64, + args: SignArgs, + status: Sender, + callback: StateCallback>, + ) { + // 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_device(info, &selector) { + Some(dev) => dev, + None => return, + }; + if !Self::wait_for_device_selector(&mut dev, &selector, &status, alive) { + return; + }; + + if args.use_ctap1_fallback { + dev.downgrade_to_ctap1(); + } + + info!("Device {:?} continues with the signing process", dev.id()); + if ctap2::sign(&mut dev, args.clone(), status, callback.clone(), alive) { + // ctap2::sign returns true if it called the callback with Ok(..). 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())); + } + }, + ); + + 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, + callback: StateCallback>, + ) { + // 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_device(info, &selector) { + Some(dev) => dev, + None => return, + }; + + if dev.get_protocol() != FidoProtocol::CTAP2 { + info!("Device does not support CTAP2"); + let _ = selector.send(DeviceSelectorEvent::NotAToken(dev.id())); + return; + } + + if !Self::wait_for_device_selector(&mut dev, &selector, &status, alive) { + return; + }; + ctap2::reset_helper(&mut dev, selector, status, callback.clone(), alive); + }, + ); + + 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, + callback: StateCallback>, + ) { + // 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_device(info, &selector) { + Some(dev) => dev, + None => return, + }; + + if dev.get_protocol() != FidoProtocol::CTAP2 { + info!("Device does not support CTAP2"); + let _ = selector.send(DeviceSelectorEvent::NotAToken(dev.id())); + return; + } + + if !Self::wait_for_device_selector(&mut dev, &selector, &status, alive) { + return; + }; + + ctap2::set_or_change_pin_helper( + &mut dev, + None, + new_pin.clone(), + status, + callback.clone(), + alive, + ); + }, + ); + self.transaction = Some(try_or!(transaction, move |e| cbc.call(Err(e)))); + } + + // Function to interactively manage a specific token. + // Difference to register/sign: These want to do something and don't care + // with which token they do it. + // This function wants to manipulate a specific token. For this, we first + // have to select one and then do something with it, based on what it + // supports (Set PIN, Change PIN, Reset, etc.). + // Hence, we first go through the discovery-phase, then provide the user + // with the AuthenticatorInfo and then let them interactively decide what to do + pub fn manage( + &mut self, + timeout: u64, + status: Sender, + callback: StateCallback>, + ) { + // 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_device(info, &selector) { + Some(dev) => dev, + None => return, + }; + + if dev.get_protocol() != FidoProtocol::CTAP2 { + info!("Device does not support CTAP2"); + let _ = selector.send(DeviceSelectorEvent::NotAToken(dev.id())); + return; + } + + if !Self::wait_for_device_selector(&mut dev, &selector, &status, alive) { + return; + }; + + info!("Device {:?} selected for interactive management.", dev.id()); + + // Sending the user the info about the token + let (tx, rx) = channel(); + send_status( + &status, + crate::StatusUpdate::InteractiveManagement(InteractiveUpdate::StartManagement( + (tx, dev.get_authenticator_info().cloned()), + )), + ); + while alive() { + match rx.recv_timeout(Duration::from_millis(400)) { + Ok(InteractiveRequest::Quit) => { + callback.call(Ok(ManageResult::Success)); + break; + } + Ok(InteractiveRequest::Reset) => { + ctap2::reset_helper( + &mut dev, + selector, + status, + callback.clone(), + alive, + ); + } + Ok(InteractiveRequest::ChangePIN(curr_pin, new_pin)) => { + ctap2::set_or_change_pin_helper( + &mut dev, + Some(curr_pin), + new_pin, + status, + callback.clone(), + alive, + ); + } + Ok(InteractiveRequest::SetPIN(pin)) => { + ctap2::set_or_change_pin_helper( + &mut dev, + None, + pin, + status, + callback.clone(), + alive, + ); + } + Ok(InteractiveRequest::ChangeConfig(authcfg, puat)) => { + ctap2::configure_authenticator( + &mut dev, + puat, + authcfg, + status.clone(), + callback.clone(), + alive, + ); + continue; + } + Ok(InteractiveRequest::CredentialManagement(cred_management, puat)) => { + ctap2::credential_management( + &mut dev, + puat, + cred_management, + status.clone(), + callback.clone(), + alive, + ); + continue; + } + Ok(InteractiveRequest::BioEnrollment(bio_enrollment, puat)) => { + ctap2::bio_enrollment( + &mut dev, + puat, + bio_enrollment, + status.clone(), + callback.clone(), + alive, + ); + continue; + } + Err(RecvTimeoutError::Timeout) => { + if !alive() { + // We got stopped at some point + callback.call(Err(AuthenticatorError::CancelledByUser)); + break; + } + continue; + } + Err(RecvTimeoutError::Disconnected) => { + // recv() failed, because the other side is dropping the Sender. + info!( + "Callback dropped the channel, so we abort the interactive session" + ); + callback.call(Err(AuthenticatorError::CancelledByUser)); + } + } + break; + } + }, + ); + + 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..c4a7fee75a --- /dev/null +++ b/third_party/rust/authenticator/src/status_update.rs @@ -0,0 +1,117 @@ +use super::Pin; +use crate::{ + ctap2::{ + commands::{ + authenticator_config::{AuthConfigCommand, AuthConfigResult}, + bio_enrollment::BioTemplateId, + get_info::AuthenticatorInfo, + PinUvAuthResult, + }, + server::{PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity}, + }, + BioEnrollmentResult, CredentialManagementResult, +}; +use serde::{Deserialize, Serialize as DeriveSer, Serializer}; +use std::sync::mpsc::Sender; + +#[derive(Debug, Deserialize, DeriveSer)] +pub enum CredManagementCmd { + GetCredentials, + DeleteCredential(PublicKeyCredentialDescriptor), + UpdateUserInformation(PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity), +} + +#[derive(Debug, Deserialize, DeriveSer)] +pub enum BioEnrollmentCmd { + GetFingerprintSensorInfo, + GetEnrollments, + StartNewEnrollment(Option), + DeleteEnrollment(BioTemplateId), + ChangeName(BioTemplateId, String), +} + +#[derive(Debug)] +pub enum InteractiveRequest { + Quit, + Reset, + ChangePIN(Pin, Pin), + SetPIN(Pin), + ChangeConfig(AuthConfigCommand, Option), + CredentialManagement(CredManagementCmd, Option), + BioEnrollment(BioEnrollmentCmd, Option), +} + +// Simply ignoring the Sender when serializing +pub(crate) fn serialize_pin_required(_: &Sender, s: S) -> Result +where + S: Serializer, +{ + s.serialize_none() +} + +// Simply ignoring the Sender when serializing +pub(crate) fn serialize_pin_invalid( + _: &Sender, + retries: &Option, + s: S, +) -> Result +where + S: Serializer, +{ + if let Some(r) = retries { + s.serialize_u8(*r) + } else { + s.serialize_none() + } +} + +#[derive(Debug, DeriveSer)] +pub enum StatusPinUv { + #[serde(serialize_with = "serialize_pin_required")] + PinRequired(Sender), + #[serde(serialize_with = "serialize_pin_invalid")] + InvalidPin(Sender, Option), + PinIsTooShort, + PinIsTooLong(usize), + InvalidUv(Option), + // This SHOULD ever only happen for CTAP2.0 devices that + // use internal UV (e.g. fingerprint sensors) and failed (e.g. wrong + // finger used). + // PinAuthInvalid, // Folded into InvalidUv + PinAuthBlocked, + PinBlocked, + PinNotSet, + UvBlocked, +} + +#[derive(Debug)] +pub enum InteractiveUpdate { + StartManagement((Sender, Option)), + // We provide the already determined PUAT to be able to issue more requests without + // forcing the user to enter another PIN. + BioEnrollmentUpdate((BioEnrollmentResult, Option)), + CredentialManagementUpdate((CredentialManagementResult, Option)), + AuthConfigUpdate((AuthConfigResult, Option)), +} + +#[derive(Debug)] +pub enum StatusUpdate { + /// We're waiting for the user to touch their token + PresenceRequired, + /// 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). + PinUvError(StatusPinUv), + /// Sent, if multiple devices are found and the user has to select one + SelectDeviceNotice, + /// Sent when a token was selected for interactive management + InteractiveManagement(InteractiveUpdate), + /// Sent when a token returns multiple results for a getAssertion request + SelectResultNotice(Sender>, Vec), +} + +pub(crate) fn send_status(status: &Sender, msg: StatusUpdate) { + match status.send(msg) { + Ok(_) => {} + Err(e) => error!("Couldn't send status: {:?}", e), + }; +} 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..f745b456d9 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/device_selector.rs @@ -0,0 +1,477 @@ +use crate::transport::hid::HIDDevice; + +pub use crate::transport::platform::device::Device; + +use runloop::RunLoop; +use std::collections::{HashMap, HashSet}; +use std::sync::mpsc::{channel, RecvTimeoutError, Sender}; +use std::time::Duration; + +pub type DeviceID = ::Id; +pub type DeviceBuildParameters = ::BuildParameters; + +trait DeviceSelectorEventMarker {} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BlinkResult { + DeviceSelected, + Cancelled, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DeviceCommand { + Blink, + Cancel, + Continue, + Removed, +} + +#[derive(Debug)] +pub enum DeviceSelectorEvent { + Cancel, + Timeout, + DevicesAdded(Vec), + DeviceRemoved(DeviceID), + NotAToken(DeviceID), + ImAToken((DeviceID, Sender)), + SelectedToken(DeviceID), +} + +pub struct DeviceSelector { + /// How to send a message to the event loop + sender: Sender, + /// Thread of the event loop + runloop: RunLoop, +} + +impl DeviceSelector { + pub fn run() -> 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(); + // Device IDs of devices that responded with "ImAToken" mapping to channels that are + // waiting to receive a DeviceCommand + 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(ref id) => { + 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(ref 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_id, tx)| { + if dev_id == id { + let _ = tx.send(DeviceCommand::Removed); + } + }); + tokens.retain(|dev_id, _| 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(ref id) => { + debug!("Device not a token event: {:?}", id); + waiting_for_response.remove(id); + } + DeviceSelectorEvent::ImAToken((id, tx)) => { + let _ = waiting_for_response.remove(&id); + if blinking { + // We are already blinking, so this new device should blink too. + if tx.send(DeviceCommand::Blink).is_ok() { + tokens.insert(id, tx.clone()); + } + continue; + } else { + tokens.insert(id, tx.clone()); + } + } + } + + // 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_id, 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; + } + 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); + }); + } + } + } + }); + Self { + runloop: runloop.unwrap(), // TODO + sender: selector_send, + } + } + + pub fn clone_sender(&self) -> Sender { + self.sender.clone() + } + + fn cancel_all(tokens: HashMap>, exclude: Option<&DeviceID>) { + for (dev_id, tx) in tokens.iter() { + if Some(dev_id) != exclude { + let _ = tx.send(DeviceCommand::Cancel); + } + } + } + + 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}, + transport::FidoDevice, + u2ftypes::U2FDeviceInfo, + }; + + pub(crate) 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, + } + } + + pub(crate) fn make_device_simple_u2f(dev: &mut Device) { + dev.set_device_info(gen_info(dev.id())); + dev.set_cid([1, 2, 3, 4]); // Need to set something other than broadcast + dev.downgrade_to_ctap1(); + dev.create_channel(); + } + + pub(crate) fn make_device_with_pin(dev: &mut Device) { + dev.set_device_info(gen_info(dev.id())); + dev.set_cid([1, 2, 3, 4]); // Need to set something other than broadcast + dev.create_channel(); + let info = AuthenticatorInfo { + options: AuthenticatorOptions { + client_pin: Some(true), + ..Default::default() + }, + ..Default::default() + }; + dev.set_authenticator_info(info); + } + + fn send_i_am_token(dev: &Device, selector: &DeviceSelector) { + selector + .sender + .send(DeviceSelectorEvent::ImAToken(( + dev.id(), + 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 add_devices<'a, T>(iter: T, selector: &DeviceSelector) + where + T: Iterator, + { + 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 selector = DeviceSelector::run(); + + // 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); + + assert_eq!( + devices[2].receiver.as_ref().unwrap().recv().unwrap(), + DeviceCommand::Continue + ); + } + + // This test is mostly for testing stop() and clone_sender() + #[test] + fn test_device_selector_stop() { + let device = Device::new("device selector 1").unwrap(); + + let mut selector = DeviceSelector::run(); + + // Adding all + selector + .clone_sender() + .send(DeviceSelectorEvent::DevicesAdded(vec![device.id()])) + .unwrap(); + + selector + .clone_sender() + .send(DeviceSelectorEvent::NotAToken(device.id())) + .unwrap(); + selector.stop(); + } + + #[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 selector = DeviceSelector::run(); + + // 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); + + devices.iter_mut().for_each(|d| { + if !d.is_u2f() { + send_no_token(d, &selector); + } + }); + + send_i_am_token(&devices[4], &selector); + + // 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 + ); + + // Plug in late device + send_i_am_token(&devices[5], &selector); + 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 selector = DeviceSelector::run(); + + // 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); + + devices.iter_mut().for_each(|d| { + if !d.is_u2f() { + send_no_token(d, &selector); + } + }); + + send_i_am_token(&devices[4], &selector); + + // 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 + ); + + // Plug in late device + send_i_am_token(&devices[5], &selector); + assert_eq!( + devices[5].receiver.as_ref().unwrap().recv().unwrap(), + DeviceCommand::Blink + ); + // Remove device again + remove_device(&devices[5], &selector); + + // 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); + 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 selector = DeviceSelector::run(); + + // 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] { + assert_eq!( + devices[idx].receiver.as_ref().unwrap().recv().unwrap(), + DeviceCommand::Blink + ); + } + + // Remove all tokens + for idx in [2, 4, 5] { + remove_device(&devices[idx], &selector); + } + + // Adding one again + send_i_am_token(&devices[4], &selector); + + // 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 + ); + } +} 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..451c27d6e8 --- /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, io::Error), + UnexpectedCmd(u8), + Command(CommandError), + ApduStatus(ApduErrorStatus), +} + +impl From for HIDError { + fn from(e: io::Error) -> HIDError { + HIDError::IO(None, e) + } +} + +impl From for HIDError { + fn from(e: CommandError) -> HIDError { + HIDError::Command(e) + } +} + +impl From 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..0e8c7d20a7 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/freebsd/device.rs @@ -0,0 +1,235 @@ +/* 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::{Capability, CID_BROADCAST, MAX_HID_RPT_SIZE}; +use crate::ctap2::commands::get_info::AuthenticatorInfo; +use crate::transport::hid::HIDDevice; +use crate::transport::platform::uhid; +use crate::transport::{FidoDevice, FidoProtocol, HIDError, SharedSecret}; +use crate::u2ftypes::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, + secret: Option, + authenticator_info: Option, + protocol: FidoProtocol, +} + +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 + + if self.write(&buf)? != buf.len() { + return Err(io_err("write ping failed")); + } + + // 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. When reports come in they are all + // exactly the same size, with no report id byte because + // there is only one report. + let n = self.read(&mut buf[1..])?; + if n != buf.len() - 1 { + return Err(io_err("read pong failed")); + } + + 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(&self, state: &mut H) { + self.path.hash(state); + } +} + +impl Read for Device { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + 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 { + 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 HIDDevice for Device { + type BuildParameters = OsString; + type Id = OsString; + + fn new(path: OsString) -> Result { + 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, + protocol: FidoProtocol::CTAP2, + }; + if res.is_u2f() { + info!("new device {:?}", res.path); + Ok(res) + } else { + Err((HIDError::DeviceNotSupported, res.path.clone())) + } + } + + fn id(&self) -> Self::Id { + self.path.clone() + } + + 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 { + 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 FidoDevice for Device { + fn pre_init(&mut self) -> Result<(), HIDError> { + HIDDevice::pre_init(self) + } + + fn should_try_ctap2(&self) -> bool { + HIDDevice::get_device_info(self) + .cap_flags + .contains(Capability::CBOR) + } + + fn initialized(&self) -> bool { + // During successful init, the broadcast channel id gets repplaced by an actual one + self.cid != CID_BROADCAST + } + + fn is_u2f(&mut self) -> bool { + if !uhid::is_u2f_device(self.fd) { + return false; + } + if self.ping().is_err() { + return false; + } + true + } + + fn get_shared_secret(&self) -> Option<&SharedSecret> { + self.secret.as_ref() + } + + fn set_shared_secret(&mut self, secret: SharedSecret) { + 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); + } + + fn get_protocol(&self) -> FidoProtocol { + self.protocol + } + + fn downgrade_to_ctap1(&mut self) { + self.protocol = FidoProtocol::CTAP1; + } +} 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..340ebef836 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/freebsd/monitor.rs @@ -0,0 +1,161 @@ +/* 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::{fs, io}; + +const POLL_TIMEOUT: usize = 100; + +pub enum Event { + Add(OsString), + Remove(OsString), +} + +impl Event { + fn from_devd(event: devd_rs::Event) -> Option { + 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 +where + F: Fn(OsString, Sender, Sender, &dyn Fn() -> bool) + + Sync, +{ + runloops: HashMap, + new_device_cb: Arc, + selector_sender: Sender, + status_sender: Sender, +} + +impl Monitor +where + F: Fn(OsString, Sender, Sender, &dyn Fn() -> bool) + + Send + + Sync + + 'static, +{ + pub fn new( + new_device_cb: F, + selector_sender: Sender, + status_sender: Sender, + ) -> 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> { + 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")?).flatten() { + 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..6b15f6751a --- /dev/null +++ b/third_party/rust/authenticator/src/transport/freebsd/transaction.rs @@ -0,0 +1,69 @@ +/* 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( + timeout: u64, + callback: StateCallback>, + status: Sender, + new_device_cb: F, + ) -> crate::Result + where + F: Fn( + DeviceBuildParameters, + Sender, + Sender, + &dyn Fn() -> bool, + ) + Sync + + Send + + 'static, + T: 'static, + { + let device_selector = DeviceSelector::run(); + 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); + + // 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..681b09a768 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/freebsd/uhid.rs @@ -0,0 +1,89 @@ +/* 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 { + 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 { + let mut desc = GenDescriptor::default(); + let _ = unsafe { usb_get_report_desc(fd, &mut desc)? }; + desc.ugd_maxlen = desc.ugd_actlen; + let mut value = vec![0; 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..339cd05d71 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/hid.rs @@ -0,0 +1,252 @@ +use super::TestDevice; +use crate::consts::{HIDCmd, CID_BROADCAST}; +use crate::ctap2::commands::{CommandError, RequestCtap1, RequestCtap2, Retryable, StatusCode}; +use crate::transport::errors::{ApduErrorStatus, HIDError}; +use crate::transport::{FidoDevice, FidoDeviceIO, FidoProtocol}; +use crate::u2ftypes::{U2FDeviceInfo, U2FHIDCont, U2FHIDInit, U2FHIDInitResp}; +use crate::util::io_err; +use rand::{thread_rng, RngCore}; +use std::cmp::Eq; +use std::fmt; +use std::hash::Hash; +use std::io; +use std::io::{Read, Write}; +use std::thread; +use std::time::Duration; + +pub trait HIDDevice: FidoDevice + Read + Write { + 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; + fn id(&self) -> Self::Id; + + fn get_device_info(&self) -> U2FDeviceInfo; + fn set_device_info(&mut self, dev_info: U2FDeviceInfo); + + // Channel ID management + fn get_cid(&self) -> &[u8; 4]; + fn set_cid(&mut self, cid: [u8; 4]); + + // HID report sizes + fn in_rpt_size(&self) -> usize; + fn out_rpt_size(&self) -> usize; + + fn get_property(&self, prop_name: &str) -> io::Result; + + // Initialize on a protocol-level + fn pre_init(&mut self) -> Result<(), HIDError> { + if self.initialized() { + return Ok(()); + } + + let mut nonce = [0u8; 8]; + thread_rng().fill_bytes(&mut nonce); + + // Send Init to broadcast address to create a new channel + self.set_cid(CID_BROADCAST); + let (cmd, raw) = HIDDevice::sendrecv(self, HIDCmd::Init, &nonce, &|| true)?; + if cmd != HIDCmd::Init { + return Err(HIDError::DeviceError); + } + + let rsp = U2FHIDInitResp::read(&raw, &nonce)?; + // Set 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")); + + let 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, + }; + debug!("{:?}: {:?}", self.id(), info); + self.set_device_info(info); + + // 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], + keep_alive: &dyn Fn() -> bool, + ) -> io::Result<(HIDCmd, Vec)> { + self.u2f_write(cmd.into(), send)?; + debug!("sent to Device {:?} cmd={:?}: {:?}", self.id(), cmd, send); + loop { + let (cmd, data) = self.u2f_read()?; + if cmd != HIDCmd::Keepalive { + debug!( + "got from Device {:?} status={:?}: {:?}", + self.id(), + cmd, + data + ); + return Ok((cmd, data)); + } + // The authenticator might send us HIDCmd::Keepalive messages indefinitely, e.g. if + // it's waiting for user presence. The keep_alive function is used to cancel the + // transaction. + if !keep_alive() { + break; + } + } + + // If this is a CTAP2 device we can tell the authenticator to cancel the transaction on its + // side as well. There's nothing to do for U2F/CTAP1 devices. + if self.get_protocol() == FidoProtocol::CTAP2 { + self.u2f_write(u8::from(HIDCmd::Cancel), &[])?; + } + // For CTAP2 devices we expect to read + // (HIDCmd::Cbor, [CTAP2_ERR_KEEPALIVE_CANCEL]) + // for U2F/CTAP1 we expect to read + // (HIDCmd::Keepalive, [status]). + self.u2f_read() + } + + 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)> { + // 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)) + } +} + +#[cfg(not(test))] +impl TestDevice for T {} + +impl FidoDeviceIO for T { + fn send_msg_cancellable + RequestCtap2>( + &mut self, + msg: &Req, + keep_alive: &dyn Fn() -> bool, + ) -> Result { + if !self.initialized() { + return Err(HIDError::DeviceNotInitialized); + } + + match self.get_protocol() { + FidoProtocol::CTAP1 => self.send_ctap1_cancellable(msg, keep_alive), + FidoProtocol::CTAP2 => self.send_cbor_cancellable(msg, keep_alive), + } + } + + fn send_cbor_cancellable( + &mut self, + msg: &Req, + keep_alive: &dyn Fn() -> bool, + ) -> Result { + debug!("sending {:?} to {:?}", msg, self); + #[cfg(test)] + { + if self.skip_serialization() { + return self.send_ctap2_unserialized(msg); + } + } + + let mut data = msg.wire_format()?; + let mut buf: Vec = Vec::with_capacity(data.len() + 1); + // CTAP2 command + buf.push(msg.command() as u8); + // payload + buf.append(&mut data); + let buf = buf; + + let (cmd, resp) = self.sendrecv(HIDCmd::Cbor, &buf, keep_alive)?; + if cmd == HIDCmd::Cbor { + Ok(msg.handle_response_ctap2(self, &resp)?) + } else { + Err(HIDError::UnexpectedCmd(cmd.into())) + } + } + + fn send_ctap1_cancellable( + &mut self, + msg: &Req, + keep_alive: &dyn Fn() -> bool, + ) -> Result { + debug!("sending {:?} to {:?}", msg, self); + #[cfg(test)] + { + if self.skip_serialization() { + return self.send_ctap1_unserialized(msg); + } + } + let (data, add_info) = msg.ctap1_format()?; + + while keep_alive() { + // sendrecv will not block with a CTAP1 device + let (cmd, mut data) = self.sendrecv(HIDCmd::Msg, &data, &|| true)?; + 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(self, status, &data, &add_info) { + 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())); + } + } + + Err(HIDError::Command(CommandError::StatusCode( + StatusCode::KeepaliveCancel, + None, + ))) + } +} 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..5a0a9d2605 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/hidproto.rs @@ -0,0 +1,257 @@ +/* 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) +)] + +#[cfg(target_os = "linux")] +use std::io; +use std::mem; + +use crate::consts::{FIDO_USAGE_PAGE, FIDO_USAGE_U2FHID}; +#[cfg(target_os = "linux")] +use crate::consts::{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, +} + +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 { + 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::()); + + // 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 { + 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 +} + +#[cfg(target_os = "linux")] +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.is_some() { + 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..6abd462d48 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/device.rs @@ -0,0 +1,181 @@ +/* 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::{Capability, CID_BROADCAST}; +use crate::ctap2::commands::get_info::AuthenticatorInfo; +use crate::transport::hid::HIDDevice; +use crate::transport::platform::{hidraw, monitor}; +use crate::transport::{FidoDevice, FidoProtocol, HIDError, SharedSecret}; +use crate::u2ftypes::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, + secret: Option, + authenticator_info: Option, + protocol: FidoProtocol, +} + +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(&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 { + 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 { + 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 HIDDevice for Device { + type BuildParameters = PathBuf; + type Id = PathBuf; + + fn new(path: PathBuf) -> Result { + 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, + protocol: FidoProtocol::CTAP2, + }; + if res.is_u2f() { + info!("new device {:?}", res.path); + Ok(res) + } else { + Err((HIDError::DeviceNotSupported, res.path)) + } + } + + fn id(&self) -> Self::Id { + self.path.clone() + } + + 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 { + 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 FidoDevice for Device { + fn pre_init(&mut self) -> Result<(), HIDError> { + HIDDevice::pre_init(self) + } + + fn should_try_ctap2(&self) -> bool { + HIDDevice::get_device_info(self) + .cap_flags + .contains(Capability::CBOR) + } + + fn initialized(&self) -> bool { + // During successful init, the broadcast channel id gets replaced by an actual one + self.cid != CID_BROADCAST + } + + fn is_u2f(&mut self) -> bool { + hidraw::is_u2f_device(self.fd.as_raw_fd()) + } + + fn get_shared_secret(&self) -> Option<&SharedSecret> { + self.secret.as_ref() + } + + fn set_shared_secret(&mut self, secret: SharedSecret) { + 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); + } + + fn get_protocol(&self) -> FidoProtocol { + self.protocol + } + + fn downgrade_to_ctap1(&mut self) { + self.protocol = FidoProtocol::CTAP1; + } +} 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 { + from_unix_result(libc::ioctl(fd, _HIDIOCGRDESCSIZE as IocType, val)) +} + +pub unsafe fn hidiocgrdesc( + fd: libc::c_int, + val: *mut LinuxReportDescriptor, +) -> io::Result { + 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 { + 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 +#include + +/* 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..bc8582c5b1 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/hidwrapper.rs @@ -0,0 +1,54 @@ +#![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"); + +#[cfg(all(target_arch = "loongarch64", target_endian = "little"))] +include!("ioctl_loongarch64.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_loongarch64.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_loongarch64.rs new file mode 100644 index 0000000000..a784e9bf46 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/ioctl_loongarch64.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..ee88622de9 --- /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((fds[..]).as_mut_ptr(), nfds, POLL_TIMEOUT) }; + + if rv < 0 { + Err(io::Error::from_raw_os_error(rv)) + } else { + Ok(()) + } +} + +pub struct Monitor +where + F: Fn(PathBuf, Sender, Sender, &dyn Fn() -> bool) + + Sync, +{ + runloops: HashMap, + new_device_cb: Arc, + selector_sender: Sender, + status_sender: Sender, +} + +impl Monitor +where + F: Fn(PathBuf, Sender, Sender, &dyn Fn() -> bool) + + Send + + Sync + + 'static, +{ + pub fn new( + new_device_cb: F, + selector_sender: Sender, + status_sender: Sender, + ) -> 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> { + let ctx = libudev::Context::new()?; + + let mut enumerator = libudev::Enumerator::new(&ctx)?; + enumerator.match_subsystem(UDEV_SUBSYSTEM)?; + + // Iterate all existing devices. + let paths: Vec = 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 { + 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..6b15f6751a --- /dev/null +++ b/third_party/rust/authenticator/src/transport/linux/transaction.rs @@ -0,0 +1,69 @@ +/* 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( + timeout: u64, + callback: StateCallback>, + status: Sender, + new_device_cb: F, + ) -> crate::Result + where + F: Fn( + DeviceBuildParameters, + Sender, + Sender, + &dyn Fn() -> bool, + ) + Sync + + Send + + 'static, + T: 'static, + { + let device_selector = DeviceSelector::run(); + 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); + + // 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..9acce3aa29 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/macos/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 log; + +use crate::consts::{Capability, CID_BROADCAST, MAX_HID_RPT_SIZE}; +use crate::ctap2::commands::get_info::AuthenticatorInfo; +use crate::transport::hid::HIDDevice; +use crate::transport::platform::iokit::*; +use crate::transport::{FidoDevice, FidoProtocol, HIDError, SharedSecret}; +use crate::u2ftypes::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>>, + dev_info: Option, + secret: Option, + authenticator_info: Option, + protocol: FidoProtocol, +} + +impl Device { + unsafe fn get_property_macos(&self, prop_name: &str) -> io::Result { + 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(&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 { + 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 { + 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 HIDDevice for Device { + type BuildParameters = (IOHIDDeviceRef, Receiver>); + type Id = IOHIDDeviceRef; + + fn new(dev_ids: Self::BuildParameters) -> Result { + 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, + protocol: FidoProtocol::CTAP2, + }) + } + + fn id(&self) -> Self::Id { + self.device_ref + } + + 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 { + 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 FidoDevice for Device { + fn pre_init(&mut self) -> Result<(), HIDError> { + HIDDevice::pre_init(self) + } + + fn should_try_ctap2(&self) -> bool { + HIDDevice::get_device_info(self) + .cap_flags + .contains(Capability::CBOR) + } + + fn initialized(&self) -> bool { + self.cid != CID_BROADCAST + } + fn is_u2f(&mut self) -> bool { + true + } + fn get_shared_secret(&self) -> Option<&SharedSecret> { + self.secret.as_ref() + } + + fn set_shared_secret(&mut self, secret: SharedSecret) { + 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); + } + + fn get_protocol(&self) -> FidoProtocol { + self.protocol + } + + fn downgrade_to_ctap1(&mut self) { + self.protocol = FidoProtocol::CTAP1; + } +} 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 *const c_void>, + pub release: Option, + pub copyDescription: Option 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, +} + +impl IOHIDDeviceMatcher { + pub fn new() -> Self { + let dict = CFDictionary::::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 = 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..32200ee7a4 --- /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>, + runloop: RunLoop, +} + +pub struct Monitor +where + F: Fn( + (IOHIDDeviceRef, Receiver>), + Sender, + Sender, + &dyn Fn() -> bool, + ) + Send + + Sync + + 'static, +{ + manager: IOHIDManagerRef, + // Keep alive until the monitor goes away. + _matcher: IOHIDDeviceMatcher, + map: HashMap, + new_device_cb: F, + selector_sender: Sender, + status_sender: Sender, +} + +impl Monitor +where + F: Fn( + (IOHIDDeviceRef, Receiver>), + Sender, + Sender, + &dyn Fn() -> bool, + ) + Send + + Sync + + 'static, +{ + pub fn new( + new_device_cb: F, + selector_sender: Sender, + status_sender: Sender, + ) -> 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::::on_device_matching, + context, + ); + IOHIDManagerRegisterDeviceRemovalCallback( + self.manager, + Monitor::::on_device_removal, + context, + ); + IOHIDManagerRegisterInputReportCallback( + self.manager, + Monitor::::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 { 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 Drop for Monitor +where + F: Fn( + (IOHIDDeviceRef, Receiver>), + Sender, + Sender, + &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..d9709e7364 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/macos/transaction.rs @@ -0,0 +1,107 @@ +/* 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, + thread: Option>, + device_selector: DeviceSelector, +} + +impl Transaction { + pub fn new( + timeout: u64, + callback: StateCallback>, + status: Sender, + new_device_cb: F, + ) -> crate::Result + where + F: Fn( + DeviceBuildParameters, + Sender, + Sender, + &dyn Fn() -> bool, + ) + Sync + + Send + + 'static, + T: 'static, + { + let (tx, rx) = channel(); + let timeout = (timeout as f64) / 1000.0; + let device_selector = DeviceSelector::run(); + 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); + 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 = 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..ac4e156d8a --- /dev/null +++ b/third_party/rust/authenticator/src/transport/mock/device.rs @@ -0,0 +1,328 @@ +/* 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, HIDCmd, CID_BROADCAST}; +use crate::crypto::SharedSecret; +use crate::ctap2::commands::get_info::AuthenticatorInfo; +use crate::ctap2::commands::{CtapResponse, RequestCtap1, RequestCtap2}; +use crate::transport::device_selector::DeviceCommand; +use crate::transport::TestDevice; +use crate::transport::{hid::HIDDevice, FidoDevice, FidoProtocol, HIDError}; +use crate::u2ftypes::{U2FDeviceInfo, U2FHIDInitResp}; +use std::any::Any; +use std::collections::VecDeque; +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, + pub authenticator_info: Option, + pub sender: Option>, + pub receiver: Option>, + pub protocol: FidoProtocol, + skip_serialization: bool, + pub upcoming_requests: VecDeque>, + pub upcoming_responses: VecDeque, HIDError>>, +} + +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 add_upcoming_ctap2_request(&mut self, msg: &impl RequestCtap2) { + self.upcoming_requests + .push_back(msg.wire_format().expect("Failed to serialize CTAP request")); + } + + pub fn add_upcoming_ctap1_request(&mut self, msg: &impl RequestCtap1) { + let (upcoming, _) = msg + .ctap1_format() + .expect("Failed to serialize CTAP request"); + self.upcoming_requests.push_back(upcoming); + } + + pub fn add_upcoming_ctap_response(&mut self, msg: impl CtapResponse) { + self.upcoming_responses.push_back(Ok(Box::new(msg))); + } + + pub fn add_upcoming_ctap_error(&mut self, msg: HIDError) { + self.upcoming_responses.push_back(Err(msg)); + } + + pub fn create_channel(&mut self) { + let (tx, rx) = channel(); + self.sender = Some(tx); + self.receiver = Some(rx); + } + + pub fn new_skipping_serialization(id: &str) -> Result { + Ok(Device { + id: id.to_string(), + cid: CID_BROADCAST, + reads: vec![], + writes: vec![], + dev_info: None, + authenticator_info: None, + sender: None, + receiver: None, + protocol: FidoProtocol::CTAP2, + skip_serialization: true, + upcoming_requests: VecDeque::new(), + upcoming_responses: VecDeque::new(), + }) + } +} + +impl Write for Device { + fn write(&mut self, bytes: &[u8]) -> io::Result { + // 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 { + 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(&self, state: &mut H) { + self.id.hash(state); + } +} + +impl HIDDevice for Device { + type Id = String; + type BuildParameters = &'static str; // None used + + fn id(&self) -> Self::Id { + self.id.clone() + } + + fn new(id: Self::BuildParameters) -> Result { + Ok(Device { + id: id.to_string(), + cid: CID_BROADCAST, + reads: vec![], + writes: vec![], + dev_info: None, + authenticator_info: None, + sender: None, + receiver: None, + protocol: FidoProtocol::CTAP2, + skip_serialization: false, + upcoming_requests: VecDeque::new(), + upcoming_responses: VecDeque::new(), + }) + } + + 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 { + IN_HID_RPT_SIZE + } + + fn out_rpt_size(&self) -> usize { + OUT_HID_RPT_SIZE + } + + fn get_property(&self, prop_name: &str) -> io::Result { + Ok(format!("{prop_name} not implemented")) + } + + 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); + } + + fn pre_init(&mut self) -> Result<(), HIDError> { + if self.initialized() { + return Ok(()); + } + + let nonce = [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]; + + // Send Init to broadcast address to create a new channel + self.set_cid(CID_BROADCAST); + let (cmd, raw) = HIDDevice::sendrecv(self, HIDCmd::Init, &nonce, &|| true)?; + if cmd != HIDCmd::Init { + return Err(HIDError::DeviceError); + } + + let rsp = U2FHIDInitResp::read(&raw, &nonce)?; + + // Set the new Channel ID + self.set_cid(rsp.cid); + + let info = U2FDeviceInfo { + vendor_name: "Test vendor".as_bytes().to_vec(), + device_name: "Test device".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, + }; + debug!("{:?}: {:?}", self.id(), info); + self.set_device_info(info); + + Ok(()) + } +} + +impl TestDevice for Device { + fn skip_serialization(&self) -> bool { + self.skip_serialization + } + + fn send_ctap1_unserialized( + &mut self, + msg: &Req, + ) -> Result { + let expected = self + .upcoming_requests + .pop_front() + .expect("No expected CTAP1 command left"); + let (incoming, _) = msg.ctap1_format().expect("Can't serialize CTAP1 request"); + assert_eq!(expected, incoming); + let response = self + .upcoming_responses + .pop_front() + .expect("No response given!"); + response.map(|x| { + *x.downcast() + .expect("Failed to downcast given CTAP response") + }) + } + + fn send_ctap2_unserialized( + &mut self, + msg: &Req, + ) -> Result { + let expected = self + .upcoming_requests + .pop_front() + .expect("No expected CTAP2 command left"); + let incoming = msg.wire_format().expect("Can't serialize CTAP2 request"); + assert_eq!(expected, incoming); + let response = self + .upcoming_responses + .pop_front() + .expect("No response given!"); + response.map(|x| { + *x.downcast() + .expect("Failed to downcast given CTAP response") + }) + } +} + +impl FidoDevice for Device { + fn pre_init(&mut self) -> Result<(), HIDError> { + HIDDevice::pre_init(self) + } + + fn should_try_ctap2(&self) -> bool { + HIDDevice::get_device_info(self) + .cap_flags + .contains(Capability::CBOR) + } + + fn initialized(&self) -> bool { + self.get_cid() != &CID_BROADCAST + } + + fn is_u2f(&mut self) -> bool { + self.sender.is_some() + } + + fn get_shared_secret(&self) -> std::option::Option<&SharedSecret> { + None + } + + fn set_shared_secret(&mut self, _: SharedSecret) { + // Nothing + } + + 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 get_protocol(&self) -> FidoProtocol { + self.protocol + } + + fn downgrade_to_ctap1(&mut self) { + self.protocol = FidoProtocol::CTAP1; + } +} 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( + _timeout: u64, + _callback: StateCallback>, + _status: Sender, + _new_device_cb: F, + ) -> crate::Result + where + F: Fn( + DeviceBuildParameters, + Sender, + Sender, + &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..318934ed36 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/mod.rs @@ -0,0 +1,369 @@ +use crate::crypto::{PinUvAuthProtocol, PinUvAuthToken, SharedSecret}; +use crate::ctap2::commands::client_pin::{ + ClientPIN, ClientPinResponse, GetKeyAgreement, GetPinToken, + GetPinUvAuthTokenUsingPinWithPermissions, GetPinUvAuthTokenUsingUvWithPermissions, + PinUvAuthTokenPermission, +}; +use crate::ctap2::commands::get_assertion::{GetAssertion, GetAssertionResult}; +use crate::ctap2::commands::get_info::{AuthenticatorInfo, AuthenticatorVersion, GetInfo}; +use crate::ctap2::commands::get_version::{GetVersion, U2FInfo}; +use crate::ctap2::commands::make_credentials::{ + dummy_make_credentials_cmd, MakeCredentials, MakeCredentialsResult, +}; +use crate::ctap2::commands::reset::Reset; +use crate::ctap2::commands::selection::Selection; +use crate::ctap2::commands::{CommandError, RequestCtap1, RequestCtap2, StatusCode}; +use crate::ctap2::preflight::CheckKeyHandle; +use crate::transport::device_selector::BlinkResult; +use crate::transport::errors::HIDError; + +use crate::Pin; +use std::convert::TryFrom; +use std::fmt; + +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, Clone, Copy, PartialEq, Eq)] +pub enum FidoProtocol { + CTAP1, + CTAP2, +} + +pub trait FidoDeviceIO { + fn send_msg + RequestCtap2>( + &mut self, + msg: &Req, + ) -> Result { + self.send_msg_cancellable(msg, &|| true) + } + + fn send_cbor(&mut self, msg: &Req) -> Result { + self.send_cbor_cancellable(msg, &|| true) + } + + fn send_ctap1(&mut self, msg: &Req) -> Result { + self.send_ctap1_cancellable(msg, &|| true) + } + + fn send_msg_cancellable + RequestCtap2>( + &mut self, + msg: &Req, + keep_alive: &dyn Fn() -> bool, + ) -> Result; + + fn send_cbor_cancellable( + &mut self, + msg: &Req, + keep_alive: &dyn Fn() -> bool, + ) -> Result; + + fn send_ctap1_cancellable( + &mut self, + msg: &Req, + keep_alive: &dyn Fn() -> bool, + ) -> Result; +} + +pub trait TestDevice { + #[cfg(test)] + fn skip_serialization(&self) -> bool; + #[cfg(test)] + fn send_ctap1_unserialized( + &mut self, + msg: &Req, + ) -> Result; + #[cfg(test)] + fn send_ctap2_unserialized( + &mut self, + msg: &Req, + ) -> Result; +} + +pub trait FidoDevice: FidoDeviceIO +where + Self: Sized, + Self: fmt::Debug, +{ + fn pre_init(&mut self) -> Result<(), HIDError>; + fn initialized(&self) -> bool; + + // Check if the device is actually a token + fn is_u2f(&mut self) -> bool; + fn should_try_ctap2(&self) -> bool; + fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo>; + fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo); + fn refresh_authenticator_info(&mut self) -> Option<&AuthenticatorInfo> { + let command = GetInfo::default(); + if let Ok(info) = self.send_cbor(&command) { + debug!("Refreshed authenticator info: {:?}", info); + self.set_authenticator_info(info); + } + self.get_authenticator_info() + } + + // `get_protocol()` indicates whether we're using CTAP1 or CTAP2. + // Prior to initializing the device, `get_protocol()` should return CTAP2 unless + // there's a reason to believe that the device does not support CTAP2 (e.g. if + // it's a HID device and it does not have the CBOR capability). + fn get_protocol(&self) -> FidoProtocol; + + // We do not provide a generic `set_protocol(..)` function as this would have complicated + // interactions with the AuthenticatorInfo state. + fn downgrade_to_ctap1(&mut self); + + fn get_shared_secret(&self) -> Option<&SharedSecret>; + fn set_shared_secret(&mut self, secret: SharedSecret); + + fn init(&mut self) -> Result<(), HIDError> { + self.pre_init()?; + + if self.should_try_ctap2() { + let command = GetInfo::default(); + if let Ok(info) = self.send_cbor(&command) { + debug!("{:?}", info); + if info.max_supported_version() == AuthenticatorVersion::U2F_V2 { + self.downgrade_to_ctap1(); + } + self.set_authenticator_info(info); + return Ok(()); + } + } + + self.downgrade_to_ctap1(); + // We want to return an error here if this device doesn't support CTAP1, + // so we send a U2F_VERSION command. + let command = GetVersion::default(); + self.send_ctap1(&command)?; + Ok(()) + } + + fn block_and_blink(&mut self, keep_alive: &dyn Fn() -> bool) -> BlinkResult { + let supports_select_cmd = self.get_protocol() == FidoProtocol::CTAP2 + && self.get_authenticator_info().map_or(false, |i| { + i.versions.contains(&AuthenticatorVersion::FIDO_2_1) + }); + let resp = if supports_select_cmd { + let msg = Selection {}; + self.send_cbor_cancellable(&msg, keep_alive) + } 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 msg = dummy_make_credentials_cmd(); + info!("Trying to blink: {:?}", &msg); + // We don't care about the Ok-value, just if it is Ok or not + self.send_msg_cancellable(&msg, keep_alive).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 establish_shared_secret( + &mut self, + alive: &dyn Fn() -> bool, + ) -> Result { + // CTAP1 devices don't support establishing a shared secret + let info = match (self.get_protocol(), self.get_authenticator_info()) { + (FidoProtocol::CTAP2, Some(info)) => info, + _ => return Err(HIDError::UnsupportedCommand), + }; + + let pin_protocol = PinUvAuthProtocol::try_from(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(pin_protocol.clone()); + let resp = self.send_cbor_cancellable(&pin_command, alive)?; + if let Some(device_key_agreement_key) = resp.key_agreement { + let shared_secret = pin_protocol + .encapsulate(&device_key_agreement_key) + .map_err(CommandError::from)?; + self.set_shared_secret(shared_secret.clone()); + Ok(shared_secret) + } else { + Err(HIDError::Command(CommandError::MissingRequiredField( + "key_agreement", + ))) + } + } + + /// CTAP 2.0-only version: + /// "Getting pinUvAuthToken using getPinToken (superseded)" + fn get_pin_token( + &mut self, + pin: &Option, + alive: &dyn Fn() -> bool, + ) -> Result { + // Asking the user for PIN before establishing the shared secret + let pin = pin + .as_ref() + .ok_or(CommandError::StatusCode(StatusCode::PinRequired, None))?; + + // 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 = self.establish_shared_secret(alive)?; + + let pin_command = GetPinToken::new(&shared_secret, pin); + let resp = self.send_cbor_cancellable(&pin_command, alive)?; + if let Some(encrypted_pin_token) = resp.pin_token { + // CTAP 2.1 spec: + // If authenticatorClientPIN's getPinToken subcommand is invoked, default permissions + // of `mc` and `ga` (value 0x03) are granted for the returned pinUvAuthToken. + let default_permissions = PinUvAuthTokenPermission::default(); + let pin_token = shared_secret + .decrypt_pin_token(default_permissions, encrypted_pin_token.as_ref()) + .map_err(CommandError::from)?; + Ok(pin_token) + } else { + Err(HIDError::Command(CommandError::MissingRequiredField( + "pin_token", + ))) + } + } + + fn get_pin_uv_auth_token_using_uv_with_permissions( + &mut self, + permission: PinUvAuthTokenPermission, + rp_id: Option<&String>, + alive: &dyn Fn() -> bool, + ) -> Result { + // Explicitly not reusing the shared secret here + let shared_secret = self.establish_shared_secret(alive)?; + let pin_command = GetPinUvAuthTokenUsingUvWithPermissions::new( + &shared_secret, + permission, + rp_id.cloned(), + ); + + let resp = self.send_cbor_cancellable(&pin_command, alive)?; + + if let Some(encrypted_pin_token) = resp.pin_token { + let pin_token = shared_secret + .decrypt_pin_token(permission, encrypted_pin_token.as_ref()) + .map_err(CommandError::from)?; + Ok(pin_token) + } else { + Err(HIDError::Command(CommandError::MissingRequiredField( + "pin_token", + ))) + } + } + + fn get_pin_uv_auth_token_using_pin_with_permissions( + &mut self, + pin: &Option, + permission: PinUvAuthTokenPermission, + rp_id: Option<&String>, + alive: &dyn Fn() -> bool, + ) -> Result { + // Asking the user for PIN before establishing the shared secret + let pin = pin + .as_ref() + .ok_or(CommandError::StatusCode(StatusCode::PinRequired, None))?; + + // 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 = self.establish_shared_secret(alive)?; + let pin_command = GetPinUvAuthTokenUsingPinWithPermissions::new( + &shared_secret, + pin, + permission, + rp_id.cloned(), + ); + + let resp = self.send_cbor_cancellable(&pin_command, alive)?; + + if let Some(encrypted_pin_token) = resp.pin_token { + let pin_token = shared_secret + .decrypt_pin_token(permission, encrypted_pin_token.as_ref()) + .map_err(CommandError::from)?; + Ok(pin_token) + } else { + Err(HIDError::Command(CommandError::MissingRequiredField( + "pin_token", + ))) + } + } +} + +pub trait VirtualFidoDevice: FidoDevice { + fn check_key_handle(&self, req: &CheckKeyHandle) -> Result<(), HIDError>; + fn client_pin(&self, req: &ClientPIN) -> Result; + fn get_assertion(&self, req: &GetAssertion) -> Result, HIDError>; + fn get_info(&self) -> Result; + fn get_version(&self, req: &GetVersion) -> Result; + fn make_credentials(&self, req: &MakeCredentials) -> Result; + fn reset(&self, req: &Reset) -> Result<(), HIDError>; + fn selection(&self, req: &Selection) -> Result<(), HIDError>; +} 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..4b426b3b9b --- /dev/null +++ b/third_party/rust/authenticator/src/transport/netbsd/device.rs @@ -0,0 +1,248 @@ +/* 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::{Capability, CID_BROADCAST, MAX_HID_RPT_SIZE}; +use crate::ctap2::commands::get_info::AuthenticatorInfo; +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::{FidoDevice, FidoProtocol, HIDError, SharedSecret}; +use crate::u2ftypes::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, + secret: Option, + authenticator_info: Option, + protocol: FidoProtocol, +} + +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 + + // Write ping request. Each write to the device contains + // exactly one report id byte[*] followed by exactly as + // many bytes as are in a report, and will be consumed all + // at once by /dev/uhidN. So we use plain write, not + // write_all to issue writes in a loop. + // + // [*] This is only for the internal authenticator-rs API, + // not for the USB HID protocol, which for a device with + // only one report id excludes the report id byte from the + // interrupt in/out pipe transfer format. + if self.write(&buf)? != buf.len() { + return Err(io_err("write ping failed")); + } + + // 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. When reports come in they are all + // exactly the same size, with no report id byte because + // there is only one report. + let n = self.read(&mut buf[1..])?; + if n != buf.len() - 1 { + return Err(io_err("read pong failed")); + } + + 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(&self, state: &mut H) { + self.fd.hash(state); + } +} + +impl Read for Device { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + 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 { + // 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 HIDDevice for Device { + type BuildParameters = WrappedOpenDevice; + type Id = OsString; + + fn new(fido: WrappedOpenDevice) -> Result { + 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, + protocol: FidoProtocol::CTAP2, + }; + if res.is_u2f() { + info!("new device {:?}", res.path); + Ok(res) + } else { + Err((HIDError::DeviceNotSupported, res.path.clone())) + } + } + + fn id(&self) -> Self::Id { + self.path.clone() + } + + 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 { + 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 FidoDevice for Device { + fn pre_init(&mut self) -> Result<(), HIDError> { + HIDDevice::pre_init(self) + } + + fn should_try_ctap2(&self) -> bool { + HIDDevice::get_device_info(self) + .cap_flags + .contains(Capability::CBOR) + } + + fn initialized(&self) -> bool { + // During successful init, the broadcast channel id gets repplaced by an actual one + self.cid != CID_BROADCAST + } + + 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 self.ping().is_err() { + return false; + } + true + } + + fn get_shared_secret(&self) -> Option<&SharedSecret> { + self.secret.as_ref() + } + + fn set_shared_secret(&mut self, secret: SharedSecret) { + 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); + } + + fn get_protocol(&self) -> FidoProtocol { + self.protocol + } + + fn downgrade_to_ctap1(&mut self) { + self.protocol = FidoProtocol::CTAP1; + } +} 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 { + 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(&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..c521bdea8b --- /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 +where + F: Fn( + WrappedOpenDevice, + Sender, + Sender, + &dyn Fn() -> bool, + ) + Sync, +{ + runloops: HashMap, + new_device_cb: Arc, + selector_sender: Sender, + status_sender: Sender, +} + +impl Monitor +where + F: Fn( + WrappedOpenDevice, + Sender, + Sender, + &dyn Fn() -> bool, + ) + Send + + Sync + + 'static, +{ + pub fn new( + new_device_cb: F, + selector_sender: Sender, + status_sender: Sender, + ) -> 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> { + // 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..6b15f6751a --- /dev/null +++ b/third_party/rust/authenticator/src/transport/netbsd/transaction.rs @@ -0,0 +1,69 @@ +/* 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( + timeout: u64, + callback: StateCallback>, + status: Sender, + new_device_cb: F, + ) -> crate::Result + where + F: Fn( + DeviceBuildParameters, + Sender, + Sender, + &dyn Fn() -> bool, + ) + Sync + + Send + + 'static, + T: 'static, + { + let device_selector = DeviceSelector::run(); + 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); + + // 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 { + 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 { + 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..f11ded7984 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/openbsd/device.rs @@ -0,0 +1,224 @@ +/* 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::{Capability, CID_BROADCAST, MAX_HID_RPT_SIZE}; +use crate::ctap2::commands::get_info::AuthenticatorInfo; +use crate::transport::hid::HIDDevice; +use crate::transport::platform::monitor::WrappedOpenDevice; +use crate::transport::{FidoDevice, FidoProtocol, HIDError, SharedSecret}; +use crate::u2ftypes::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, + secret: Option, + authenticator_info: Option, + protocol: FidoProtocol, +} + +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(&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 { + 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 { + // 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 HIDDevice for Device { + type BuildParameters = WrappedOpenDevice; + type Id = OsString; + + fn new(fido: WrappedOpenDevice) -> Result { + 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, + protocol: FidoProtocol::CTAP2, + }; + if res.is_u2f() { + info!("new device {:?}", res.path); + Ok(res) + } else { + Err((HIDError::DeviceNotSupported, res.path.clone())) + } + } + + fn id(&self) -> Self::Id { + self.path.clone() + } + + 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 { + 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 FidoDevice for Device { + fn pre_init(&mut self) -> Result<(), HIDError> { + HIDDevice::pre_init(self) + } + + fn should_try_ctap2(&self) -> bool { + HIDDevice::get_device_info(self) + .cap_flags + .contains(Capability::CBOR) + } + + fn initialized(&self) -> bool { + // During successful init, the broadcast channel id gets repplaced by an actual one + self.cid != CID_BROADCAST + } + + 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<&SharedSecret> { + self.secret.as_ref() + } + + fn set_shared_secret(&mut self, secret: SharedSecret) { + 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); + } + + fn get_protocol(&self) -> FidoProtocol { + self.protocol + } + + fn downgrade_to_ctap1(&mut self) { + self.protocol = FidoProtocol::CTAP1; + } +} 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 +where + F: Fn( + WrappedOpenDevice, + Sender, + Sender, + &dyn Fn() -> bool, + ) + Sync, +{ + runloops: HashMap, + new_device_cb: Arc, + selector_sender: Sender, + status_sender: Sender, +} + +impl Monitor +where + F: Fn( + WrappedOpenDevice, + Sender, + Sender, + &dyn Fn() -> bool, + ) + Send + + Sync + + 'static, +{ + pub fn new( + new_device_cb: F, + selector_sender: Sender, + status_sender: Sender, + ) -> 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> { + // 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..6b15f6751a --- /dev/null +++ b/third_party/rust/authenticator/src/transport/openbsd/transaction.rs @@ -0,0 +1,69 @@ +/* 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( + timeout: u64, + callback: StateCallback>, + status: Sender, + new_device_cb: F, + ) -> crate::Result + where + F: Fn( + DeviceBuildParameters, + Sender, + Sender, + &dyn Fn() -> bool, + ) + Sync + + Send + + 'static, + T: 'static, + { + let device_selector = DeviceSelector::run(); + 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); + + // 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..29d8a3ab9b --- /dev/null +++ b/third_party/rust/authenticator/src/transport/stub/device.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::ctap2::commands::get_info::AuthenticatorInfo; +use crate::transport::hid::HIDDevice; +use crate::transport::{FidoDevice, FidoProtocol}; +use crate::transport::{HIDError, SharedSecret}; +use crate::u2ftypes::U2FDeviceInfo; +use std::hash::Hash; +use std::io; +use std::io::{Read, Write}; +use std::path::PathBuf; + +#[derive(Debug, Hash, PartialEq, Eq)] +pub struct Device {} + +impl Read for Device { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + unimplemented!(); + } +} + +impl Write for Device { + fn write(&mut self, buf: &[u8]) -> io::Result { + unimplemented!(); + } + + fn flush(&mut self) -> io::Result<()> { + unimplemented!(); + } +} + +impl HIDDevice for Device { + type BuildParameters = PathBuf; + type Id = PathBuf; + + fn new(parameters: Self::BuildParameters) -> Result { + unimplemented!(); + } + + fn id(&self) -> Self::Id { + unimplemented!() + } + + fn get_cid(&self) -> &[u8; 4] { + unimplemented!(); + } + + fn set_cid(&mut self, cid: [u8; 4]) { + unimplemented!(); + } + + fn in_rpt_size(&self) -> usize { + unimplemented!(); + } + + fn out_rpt_size(&self) -> usize { + unimplemented!(); + } + + fn get_property(&self, prop_name: &str) -> io::Result { + unimplemented!(); + } + + fn get_device_info(&self) -> U2FDeviceInfo { + unimplemented!(); + } + + fn set_device_info(&mut self, dev_info: U2FDeviceInfo) { + unimplemented!(); + } +} + +impl FidoDevice for Device { + fn pre_init(&mut self) -> Result<(), HIDError> { + unimplemented!(); + } + + fn should_try_ctap2(&self) -> bool { + unimplemented!(); + } + + fn initialized(&self) -> bool { + 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: SharedSecret) { + unimplemented!() + } + + fn get_shared_secret(&self) -> Option<&SharedSecret> { + unimplemented!() + } + + fn get_protocol(&self) -> FidoProtocol { + unimplemented!() + } + + fn downgrade_to_ctap1(&mut self) { + unimplemented!() + } +} 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..d471c94da8 --- /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( + timeout: u64, + callback: StateCallback>, + _status: Sender, + new_device_cb: F, + ) -> crate::Result + where + F: Fn( + DeviceBuildParameters, + Sender, + Sender, + &dyn Fn() -> bool, + ) + Sync + + Send + + 'static, + T: 'static, + { + // Just to silence "unused"-warnings + let mut device_selector = DeviceSelector::run(); + 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..bda5d385cc --- /dev/null +++ b/third_party/rust/authenticator/src/transport/windows/device.rs @@ -0,0 +1,173 @@ +/* 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::{ + Capability, CID_BROADCAST, FIDO_USAGE_PAGE, FIDO_USAGE_U2FHID, MAX_HID_RPT_SIZE, +}; +use crate::ctap2::commands::get_info::AuthenticatorInfo; +use crate::transport::hid::HIDDevice; +use crate::transport::{FidoDevice, FidoProtocol, HIDError, SharedSecret}; +use crate::u2ftypes::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, + secret: Option, + authenticator_info: Option, + protocol: FidoProtocol, +} + +impl PartialEq for Device { + fn eq(&self, other: &Device) -> bool { + self.path == other.path + } +} + +impl Eq for Device {} + +impl Hash for Device { + fn hash(&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 { + // 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()) + } +} + +impl Write for Device { + fn write(&mut self, bytes: &[u8]) -> io::Result { + self.file.write(bytes) + } + + fn flush(&mut self) -> io::Result<()> { + self.file.flush() + } +} + +impl HIDDevice for Device { + type BuildParameters = String; + type Id = String; + + fn new(path: String) -> Result { + 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, + protocol: FidoProtocol::CTAP2, + }; + if res.is_u2f() { + info!("new device {:?}", res.path); + Ok(res) + } else { + Err((HIDError::DeviceNotSupported, res.path)) + } + } + + fn id(&self) -> Self::Id { + self.path.clone() + } + 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 { + 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 FidoDevice for Device { + fn pre_init(&mut self) -> Result<(), HIDError> { + HIDDevice::pre_init(self) + } + + fn should_try_ctap2(&self) -> bool { + HIDDevice::get_device_info(self) + .cap_flags + .contains(Capability::CBOR) + } + + fn initialized(&self) -> bool { + // During successful init, the broadcast channel id gets repplaced by an actual one + self.cid != CID_BROADCAST + } + + 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<&SharedSecret> { + self.secret.as_ref() + } + + fn set_shared_secret(&mut self, secret: SharedSecret) { + 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); + } + + fn get_protocol(&self) -> FidoProtocol { + self.protocol + } + + fn downgrade_to_ctap1(&mut self) { + self.protocol = FidoProtocol::CTAP1; + } +} 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..c73c012b05 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/windows/monitor.rs @@ -0,0 +1,125 @@ +/* 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::{DeviceID, 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 +where + F: Fn(String, Sender, Sender, &dyn Fn() -> bool) + + Sync, +{ + runloops: HashMap, + new_device_cb: Arc, + selector_sender: Sender, + status_sender: Sender, +} + +impl Monitor +where + F: Fn(String, Sender, Sender, &dyn Fn() -> bool) + + Send + + Sync + + 'static, +{ + pub fn new( + new_device_cb: F, + selector_sender: Sender, + status_sender: Sender, + ) -> 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> { + let mut current = HashSet::new(); + let mut previous; + + while alive() { + let device_info_set = DeviceInfoSet::new()?; + previous = current; + current = HashSet::from_iter(device_info_set.devices()); + + // Remove devices that are gone. + for path in previous.difference(¤t) { + self.remove_device(path); + } + + let added: Vec = current.difference(&previous).cloned().collect(); + + // We have to notify additions in batches to avoid + // arbitrarily selecting the first added device. + if !added.is_empty() + && self + .selector_sender + .send(DeviceSelectorEvent::DevicesAdded(added.clone())) + .is_err() + { + // Send only fails if the receiver hung up. We should exit the loop. + break; + } + + // Add devices that were plugged in. + for path in added { + self.add_device(&path); + } + + // 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: &DeviceID) { + 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: &DeviceID) { + 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..6b15f6751a --- /dev/null +++ b/third_party/rust/authenticator/src/transport/windows/transaction.rs @@ -0,0 +1,69 @@ +/* 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( + timeout: u64, + callback: StateCallback>, + status: Sender, + new_device_cb: F, + ) -> crate::Result + where + F: Fn( + DeviceBuildParameters, + Sender, + Sender, + &dyn Fn() -> bool, + ) + Sync + + Send + + 'static, + T: 'static, + { + let device_selector = DeviceSelector::run(); + 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); + + // 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..44b4489811 --- /dev/null +++ b/third_party/rust/authenticator/src/transport/windows/winapi.rs @@ -0,0 +1,263 @@ +/* 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 { + 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 { + let mut device_interface_data = + mem::MaybeUninit::::zeroed(); + unsafe { + (*device_interface_data.as_mut_ptr()).cbSize = + mem::size_of::() 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)?; + 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 { + let mut cb_size = mem::size_of::(); + 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(ptr::addr_of!((*self.data).DevicePath[0]), 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 { + 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::::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/u2ftypes.rs b/third_party/rust/authenticator/src/u2ftypes.rs new file mode 100644 index 0000000000..0237f3acb7 --- /dev/null +++ b/third_party/rust/authenticator/src/u2ftypes.rs @@ -0,0 +1,341 @@ +/* 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::transport::hid::HIDDevice; +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 = data.iter().map(|byte| format!("{byte:02x}")).collect(); + parts.join(joiner) +} + +pub fn trace_hex(data: &[u8]) { + if log_enabled!(log::Level::Trace) { + trace!("USB send: {}", to_hex(data, "")); + } +} + +// 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(dev: &mut T) -> io::Result<(HIDCmd, Vec)> { + 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 = if dev.in_rpt_size() >= INIT_HEADER_SIZE { + cmp::min(cap, dev.in_rpt_size() - INIT_HEADER_SIZE) + } else { + cap + }; + data.extend_from_slice(&frame[7..7 + len]); + + Ok((cmd, data)) + } + + pub fn write(dev: &mut T, cmd: u8, data: &[u8]) -> io::Result { + 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 = if dev.out_rpt_size() >= INIT_HEADER_SIZE { + cmp::min(data.len(), dev.out_rpt_size() - INIT_HEADER_SIZE) + } else { + data.len() + }; + 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(dev: &mut T, seq: u8, max: usize) -> io::Result> { + 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 = if dev.in_rpt_size() >= CONT_HEADER_SIZE { + cmp::min(max, dev.in_rpt_size() - CONT_HEADER_SIZE) + } else { + max + }; + Ok(frame[5..5 + max].to_vec()) + } + + pub fn write(dev: &mut T, seq: u8, data: &[u8]) -> io::Result { + let mut frame = vec![0u8; dev.out_rpt_size() + 1]; + frame[1..5].copy_from_slice(dev.get_cid()); + frame[5] = seq; + + let count = if dev.out_rpt_size() >= CONT_HEADER_SIZE { + cmp::min(data.len(), dev.out_rpt_size() - CONT_HEADER_SIZE) + } else { + data.len() + }; + 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 { + 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 Nc (command data length) = 0 +/// (ie: Lc is *always* present). +/// +/// * FIDO v1.0 declares extended Lc as a 24-bit integer, rather than +/// 16-bit with padding byte. +/// +/// * FIDO v1.0 omits Le 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> { + 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, + pub device_name: Vec, + 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..034259e85a --- /dev/null +++ b/third_party/rust/authenticator/src/util.rs @@ -0,0 +1,76 @@ +/* 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) => { + #[allow(clippy::redundant_closure_call)] + 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(all(target_os = "linux", not(test)))] +pub fn from_unix_result(rv: T) -> io::Result { + if rv.is_negative() { + let errno = unsafe { *libc::__errno_location() }; + Err(io::Error::from_raw_os_error(errno)) + } else { + Ok(rv) + } +} + +#[cfg(all(target_os = "freebsd", not(test)))] +pub fn from_unix_result(rv: T) -> io::Result { + if rv.is_negative() { + let errno = unsafe { *libc::__error() }; + Err(io::Error::from_raw_os_error(errno)) + } else { + Ok(rv) + } +} + +#[cfg(all(target_os = "openbsd", not(test)))] +pub fn from_unix_result(rv: T) -> io::Result { + 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(all(test, not(feature = "crypto_dummy")))] +pub fn decode_hex(s: &str) -> Vec { + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()) + .collect() +} -- cgit v1.2.3