summaryrefslogtreecommitdiffstats
path: root/third_party/rust/authenticator/src
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /third_party/rust/authenticator/src
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/authenticator/src')
-rw-r--r--third_party/rust/authenticator/src/authenticatorservice.rs1023
-rw-r--r--third_party/rust/authenticator/src/capi.rs402
-rw-r--r--third_party/rust/authenticator/src/consts.rs160
-rw-r--r--third_party/rust/authenticator/src/crypto/dummy.rs42
-rw-r--r--third_party/rust/authenticator/src/crypto/mod.rs924
-rw-r--r--third_party/rust/authenticator/src/crypto/nss.rs543
-rw-r--r--third_party/rust/authenticator/src/crypto/openssl.rs283
-rw-r--r--third_party/rust/authenticator/src/crypto/ring.rs168
-rw-r--r--third_party/rust/authenticator/src/ctap2-capi.h254
-rw-r--r--third_party/rust/authenticator/src/ctap2/attestation.rs799
-rw-r--r--third_party/rust/authenticator/src/ctap2/client_data.rs399
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/client_pin.rs786
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/get_assertion.rs1282
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/get_info.rs733
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/get_next_assertion.rs53
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/get_version.rs118
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/make_credentials.rs1033
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/mod.rs470
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/reset.rs129
-rw-r--r--third_party/rust/authenticator/src/ctap2/commands/selection.rs129
-rw-r--r--third_party/rust/authenticator/src/ctap2/mod.rs9
-rw-r--r--third_party/rust/authenticator/src/ctap2/server.rs510
-rw-r--r--third_party/rust/authenticator/src/ctap2/utils.rs14
-rw-r--r--third_party/rust/authenticator/src/ctap2_capi.rs945
-rw-r--r--third_party/rust/authenticator/src/errors.rs140
-rw-r--r--third_party/rust/authenticator/src/lib.rs110
-rw-r--r--third_party/rust/authenticator/src/manager.rs576
-rw-r--r--third_party/rust/authenticator/src/statecallback.rs166
-rw-r--r--third_party/rust/authenticator/src/statemachine.rs829
-rw-r--r--third_party/rust/authenticator/src/status_update.rs101
-rw-r--r--third_party/rust/authenticator/src/transport/device_selector.rs520
-rw-r--r--third_party/rust/authenticator/src/transport/errors.rs98
-rw-r--r--third_party/rust/authenticator/src/transport/freebsd/device.rs228
-rw-r--r--third_party/rust/authenticator/src/transport/freebsd/mod.rs9
-rw-r--r--third_party/rust/authenticator/src/transport/freebsd/monitor.rs165
-rw-r--r--third_party/rust/authenticator/src/transport/freebsd/transaction.rs70
-rw-r--r--third_party/rust/authenticator/src/transport/freebsd/uhid.rs92
-rw-r--r--third_party/rust/authenticator/src/transport/hid.rs136
-rw-r--r--third_party/rust/authenticator/src/transport/hidproto.rs253
-rw-r--r--third_party/rust/authenticator/src/transport/linux/device.rs182
-rw-r--r--third_party/rust/authenticator/src/transport/linux/hidraw.rs80
-rw-r--r--third_party/rust/authenticator/src/transport/linux/hidwrapper.h12
-rw-r--r--third_party/rust/authenticator/src/transport/linux/hidwrapper.rs51
-rw-r--r--third_party/rust/authenticator/src/transport/linux/ioctl_aarch64le.rs5
-rw-r--r--third_party/rust/authenticator/src/transport/linux/ioctl_armle.rs5
-rw-r--r--third_party/rust/authenticator/src/transport/linux/ioctl_mips64le.rs5
-rw-r--r--third_party/rust/authenticator/src/transport/linux/ioctl_mipsbe.rs5
-rw-r--r--third_party/rust/authenticator/src/transport/linux/ioctl_mipsle.rs5
-rw-r--r--third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64be.rs5
-rw-r--r--third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64le.rs5
-rw-r--r--third_party/rust/authenticator/src/transport/linux/ioctl_powerpcbe.rs5
-rw-r--r--third_party/rust/authenticator/src/transport/linux/ioctl_riscv64.rs5
-rw-r--r--third_party/rust/authenticator/src/transport/linux/ioctl_s390xbe.rs5
-rw-r--r--third_party/rust/authenticator/src/transport/linux/ioctl_x86.rs5
-rw-r--r--third_party/rust/authenticator/src/transport/linux/ioctl_x86_64.rs5
-rw-r--r--third_party/rust/authenticator/src/transport/linux/mod.rs12
-rw-r--r--third_party/rust/authenticator/src/transport/linux/monitor.rs194
-rw-r--r--third_party/rust/authenticator/src/transport/linux/transaction.rs70
-rw-r--r--third_party/rust/authenticator/src/transport/macos/device.rs227
-rw-r--r--third_party/rust/authenticator/src/transport/macos/iokit.rs292
-rw-r--r--third_party/rust/authenticator/src/transport/macos/mod.rs9
-rw-r--r--third_party/rust/authenticator/src/transport/macos/monitor.rs212
-rw-r--r--third_party/rust/authenticator/src/transport/macos/transaction.rs108
-rw-r--r--third_party/rust/authenticator/src/transport/mock/device.rs194
-rw-r--r--third_party/rust/authenticator/src/transport/mock/mod.rs6
-rw-r--r--third_party/rust/authenticator/src/transport/mock/transaction.rs35
-rw-r--r--third_party/rust/authenticator/src/transport/mod.rs263
-rw-r--r--third_party/rust/authenticator/src/transport/netbsd/device.rs227
-rw-r--r--third_party/rust/authenticator/src/transport/netbsd/fd.rs62
-rw-r--r--third_party/rust/authenticator/src/transport/netbsd/mod.rs10
-rw-r--r--third_party/rust/authenticator/src/transport/netbsd/monitor.rs132
-rw-r--r--third_party/rust/authenticator/src/transport/netbsd/transaction.rs70
-rw-r--r--third_party/rust/authenticator/src/transport/netbsd/uhid.rs77
-rw-r--r--third_party/rust/authenticator/src/transport/openbsd/device.rs226
-rw-r--r--third_party/rust/authenticator/src/transport/openbsd/mod.rs8
-rw-r--r--third_party/rust/authenticator/src/transport/openbsd/monitor.rs138
-rw-r--r--third_party/rust/authenticator/src/transport/openbsd/transaction.rs70
-rw-r--r--third_party/rust/authenticator/src/transport/stub/device.rs110
-rw-r--r--third_party/rust/authenticator/src/transport/stub/mod.rs11
-rw-r--r--third_party/rust/authenticator/src/transport/stub/transaction.rs52
-rw-r--r--third_party/rust/authenticator/src/transport/windows/device.rs171
-rw-r--r--third_party/rust/authenticator/src/transport/windows/mod.rs9
-rw-r--r--third_party/rust/authenticator/src/transport/windows/monitor.rs115
-rw-r--r--third_party/rust/authenticator/src/transport/windows/transaction.rs70
-rw-r--r--third_party/rust/authenticator/src/transport/windows/winapi.rs268
-rw-r--r--third_party/rust/authenticator/src/u2fhid-capi.h119
-rw-r--r--third_party/rust/authenticator/src/u2fprotocol.rs398
-rw-r--r--third_party/rust/authenticator/src/u2ftypes.rs363
-rw-r--r--third_party/rust/authenticator/src/util.rs75
-rw-r--r--third_party/rust/authenticator/src/virtualdevices/mod.rs8
-rw-r--r--third_party/rust/authenticator/src/virtualdevices/software_u2f.rs65
-rw-r--r--third_party/rust/authenticator/src/virtualdevices/webdriver/mod.rs9
-rw-r--r--third_party/rust/authenticator/src/virtualdevices/webdriver/testtoken.rs140
-rw-r--r--third_party/rust/authenticator/src/virtualdevices/webdriver/virtualmanager.rs151
-rw-r--r--third_party/rust/authenticator/src/virtualdevices/webdriver/web_api.rs964
95 files changed, 20826 insertions, 0 deletions
diff --git a/third_party/rust/authenticator/src/authenticatorservice.rs b/third_party/rust/authenticator/src/authenticatorservice.rs
new file mode 100644
index 0000000000..93195cd833
--- /dev/null
+++ b/third_party/rust/authenticator/src/authenticatorservice.rs
@@ -0,0 +1,1023 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::consts::PARAMETER_SIZE;
+use crate::ctap2::commands::client_pin::Pin;
+pub use crate::ctap2::commands::get_assertion::{
+ GetAssertionExtensions, GetAssertionOptions, HmacSecretExtension,
+};
+pub use crate::ctap2::commands::make_credentials::{
+ MakeCredentialsExtensions, MakeCredentialsOptions,
+};
+use crate::ctap2::server::{
+ PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, User,
+};
+use crate::errors::*;
+use crate::manager::Manager;
+use crate::statecallback::StateCallback;
+use std::sync::{mpsc::Sender, Arc, Mutex};
+
+// TODO(MS): Once U2FManager gets completely removed, this can be removed as well
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum CtapVersion {
+ CTAP1,
+ CTAP2,
+}
+
+#[derive(Debug, Clone)]
+pub struct RegisterArgsCtap1 {
+ pub flags: crate::RegisterFlags,
+ pub challenge: Vec<u8>,
+ pub application: crate::AppId,
+ pub key_handles: Vec<crate::KeyHandle>,
+}
+
+#[derive(Debug, Clone)]
+pub struct RegisterArgsCtap2 {
+ pub challenge: Vec<u8>,
+ pub relying_party: RelyingParty,
+ pub origin: String,
+ pub user: User,
+ pub pub_cred_params: Vec<PublicKeyCredentialParameters>,
+ pub exclude_list: Vec<PublicKeyCredentialDescriptor>,
+ pub options: MakeCredentialsOptions,
+ pub extensions: MakeCredentialsExtensions,
+ pub pin: Option<Pin>,
+}
+
+#[derive(Debug)]
+pub enum RegisterArgs {
+ CTAP1(RegisterArgsCtap1),
+ CTAP2(RegisterArgsCtap2),
+}
+
+impl From<RegisterArgsCtap1> for RegisterArgs {
+ fn from(args: RegisterArgsCtap1) -> Self {
+ RegisterArgs::CTAP1(args)
+ }
+}
+
+impl From<RegisterArgsCtap2> for RegisterArgs {
+ fn from(args: RegisterArgsCtap2) -> Self {
+ RegisterArgs::CTAP2(args)
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct SignArgsCtap1 {
+ pub flags: crate::SignFlags,
+ pub challenge: Vec<u8>,
+ pub app_ids: Vec<crate::AppId>,
+ pub key_handles: Vec<crate::KeyHandle>,
+}
+
+#[derive(Debug, Clone)]
+pub struct SignArgsCtap2 {
+ pub challenge: Vec<u8>,
+ pub origin: String,
+ pub relying_party_id: String,
+ pub allow_list: Vec<PublicKeyCredentialDescriptor>,
+ pub options: GetAssertionOptions,
+ pub extensions: GetAssertionExtensions,
+ pub pin: Option<Pin>,
+ // Todo: Extensions
+}
+
+#[derive(Debug)]
+pub enum SignArgs {
+ CTAP1(SignArgsCtap1),
+ CTAP2(SignArgsCtap2),
+}
+
+impl From<SignArgsCtap1> for SignArgs {
+ fn from(args: SignArgsCtap1) -> Self {
+ SignArgs::CTAP1(args)
+ }
+}
+
+impl From<SignArgsCtap2> for SignArgs {
+ fn from(args: SignArgsCtap2) -> Self {
+ SignArgs::CTAP2(args)
+ }
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct AssertionExtensions {
+ pub hmac_secret: Option<HmacSecretExtension>,
+}
+
+pub trait AuthenticatorTransport {
+ /// The implementation of this method must return quickly and should
+ /// report its status via the status and callback methods
+ fn register(
+ &mut self,
+ timeout: u64,
+ ctap_args: RegisterArgs,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::RegisterResult>>,
+ ) -> crate::Result<()>;
+
+ /// The implementation of this method must return quickly and should
+ /// report its status via the status and callback methods
+ fn sign(
+ &mut self,
+ timeout: u64,
+ ctap_args: SignArgs,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::SignResult>>,
+ ) -> crate::Result<()>;
+
+ fn cancel(&mut self) -> crate::Result<()>;
+ fn reset(
+ &mut self,
+ timeout: u64,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ResetResult>>,
+ ) -> crate::Result<()>;
+ fn set_pin(
+ &mut self,
+ timeout: u64,
+ new_pin: Pin,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ResetResult>>,
+ ) -> crate::Result<()>;
+}
+
+pub struct AuthenticatorService {
+ transports: Vec<Arc<Mutex<Box<dyn AuthenticatorTransport + Send>>>>,
+ ctap_version: CtapVersion,
+}
+
+fn clone_and_configure_cancellation_callback<T>(
+ mut callback: StateCallback<T>,
+ transports_to_cancel: Vec<Arc<Mutex<Box<dyn AuthenticatorTransport + Send>>>>,
+) -> StateCallback<T> {
+ callback.add_uncloneable_observer(Box::new(move || {
+ debug!(
+ "Callback observer is running, cancelling \
+ {} unchosen transports...",
+ transports_to_cancel.len()
+ );
+ for transport_mutex in &transports_to_cancel {
+ if let Err(e) = transport_mutex.lock().unwrap().cancel() {
+ error!("Cancellation failed: {:?}", e);
+ }
+ }
+ }));
+ callback
+}
+
+impl AuthenticatorService {
+ pub fn new(ctap_version: CtapVersion) -> crate::Result<Self> {
+ Ok(Self {
+ transports: Vec::new(),
+ ctap_version,
+ })
+ }
+
+ /// Add any detected platform transports
+ pub fn add_detected_transports(&mut self) {
+ self.add_u2f_usb_hid_platform_transports();
+ }
+
+ fn add_transport(&mut self, boxed_token: Box<dyn AuthenticatorTransport + Send>) {
+ self.transports.push(Arc::new(Mutex::new(boxed_token)))
+ }
+
+ pub fn add_u2f_usb_hid_platform_transports(&mut self) {
+ if self.ctap_version == CtapVersion::CTAP1 {
+ match crate::U2FManager::new() {
+ Ok(token) => self.add_transport(Box::new(token)),
+ Err(e) => error!("Could not add U2F HID transport: {}", e),
+ }
+ } else {
+ match Manager::new() {
+ Ok(token) => self.add_transport(Box::new(token)),
+ Err(e) => error!("Could not add CTAP2 HID transport: {}", e),
+ }
+ }
+ }
+
+ #[cfg(feature = "webdriver")]
+ pub fn add_webdriver_virtual_bus(&mut self) {
+ match crate::virtualdevices::webdriver::VirtualManager::new() {
+ Ok(token) => {
+ println!("WebDriver ready, listening at {}", &token.url());
+ self.add_transport(Box::new(token));
+ }
+ Err(e) => error!("Could not add WebDriver virtual bus: {}", e),
+ }
+ }
+
+ pub fn register(
+ &mut self,
+ timeout: u64,
+ ctap_args: RegisterArgs,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::RegisterResult>>,
+ ) -> crate::Result<()> {
+ match ctap_args {
+ RegisterArgs::CTAP1(a) => self.register_ctap1(timeout, a, status, callback),
+ RegisterArgs::CTAP2(a) => self.register_ctap2(timeout, a, status, callback),
+ }
+ }
+
+ fn register_ctap1(
+ &mut self,
+ timeout: u64,
+ args: RegisterArgsCtap1,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::RegisterResult>>,
+ ) -> crate::Result<()> {
+ if args.challenge.len() != PARAMETER_SIZE || args.application.len() != PARAMETER_SIZE {
+ return Err(AuthenticatorError::InvalidRelyingPartyInput);
+ }
+
+ for key_handle in &args.key_handles {
+ if key_handle.credential.len() >= 256 {
+ return Err(AuthenticatorError::InvalidRelyingPartyInput);
+ }
+ }
+
+ let iterable_transports = self.transports.clone();
+ if iterable_transports.is_empty() {
+ return Err(AuthenticatorError::NoConfiguredTransports);
+ }
+
+ debug!(
+ "register called with {} transports, iterable is {}",
+ self.transports.len(),
+ iterable_transports.len()
+ );
+
+ for (idx, transport_mutex) in iterable_transports.iter().enumerate() {
+ let mut transports_to_cancel = iterable_transports.clone();
+ transports_to_cancel.remove(idx);
+
+ debug!(
+ "register transports_to_cancel {}",
+ transports_to_cancel.len()
+ );
+
+ transport_mutex.lock().unwrap().register(
+ timeout,
+ RegisterArgs::CTAP1(args.clone()),
+ status.clone(),
+ clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel),
+ )?;
+ }
+
+ Ok(())
+ }
+
+ fn register_ctap2(
+ &mut self,
+ timeout: u64,
+ args: RegisterArgsCtap2,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::RegisterResult>>,
+ ) -> crate::Result<()> {
+ let iterable_transports = self.transports.clone();
+ if iterable_transports.is_empty() {
+ return Err(AuthenticatorError::NoConfiguredTransports);
+ }
+
+ debug!(
+ "register called with {} transports, iterable is {}",
+ self.transports.len(),
+ iterable_transports.len()
+ );
+
+ for (idx, transport_mutex) in iterable_transports.iter().enumerate() {
+ let mut transports_to_cancel = iterable_transports.clone();
+ transports_to_cancel.remove(idx);
+
+ debug!(
+ "register transports_to_cancel {}",
+ transports_to_cancel.len()
+ );
+
+ transport_mutex.lock().unwrap().register(
+ timeout,
+ RegisterArgs::CTAP2(args.clone()),
+ status.clone(),
+ clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel),
+ )?;
+ }
+
+ Ok(())
+ }
+
+ pub fn sign(
+ &mut self,
+ timeout: u64,
+ ctap_args: SignArgs,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::SignResult>>,
+ ) -> crate::Result<()> {
+ match ctap_args {
+ SignArgs::CTAP1(a) => self.sign_ctap1(timeout, a, status, callback),
+ SignArgs::CTAP2(a) => self.sign_ctap2(timeout, a, status, callback),
+ }
+ }
+
+ pub fn sign_ctap1(
+ &mut self,
+ timeout: u64,
+ args: SignArgsCtap1,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::SignResult>>,
+ ) -> crate::Result<()> {
+ if args.challenge.len() != PARAMETER_SIZE {
+ return Err(AuthenticatorError::InvalidRelyingPartyInput);
+ }
+
+ if args.app_ids.is_empty() {
+ return Err(AuthenticatorError::InvalidRelyingPartyInput);
+ }
+
+ for app_id in &args.app_ids {
+ if app_id.len() != PARAMETER_SIZE {
+ return Err(AuthenticatorError::InvalidRelyingPartyInput);
+ }
+ }
+
+ for key_handle in &args.key_handles {
+ if key_handle.credential.len() >= 256 {
+ return Err(AuthenticatorError::InvalidRelyingPartyInput);
+ }
+ }
+
+ let iterable_transports = self.transports.clone();
+ if iterable_transports.is_empty() {
+ return Err(AuthenticatorError::NoConfiguredTransports);
+ }
+
+ for (idx, transport_mutex) in iterable_transports.iter().enumerate() {
+ let mut transports_to_cancel = iterable_transports.clone();
+ transports_to_cancel.remove(idx);
+
+ transport_mutex.lock().unwrap().sign(
+ timeout,
+ SignArgs::CTAP1(args.clone()),
+ status.clone(),
+ clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel),
+ )?;
+ }
+
+ Ok(())
+ }
+
+ pub fn sign_ctap2(
+ &mut self,
+ timeout: u64,
+ args: SignArgsCtap2,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::SignResult>>,
+ ) -> crate::Result<()> {
+ let iterable_transports = self.transports.clone();
+ if iterable_transports.is_empty() {
+ return Err(AuthenticatorError::NoConfiguredTransports);
+ }
+
+ for (idx, transport_mutex) in iterable_transports.iter().enumerate() {
+ let mut transports_to_cancel = iterable_transports.clone();
+ transports_to_cancel.remove(idx);
+
+ transport_mutex.lock().unwrap().sign(
+ timeout,
+ SignArgs::CTAP2(args.clone()),
+ status.clone(),
+ clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel),
+ )?;
+ }
+
+ Ok(())
+ }
+
+ pub fn cancel(&mut self) -> crate::Result<()> {
+ if self.transports.is_empty() {
+ return Err(AuthenticatorError::NoConfiguredTransports);
+ }
+
+ for transport_mutex in &mut self.transports {
+ transport_mutex.lock().unwrap().cancel()?;
+ }
+
+ Ok(())
+ }
+
+ pub fn reset(
+ &mut self,
+ timeout: u64,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ResetResult>>,
+ ) -> crate::Result<()> {
+ let iterable_transports = self.transports.clone();
+ if iterable_transports.is_empty() {
+ return Err(AuthenticatorError::NoConfiguredTransports);
+ }
+
+ debug!(
+ "reset called with {} transports, iterable is {}",
+ self.transports.len(),
+ iterable_transports.len()
+ );
+
+ for (idx, transport_mutex) in iterable_transports.iter().enumerate() {
+ let mut transports_to_cancel = iterable_transports.clone();
+ transports_to_cancel.remove(idx);
+
+ debug!("reset transports_to_cancel {}", transports_to_cancel.len());
+
+ transport_mutex.lock().unwrap().reset(
+ timeout,
+ status.clone(),
+ clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel),
+ )?;
+ }
+
+ Ok(())
+ }
+
+ pub fn set_pin(
+ &mut self,
+ timeout: u64,
+ new_pin: Pin,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ResetResult>>,
+ ) -> crate::Result<()> {
+ let iterable_transports = self.transports.clone();
+ if iterable_transports.is_empty() {
+ return Err(AuthenticatorError::NoConfiguredTransports);
+ }
+
+ debug!(
+ "reset called with {} transports, iterable is {}",
+ self.transports.len(),
+ iterable_transports.len()
+ );
+
+ for (idx, transport_mutex) in iterable_transports.iter().enumerate() {
+ let mut transports_to_cancel = iterable_transports.clone();
+ transports_to_cancel.remove(idx);
+
+ debug!("reset transports_to_cancel {}", transports_to_cancel.len());
+
+ transport_mutex.lock().unwrap().set_pin(
+ timeout,
+ new_pin.clone(),
+ status.clone(),
+ clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel),
+ )?;
+ }
+
+ Ok(())
+ }
+}
+
+////////////////////////////////////////////////////////////////////////
+// Tests
+////////////////////////////////////////////////////////////////////////
+
+#[cfg(test)]
+mod tests {
+ use super::{
+ AuthenticatorService, AuthenticatorTransport, CtapVersion, Pin,
+ PublicKeyCredentialDescriptor, RegisterArgs, RegisterArgsCtap1, RegisterArgsCtap2,
+ SignArgs, SignArgsCtap1, SignArgsCtap2, User,
+ };
+ use crate::consts::Capability;
+ use crate::consts::PARAMETER_SIZE;
+ use crate::ctap2::server::RelyingParty;
+ use crate::statecallback::StateCallback;
+ use crate::{AuthenticatorTransports, KeyHandle, RegisterFlags, SignFlags, StatusUpdate};
+ use crate::{RegisterResult, SignResult};
+ use std::sync::atomic::{AtomicBool, Ordering};
+ use std::sync::mpsc::{channel, Sender};
+ use std::sync::Arc;
+ use std::{io, thread};
+
+ fn init() {
+ let _ = env_logger::builder().is_test(true).try_init();
+ }
+
+ pub struct TestTransportDriver {
+ consent: bool,
+ was_cancelled: Arc<AtomicBool>,
+ }
+
+ impl TestTransportDriver {
+ pub fn new(consent: bool) -> io::Result<Self> {
+ Ok(Self {
+ consent,
+ was_cancelled: Arc::new(AtomicBool::new(false)),
+ })
+ }
+ }
+
+ impl TestTransportDriver {
+ fn dev_info(&self) -> crate::u2ftypes::U2FDeviceInfo {
+ crate::u2ftypes::U2FDeviceInfo {
+ vendor_name: String::from("Mozilla").into_bytes(),
+ device_name: String::from("Test Transport Token").into_bytes(),
+ version_interface: 0,
+ version_major: 1,
+ version_minor: 2,
+ version_build: 3,
+ cap_flags: Capability::empty(),
+ }
+ }
+ }
+
+ impl AuthenticatorTransport for TestTransportDriver {
+ fn register(
+ &mut self,
+ _timeout: u64,
+ _args: RegisterArgs,
+ _status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::RegisterResult>>,
+ ) -> crate::Result<()> {
+ if self.consent {
+ let rv = Ok(RegisterResult::CTAP1(vec![0u8; 16], self.dev_info()));
+ thread::spawn(move || callback.call(rv));
+ }
+ Ok(())
+ }
+
+ fn sign(
+ &mut self,
+ _timeout: u64,
+ _ctap_args: SignArgs,
+ _status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::SignResult>>,
+ ) -> crate::Result<()> {
+ if self.consent {
+ let rv = Ok(SignResult::CTAP1(
+ vec![0u8; 0],
+ vec![0u8; 0],
+ vec![0u8; 0],
+ self.dev_info(),
+ ));
+ thread::spawn(move || callback.call(rv));
+ }
+ Ok(())
+ }
+
+ fn cancel(&mut self) -> crate::Result<()> {
+ self.was_cancelled
+ .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
+ .map_or(
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::InvalidState,
+ )),
+ |_| Ok(()),
+ )
+ }
+
+ fn reset(
+ &mut self,
+ _timeout: u64,
+ _status: Sender<crate::StatusUpdate>,
+ _callback: StateCallback<crate::Result<crate::ResetResult>>,
+ ) -> crate::Result<()> {
+ unimplemented!();
+ }
+
+ fn set_pin(
+ &mut self,
+ _timeout: u64,
+ _new_pin: Pin,
+ _status: Sender<crate::StatusUpdate>,
+ _callback: StateCallback<crate::Result<crate::ResetResult>>,
+ ) -> crate::Result<()> {
+ unimplemented!();
+ }
+ }
+
+ fn mk_key() -> KeyHandle {
+ KeyHandle {
+ credential: vec![0],
+ transports: AuthenticatorTransports::USB,
+ }
+ }
+
+ fn mk_challenge() -> Vec<u8> {
+ vec![0x11; PARAMETER_SIZE]
+ }
+
+ fn mk_appid() -> Vec<u8> {
+ vec![0x22; PARAMETER_SIZE]
+ }
+
+ #[test]
+ fn test_no_challenge() {
+ init();
+ let (status_tx, _) = channel::<StatusUpdate>();
+
+ let mut s = AuthenticatorService::new(CtapVersion::CTAP1).unwrap();
+ s.add_transport(Box::new(TestTransportDriver::new(true).unwrap()));
+
+ assert_matches!(
+ s.register(
+ 1_000,
+ RegisterArgsCtap1 {
+ challenge: vec![],
+ flags: RegisterFlags::empty(),
+ application: mk_appid(),
+ key_handles: vec![mk_key()],
+ }
+ .into(),
+ status_tx.clone(),
+ StateCallback::new(Box::new(move |_rv| {})),
+ )
+ .unwrap_err(),
+ crate::errors::AuthenticatorError::InvalidRelyingPartyInput
+ );
+
+ assert_matches!(
+ s.sign(
+ 1_000,
+ SignArgsCtap1 {
+ flags: SignFlags::empty(),
+ challenge: vec![],
+ app_ids: vec![mk_appid()],
+ key_handles: vec![mk_key()]
+ }
+ .into(),
+ status_tx,
+ StateCallback::new(Box::new(move |_rv| {})),
+ )
+ .unwrap_err(),
+ crate::errors::AuthenticatorError::InvalidRelyingPartyInput
+ );
+ }
+
+ #[test]
+ fn test_no_appids() {
+ init();
+ let (status_tx, _) = channel::<StatusUpdate>();
+
+ let mut s = AuthenticatorService::new(CtapVersion::CTAP1).unwrap();
+ s.add_transport(Box::new(TestTransportDriver::new(true).unwrap()));
+
+ assert_matches!(
+ s.register(
+ 1_000,
+ RegisterArgsCtap1 {
+ challenge: mk_challenge(),
+ flags: RegisterFlags::empty(),
+ application: vec![],
+ key_handles: vec![mk_key()],
+ }
+ .into(),
+ status_tx.clone(),
+ StateCallback::new(Box::new(move |_rv| {})),
+ )
+ .unwrap_err(),
+ crate::errors::AuthenticatorError::InvalidRelyingPartyInput
+ );
+
+ assert_matches!(
+ s.sign(
+ 1_000,
+ SignArgsCtap1 {
+ flags: SignFlags::empty(),
+ challenge: mk_challenge(),
+ app_ids: vec![],
+ key_handles: vec![mk_key()]
+ }
+ .into(),
+ status_tx,
+ StateCallback::new(Box::new(move |_rv| {})),
+ )
+ .unwrap_err(),
+ crate::errors::AuthenticatorError::InvalidRelyingPartyInput
+ );
+ }
+
+ #[test]
+ fn test_no_keys() {
+ init();
+ // No Keys is a resident-key use case. For U2F this would time out,
+ // but the actual reactions are up to the service implementation.
+ // This test yields OKs.
+ let (status_tx, _) = channel::<StatusUpdate>();
+
+ let mut s = AuthenticatorService::new(CtapVersion::CTAP1).unwrap();
+ s.add_transport(Box::new(TestTransportDriver::new(true).unwrap()));
+
+ assert_matches!(
+ s.register(
+ 100,
+ RegisterArgsCtap1 {
+ challenge: mk_challenge(),
+ flags: RegisterFlags::empty(),
+ application: mk_appid(),
+ key_handles: vec![],
+ }
+ .into(),
+ status_tx.clone(),
+ StateCallback::new(Box::new(move |_rv| {})),
+ ),
+ Ok(())
+ );
+
+ assert_matches!(
+ s.sign(
+ 100,
+ SignArgsCtap1 {
+ flags: SignFlags::empty(),
+ challenge: mk_challenge(),
+ app_ids: vec![mk_appid()],
+ key_handles: vec![]
+ }
+ .into(),
+ status_tx,
+ StateCallback::new(Box::new(move |_rv| {})),
+ ),
+ Ok(())
+ );
+ }
+
+ #[test]
+ fn test_large_keys() {
+ init();
+ let (status_tx, _) = channel::<StatusUpdate>();
+
+ let large_key = KeyHandle {
+ credential: vec![0; 257],
+ transports: AuthenticatorTransports::USB,
+ };
+
+ let mut s = AuthenticatorService::new(CtapVersion::CTAP1).unwrap();
+ s.add_transport(Box::new(TestTransportDriver::new(true).unwrap()));
+
+ assert_matches!(
+ s.register(
+ 1_000,
+ RegisterArgsCtap1 {
+ challenge: mk_challenge(),
+ flags: RegisterFlags::empty(),
+ application: mk_appid(),
+ key_handles: vec![large_key.clone()],
+ }
+ .into(),
+ status_tx.clone(),
+ StateCallback::new(Box::new(move |_rv| {})),
+ )
+ .unwrap_err(),
+ crate::errors::AuthenticatorError::InvalidRelyingPartyInput
+ );
+
+ assert_matches!(
+ s.sign(
+ 1_000,
+ SignArgsCtap1 {
+ flags: SignFlags::empty(),
+ challenge: mk_challenge(),
+ app_ids: vec![mk_appid()],
+ key_handles: vec![large_key]
+ }
+ .into(),
+ status_tx,
+ StateCallback::new(Box::new(move |_rv| {})),
+ )
+ .unwrap_err(),
+ crate::errors::AuthenticatorError::InvalidRelyingPartyInput
+ );
+ }
+
+ #[test]
+ fn test_large_keys_ctap2() {
+ init();
+ let (status_tx, _) = channel::<StatusUpdate>();
+
+ let large_key = KeyHandle {
+ credential: vec![0; 1000],
+ transports: AuthenticatorTransports::USB,
+ };
+
+ let mut s = AuthenticatorService::new(CtapVersion::CTAP2).unwrap();
+ s.add_transport(Box::new(TestTransportDriver::new(true).unwrap()));
+
+ let ctap2_register_args = RegisterArgsCtap2 {
+ challenge: mk_challenge(),
+ relying_party: RelyingParty {
+ id: "example.com".to_string(),
+ name: None,
+ icon: None,
+ },
+ origin: "example.com".to_string(),
+ user: User {
+ id: "user_id".as_bytes().to_vec(),
+ icon: None,
+ name: Some("A. User".to_string()),
+ display_name: None,
+ },
+ pub_cred_params: vec![],
+ exclude_list: vec![(&large_key).into()],
+ options: Default::default(),
+ extensions: Default::default(),
+ pin: None,
+ };
+
+ assert!(s
+ .register(
+ 1_000,
+ ctap2_register_args.into(),
+ status_tx.clone(),
+ StateCallback::new(Box::new(move |_rv| {})),
+ )
+ .is_ok(),);
+
+ let ctap2_sign_args = SignArgsCtap2 {
+ challenge: mk_challenge(),
+ origin: "example.com".to_string(),
+ relying_party_id: "example.com".to_string(),
+ allow_list: vec![(&large_key).into()],
+ options: Default::default(),
+ extensions: Default::default(),
+ pin: None,
+ };
+ assert!(s
+ .sign(
+ 1_000,
+ ctap2_sign_args.into(),
+ status_tx,
+ StateCallback::new(Box::new(move |_rv| {})),
+ )
+ .is_ok(),);
+ }
+
+ #[test]
+ fn test_no_transports() {
+ init();
+ let (status_tx, _) = channel::<StatusUpdate>();
+
+ let mut s = AuthenticatorService::new(CtapVersion::CTAP1).unwrap();
+ assert_matches!(
+ s.register(
+ 1_000,
+ RegisterArgsCtap1 {
+ challenge: mk_challenge(),
+ flags: RegisterFlags::empty(),
+ application: mk_appid(),
+ key_handles: vec![mk_key()],
+ }
+ .into(),
+ status_tx.clone(),
+ StateCallback::new(Box::new(move |_rv| {})),
+ )
+ .unwrap_err(),
+ crate::errors::AuthenticatorError::NoConfiguredTransports
+ );
+
+ assert_matches!(
+ s.sign(
+ 1_000,
+ SignArgsCtap1 {
+ flags: SignFlags::empty(),
+ challenge: mk_challenge(),
+ app_ids: vec![mk_appid()],
+ key_handles: vec![mk_key()]
+ }
+ .into(),
+ status_tx,
+ StateCallback::new(Box::new(move |_rv| {})),
+ )
+ .unwrap_err(),
+ crate::errors::AuthenticatorError::NoConfiguredTransports
+ );
+
+ assert_matches!(
+ s.cancel().unwrap_err(),
+ crate::errors::AuthenticatorError::NoConfiguredTransports
+ );
+ }
+
+ #[test]
+ fn test_cancellation_register() {
+ init();
+ let (status_tx, _) = channel::<StatusUpdate>();
+
+ let mut s = AuthenticatorService::new(CtapVersion::CTAP1).unwrap();
+ let ttd_one = TestTransportDriver::new(true).unwrap();
+ let ttd_two = TestTransportDriver::new(false).unwrap();
+ let ttd_three = TestTransportDriver::new(false).unwrap();
+
+ let was_cancelled_one = ttd_one.was_cancelled.clone();
+ let was_cancelled_two = ttd_two.was_cancelled.clone();
+ let was_cancelled_three = ttd_three.was_cancelled.clone();
+
+ s.add_transport(Box::new(ttd_one));
+ s.add_transport(Box::new(ttd_two));
+ s.add_transport(Box::new(ttd_three));
+
+ let callback = StateCallback::new(Box::new(move |_rv| {}));
+ assert!(s
+ .register(
+ 1_000,
+ RegisterArgsCtap1 {
+ challenge: mk_challenge(),
+ flags: RegisterFlags::empty(),
+ application: mk_appid(),
+ key_handles: vec![],
+ }
+ .into(),
+ status_tx,
+ callback.clone(),
+ )
+ .is_ok());
+ callback.wait();
+
+ assert_eq!(was_cancelled_one.load(Ordering::SeqCst), false);
+ assert_eq!(was_cancelled_two.load(Ordering::SeqCst), true);
+ assert_eq!(was_cancelled_three.load(Ordering::SeqCst), true);
+ }
+
+ #[test]
+ fn test_cancellation_sign() {
+ init();
+ let (status_tx, _) = channel::<StatusUpdate>();
+
+ let mut s = AuthenticatorService::new(CtapVersion::CTAP1).unwrap();
+ let ttd_one = TestTransportDriver::new(true).unwrap();
+ let ttd_two = TestTransportDriver::new(false).unwrap();
+ let ttd_three = TestTransportDriver::new(false).unwrap();
+
+ let was_cancelled_one = ttd_one.was_cancelled.clone();
+ let was_cancelled_two = ttd_two.was_cancelled.clone();
+ let was_cancelled_three = ttd_three.was_cancelled.clone();
+
+ s.add_transport(Box::new(ttd_one));
+ s.add_transport(Box::new(ttd_two));
+ s.add_transport(Box::new(ttd_three));
+
+ let callback = StateCallback::new(Box::new(move |_rv| {}));
+ assert!(s
+ .sign(
+ 1_000,
+ SignArgsCtap1 {
+ flags: SignFlags::empty(),
+ challenge: mk_challenge(),
+ app_ids: vec![mk_appid()],
+ key_handles: vec![mk_key()]
+ }
+ .into(),
+ status_tx,
+ callback.clone(),
+ )
+ .is_ok());
+ callback.wait();
+
+ assert_eq!(was_cancelled_one.load(Ordering::SeqCst), false);
+ assert_eq!(was_cancelled_two.load(Ordering::SeqCst), true);
+ assert_eq!(was_cancelled_three.load(Ordering::SeqCst), true);
+ }
+
+ #[test]
+ fn test_cancellation_race() {
+ init();
+ let (status_tx, _) = channel::<StatusUpdate>();
+
+ let mut s = AuthenticatorService::new(CtapVersion::CTAP1).unwrap();
+ // Let both of these race which one provides consent.
+ let ttd_one = TestTransportDriver::new(true).unwrap();
+ let ttd_two = TestTransportDriver::new(true).unwrap();
+
+ let was_cancelled_one = ttd_one.was_cancelled.clone();
+ let was_cancelled_two = ttd_two.was_cancelled.clone();
+
+ s.add_transport(Box::new(ttd_one));
+ s.add_transport(Box::new(ttd_two));
+
+ let callback = StateCallback::new(Box::new(move |_rv| {}));
+ assert!(s
+ .register(
+ 1_000,
+ RegisterArgsCtap1 {
+ challenge: mk_challenge(),
+ flags: RegisterFlags::empty(),
+ application: mk_appid(),
+ key_handles: vec![],
+ }
+ .into(),
+ status_tx,
+ callback.clone(),
+ )
+ .is_ok());
+ callback.wait();
+
+ let one = was_cancelled_one.load(Ordering::SeqCst);
+ let two = was_cancelled_two.load(Ordering::SeqCst);
+ assert_eq!(
+ one ^ two,
+ true,
+ "asserting that one={} xor two={} is true",
+ one,
+ two
+ );
+ }
+}
diff --git a/third_party/rust/authenticator/src/capi.rs b/third_party/rust/authenticator/src/capi.rs
new file mode 100644
index 0000000000..078f976e6c
--- /dev/null
+++ b/third_party/rust/authenticator/src/capi.rs
@@ -0,0 +1,402 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::authenticatorservice::{
+ AuthenticatorService, CtapVersion, RegisterArgsCtap1, SignArgsCtap1,
+};
+use crate::errors;
+use crate::statecallback::StateCallback;
+use crate::{RegisterResult, SignResult};
+use libc::size_t;
+use rand::{thread_rng, Rng};
+use std::collections::HashMap;
+use std::sync::mpsc::channel;
+use std::thread;
+use std::{ptr, slice};
+
+type U2FAppIds = Vec<crate::AppId>;
+type U2FKeyHandles = Vec<crate::KeyHandle>;
+type U2FCallback = extern "C" fn(u64, *mut U2FResult);
+
+pub enum U2FResult {
+ Success(HashMap<u8, Vec<u8>>),
+ Error(errors::AuthenticatorError),
+}
+
+const RESBUF_ID_REGISTRATION: u8 = 0;
+const RESBUF_ID_KEYHANDLE: u8 = 1;
+const RESBUF_ID_SIGNATURE: u8 = 2;
+const RESBUF_ID_APPID: u8 = 3;
+const RESBUF_ID_VENDOR_NAME: u8 = 4;
+const RESBUF_ID_DEVICE_NAME: u8 = 5;
+const RESBUF_ID_FIRMWARE_MAJOR: u8 = 6;
+const RESBUF_ID_FIRMWARE_MINOR: u8 = 7;
+const RESBUF_ID_FIRMWARE_BUILD: u8 = 8;
+
+// Generates a new 64-bit transaction id with collision probability 2^-32.
+fn new_tid() -> u64 {
+ thread_rng().gen::<u64>()
+}
+
+unsafe fn from_raw(ptr: *const u8, len: usize) -> Vec<u8> {
+ slice::from_raw_parts(ptr, len).to_vec()
+}
+
+/// # Safety
+///
+/// The handle returned by this method must be freed by the caller.
+#[no_mangle]
+pub extern "C" fn rust_u2f_mgr_new() -> *mut AuthenticatorService {
+ if let Ok(mut mgr) = AuthenticatorService::new(CtapVersion::CTAP1) {
+ mgr.add_detected_transports();
+ Box::into_raw(Box::new(mgr))
+ } else {
+ ptr::null_mut()
+ }
+}
+
+/// # Safety
+///
+/// This method must not be called on a handle twice, and the handle is unusable
+/// after.
+#[no_mangle]
+pub unsafe extern "C" fn rust_u2f_mgr_free(mgr: *mut AuthenticatorService) {
+ if !mgr.is_null() {
+ drop(Box::from_raw(mgr));
+ }
+}
+
+/// # Safety
+///
+/// The handle returned by this method must be freed by the caller.
+#[no_mangle]
+pub unsafe extern "C" fn rust_u2f_app_ids_new() -> *mut U2FAppIds {
+ Box::into_raw(Box::new(vec![]))
+}
+
+/// # Safety
+///
+/// This method must be used on an actual U2FAppIds handle
+#[no_mangle]
+pub unsafe extern "C" fn rust_u2f_app_ids_add(
+ ids: *mut U2FAppIds,
+ id_ptr: *const u8,
+ id_len: usize,
+) {
+ (*ids).push(from_raw(id_ptr, id_len));
+}
+
+/// # Safety
+///
+/// This method must not be called on a handle twice, and the handle is unusable
+/// after.
+#[no_mangle]
+pub unsafe extern "C" fn rust_u2f_app_ids_free(ids: *mut U2FAppIds) {
+ if !ids.is_null() {
+ drop(Box::from_raw(ids));
+ }
+}
+
+/// # Safety
+///
+/// The handle returned by this method must be freed by the caller.
+#[no_mangle]
+pub unsafe extern "C" fn rust_u2f_khs_new() -> *mut U2FKeyHandles {
+ Box::into_raw(Box::new(vec![]))
+}
+
+/// # Safety
+///
+/// This method must be used on an actual U2FKeyHandles handle
+#[no_mangle]
+pub unsafe extern "C" fn rust_u2f_khs_add(
+ khs: *mut U2FKeyHandles,
+ key_handle_ptr: *const u8,
+ key_handle_len: usize,
+ transports: u8,
+) {
+ (*khs).push(crate::KeyHandle {
+ credential: from_raw(key_handle_ptr, key_handle_len),
+ transports: crate::AuthenticatorTransports::from_bits_truncate(transports),
+ });
+}
+
+/// # Safety
+///
+/// This method must not be called on a handle twice, and the handle is unusable
+/// after.
+#[no_mangle]
+pub unsafe extern "C" fn rust_u2f_khs_free(khs: *mut U2FKeyHandles) {
+ if !khs.is_null() {
+ drop(Box::from_raw(khs));
+ }
+}
+
+/// # Safety
+///
+/// This method must be used on an actual U2FResult handle
+#[no_mangle]
+pub unsafe extern "C" fn rust_u2f_result_error(res: *const U2FResult) -> u8 {
+ if res.is_null() {
+ return errors::U2FTokenError::Unknown as u8;
+ }
+
+ if let U2FResult::Error(ref err) = *res {
+ return err.as_u2f_errorcode();
+ }
+
+ 0 /* No error, the request succeeded. */
+}
+
+/// # Safety
+///
+/// This method must be used before rust_u2f_resbuf_copy
+#[no_mangle]
+pub unsafe extern "C" fn rust_u2f_resbuf_contains(res: *const U2FResult, bid: u8) -> bool {
+ if res.is_null() {
+ return false;
+ }
+
+ if let U2FResult::Success(ref bufs) = *res {
+ return bufs.contains_key(&bid);
+ }
+
+ false
+}
+
+/// # Safety
+///
+/// This method must be used before rust_u2f_resbuf_copy
+#[no_mangle]
+pub unsafe extern "C" fn rust_u2f_resbuf_length(
+ res: *const U2FResult,
+ bid: u8,
+ len: *mut size_t,
+) -> bool {
+ if res.is_null() {
+ return false;
+ }
+
+ if let U2FResult::Success(ref bufs) = *res {
+ if let Some(buf) = bufs.get(&bid) {
+ *len = buf.len();
+ return true;
+ }
+ }
+
+ false
+}
+
+/// # Safety
+///
+/// This method does not ensure anything about dst before copying, so
+/// ensure it is long enough (using rust_u2f_resbuf_length)
+#[no_mangle]
+pub unsafe extern "C" fn rust_u2f_resbuf_copy(
+ res: *const U2FResult,
+ bid: u8,
+ dst: *mut u8,
+) -> bool {
+ if res.is_null() {
+ return false;
+ }
+
+ if let U2FResult::Success(ref bufs) = *res {
+ if let Some(buf) = bufs.get(&bid) {
+ ptr::copy_nonoverlapping(buf.as_ptr(), dst, buf.len());
+ return true;
+ }
+ }
+
+ false
+}
+
+/// # Safety
+///
+/// This method should not be called on U2FResult handles after they've been
+/// freed or a double-free will occur
+#[no_mangle]
+pub unsafe extern "C" fn rust_u2f_res_free(res: *mut U2FResult) {
+ if !res.is_null() {
+ drop(Box::from_raw(res));
+ }
+}
+
+/// # Safety
+///
+/// This method should not be called on AuthenticatorService handles after
+/// they've been freed
+#[no_mangle]
+pub unsafe extern "C" fn rust_u2f_mgr_register(
+ mgr: *mut AuthenticatorService,
+ flags: u64,
+ timeout: u64,
+ callback: U2FCallback,
+ challenge_ptr: *const u8,
+ challenge_len: usize,
+ application_ptr: *const u8,
+ application_len: usize,
+ khs: *const U2FKeyHandles,
+) -> u64 {
+ if mgr.is_null() {
+ return 0;
+ }
+
+ // Check buffers.
+ if challenge_ptr.is_null() || application_ptr.is_null() {
+ return 0;
+ }
+
+ let flags = crate::RegisterFlags::from_bits_truncate(flags);
+ let challenge = from_raw(challenge_ptr, challenge_len);
+ let application = from_raw(application_ptr, application_len);
+ let key_handles = (*khs).clone();
+
+ let (status_tx, status_rx) = channel::<crate::StatusUpdate>();
+ thread::spawn(move || loop {
+ // Issue https://github.com/mozilla/authenticator-rs/issues/132 will
+ // plumb the status channel through to the actual C API signatures
+ match status_rx.recv() {
+ Ok(_) => {}
+ Err(_recv_error) => return,
+ }
+ });
+
+ let tid = new_tid();
+
+ let state_callback = StateCallback::<crate::Result<RegisterResult>>::new(Box::new(move |rv| {
+ let result = match rv {
+ Ok(RegisterResult::CTAP1(registration, dev_info)) => {
+ let mut bufs = HashMap::new();
+ bufs.insert(RESBUF_ID_REGISTRATION, registration);
+ bufs.insert(RESBUF_ID_VENDOR_NAME, dev_info.vendor_name);
+ bufs.insert(RESBUF_ID_DEVICE_NAME, dev_info.device_name);
+ bufs.insert(RESBUF_ID_FIRMWARE_MAJOR, vec![dev_info.version_major]);
+ bufs.insert(RESBUF_ID_FIRMWARE_MINOR, vec![dev_info.version_minor]);
+ bufs.insert(RESBUF_ID_FIRMWARE_BUILD, vec![dev_info.version_build]);
+ U2FResult::Success(bufs)
+ }
+ Ok(RegisterResult::CTAP2(..)) => U2FResult::Error(
+ errors::AuthenticatorError::VersionMismatch("rust_u2f_mgr_register", 1),
+ ),
+ Err(e) => U2FResult::Error(e),
+ };
+
+ callback(tid, Box::into_raw(Box::new(result)));
+ }));
+ let ctap_args = RegisterArgsCtap1 {
+ flags,
+ challenge,
+ application,
+ key_handles,
+ };
+
+ let res = (*mgr).register(timeout, ctap_args.into(), status_tx, state_callback);
+
+ if res.is_ok() {
+ tid
+ } else {
+ 0
+ }
+}
+
+/// # Safety
+///
+/// This method should not be called on AuthenticatorService handles after
+/// they've been freed
+#[no_mangle]
+pub unsafe extern "C" fn rust_u2f_mgr_sign(
+ mgr: *mut AuthenticatorService,
+ flags: u64,
+ timeout: u64,
+ callback: U2FCallback,
+ challenge_ptr: *const u8,
+ challenge_len: usize,
+ app_ids: *const U2FAppIds,
+ khs: *const U2FKeyHandles,
+) -> u64 {
+ if mgr.is_null() || khs.is_null() {
+ return 0;
+ }
+
+ // Check buffers.
+ if challenge_ptr.is_null() {
+ return 0;
+ }
+
+ // Need at least one app_id.
+ if (*app_ids).is_empty() {
+ return 0;
+ }
+
+ let flags = crate::SignFlags::from_bits_truncate(flags);
+ let challenge = from_raw(challenge_ptr, challenge_len);
+ let app_ids = (*app_ids).clone();
+ let key_handles = (*khs).clone();
+
+ let (status_tx, status_rx) = channel::<crate::StatusUpdate>();
+ thread::spawn(move || loop {
+ // Issue https://github.com/mozilla/authenticator-rs/issues/132 will
+ // plumb the status channel through to the actual C API signatures
+ match status_rx.recv() {
+ Ok(_) => {}
+ Err(_recv_error) => return,
+ }
+ });
+
+ let tid = new_tid();
+ let state_callback = StateCallback::<crate::Result<SignResult>>::new(Box::new(move |rv| {
+ let result = match rv {
+ Ok(SignResult::CTAP1(app_id, key_handle, signature, dev_info)) => {
+ let mut bufs = HashMap::new();
+ bufs.insert(RESBUF_ID_KEYHANDLE, key_handle);
+ bufs.insert(RESBUF_ID_SIGNATURE, signature);
+ bufs.insert(RESBUF_ID_APPID, app_id);
+ bufs.insert(RESBUF_ID_VENDOR_NAME, dev_info.vendor_name);
+ bufs.insert(RESBUF_ID_DEVICE_NAME, dev_info.device_name);
+ bufs.insert(RESBUF_ID_FIRMWARE_MAJOR, vec![dev_info.version_major]);
+ bufs.insert(RESBUF_ID_FIRMWARE_MINOR, vec![dev_info.version_minor]);
+ bufs.insert(RESBUF_ID_FIRMWARE_BUILD, vec![dev_info.version_build]);
+ U2FResult::Success(bufs)
+ }
+ Ok(SignResult::CTAP2(..)) => U2FResult::Error(
+ errors::AuthenticatorError::VersionMismatch("rust_u2f_mgr_sign", 1),
+ ),
+ Err(e) => U2FResult::Error(e),
+ };
+
+ callback(tid, Box::into_raw(Box::new(result)));
+ }));
+
+ let res = (*mgr).sign(
+ timeout,
+ SignArgsCtap1 {
+ flags,
+ challenge,
+ app_ids,
+ key_handles,
+ }
+ .into(),
+ status_tx,
+ state_callback,
+ );
+
+ if res.is_ok() {
+ tid
+ } else {
+ 0
+ }
+}
+
+/// # Safety
+///
+/// This method should not be called AuthenticatorService handles after they've
+/// been freed
+#[no_mangle]
+pub unsafe extern "C" fn rust_u2f_mgr_cancel(mgr: *mut AuthenticatorService) {
+ if !mgr.is_null() {
+ // Ignore return value.
+ let _ = (*mgr).cancel();
+ }
+}
diff --git a/third_party/rust/authenticator/src/consts.rs b/third_party/rust/authenticator/src/consts.rs
new file mode 100644
index 0000000000..e96bd202c6
--- /dev/null
+++ b/third_party/rust/authenticator/src/consts.rs
@@ -0,0 +1,160 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Allow dead code in this module, since it's all packet consts anyways.
+#![allow(dead_code)]
+
+use serde::Serialize;
+
+pub const MAX_HID_RPT_SIZE: usize = 64;
+
+/// Minimum size of the U2F Raw Message header (FIDO v1.x) in extended mode,
+/// including expected response length (L<sub>e</sub>).
+///
+/// Fields `CLA`, `INS`, `P1` and `P2` are 1 byte each, and L<sub>e</sub> is 3
+/// bytes. If there is a data payload, add 2 bytes (L<sub>c</sub> is 3 bytes,
+/// and L<sub>e</sub> is 2 bytes).
+pub const U2FAPDUHEADER_SIZE: usize = 7;
+
+pub const CID_BROADCAST: [u8; 4] = [0xff, 0xff, 0xff, 0xff];
+pub const TYPE_MASK: u8 = 0x80;
+pub const TYPE_INIT: u8 = 0x80;
+pub const TYPE_CONT: u8 = 0x80;
+
+// Size of header in U2F Init USB HID Packets
+pub const INIT_HEADER_SIZE: usize = 7;
+// Size of header in U2F Cont USB HID Packets
+pub const CONT_HEADER_SIZE: usize = 5;
+
+pub const PARAMETER_SIZE: usize = 32;
+
+pub const FIDO_USAGE_PAGE: u16 = 0xf1d0; // FIDO alliance HID usage page
+pub const FIDO_USAGE_U2FHID: u16 = 0x01; // U2FHID usage for top-level collection
+pub const FIDO_USAGE_DATA_IN: u8 = 0x20; // Raw IN data report
+pub const FIDO_USAGE_DATA_OUT: u8 = 0x21; // Raw OUT data report
+
+// General pub constants
+
+pub const U2FHID_IF_VERSION: u32 = 2; // Current interface implementation version
+pub const U2FHID_FRAME_TIMEOUT: u32 = 500; // Default frame timeout in ms
+pub const U2FHID_TRANS_TIMEOUT: u32 = 3000; // Default message timeout in ms
+
+// CTAPHID native commands
+const CTAPHID_PING: u8 = TYPE_INIT | 0x01; // Echo data through local processor only
+const CTAPHID_MSG: u8 = TYPE_INIT | 0x03; // Send U2F message frame
+const CTAPHID_LOCK: u8 = TYPE_INIT | 0x04; // Send lock channel command
+const CTAPHID_INIT: u8 = TYPE_INIT | 0x06; // Channel initialization
+const CTAPHID_WINK: u8 = TYPE_INIT | 0x08; // Send device identification wink
+const CTAPHID_CBOR: u8 = TYPE_INIT | 0x10; // Encapsulated CBOR encoded message
+const CTAPHID_CANCEL: u8 = TYPE_INIT | 0x11; // Cancel outstanding requests
+const CTAPHID_KEEPALIVE: u8 = TYPE_INIT | 0x3b; // Keepalive sent to authenticator every 100ms and whenever a status changes
+const CTAPHID_ERROR: u8 = TYPE_INIT | 0x3f; // Error response
+
+#[derive(Debug, PartialEq, Eq, Copy, Clone)]
+#[repr(u8)]
+pub enum HIDCmd {
+ Ping,
+ Msg,
+ Lock,
+ Init,
+ Wink,
+ Cbor,
+ Cancel,
+ Keepalive,
+ Error,
+ Unknown(u8),
+}
+
+impl Into<u8> for HIDCmd {
+ fn into(self) -> u8 {
+ match self {
+ HIDCmd::Ping => CTAPHID_PING,
+ HIDCmd::Msg => CTAPHID_MSG,
+ HIDCmd::Lock => CTAPHID_LOCK,
+ HIDCmd::Init => CTAPHID_INIT,
+ HIDCmd::Wink => CTAPHID_WINK,
+ HIDCmd::Cbor => CTAPHID_CBOR,
+ HIDCmd::Cancel => CTAPHID_CANCEL,
+ HIDCmd::Keepalive => CTAPHID_KEEPALIVE,
+ HIDCmd::Error => CTAPHID_ERROR,
+ HIDCmd::Unknown(v) => v,
+ }
+ }
+}
+
+impl From<u8> for HIDCmd {
+ fn from(v: u8) -> HIDCmd {
+ match v {
+ CTAPHID_PING => HIDCmd::Ping,
+ CTAPHID_MSG => HIDCmd::Msg,
+ CTAPHID_LOCK => HIDCmd::Lock,
+ CTAPHID_INIT => HIDCmd::Init,
+ CTAPHID_WINK => HIDCmd::Wink,
+ CTAPHID_CBOR => HIDCmd::Cbor,
+ CTAPHID_CANCEL => HIDCmd::Cancel,
+ CTAPHID_KEEPALIVE => HIDCmd::Keepalive,
+ CTAPHID_ERROR => HIDCmd::Error,
+ v => HIDCmd::Unknown(v),
+ }
+ }
+}
+
+// U2FHID_MSG commands
+pub const U2F_VENDOR_FIRST: u8 = TYPE_INIT | 0x40; // First vendor defined command
+pub const U2F_VENDOR_LAST: u8 = TYPE_INIT | 0x7f; // Last vendor defined command
+pub const U2F_REGISTER: u8 = 0x01; // Registration command
+pub const U2F_AUTHENTICATE: u8 = 0x02; // Authenticate/sign command
+pub const U2F_VERSION: u8 = 0x03; // Read version string command
+
+pub const YKPIV_INS_GET_VERSION: u8 = 0xfd; // Get firmware version, yubico ext
+
+// U2F_REGISTER command defines
+pub const U2F_REGISTER_ID: u8 = 0x05; // Version 2 registration identifier
+pub const U2F_REGISTER_HASH_ID: u8 = 0x00; // Version 2 hash identintifier
+
+// U2F_AUTHENTICATE command defines
+pub const U2F_REQUEST_USER_PRESENCE: u8 = 0x03; // Verify user presence and sign
+pub const U2F_CHECK_IS_REGISTERED: u8 = 0x07; // Check if the key handle is registered
+
+// U2FHID_INIT command defines
+pub const INIT_NONCE_SIZE: usize = 8; // Size of channel initialization challenge
+
+bitflags! {
+ #[derive(Serialize)]
+ pub struct Capability: u8 {
+ const WINK = 0x01;
+ const LOCK = 0x02;
+ const CBOR = 0x04;
+ const NMSG = 0x08;
+ }
+}
+
+impl Capability {
+ pub fn has_fido1(self) -> bool {
+ !self.contains(Capability::NMSG)
+ }
+
+ pub fn has_fido2(self) -> bool {
+ self.contains(Capability::CBOR)
+ }
+}
+
+// Low-level error codes. Return as negatives.
+
+pub const ERR_NONE: u8 = 0x00; // No error
+pub const ERR_INVALID_CMD: u8 = 0x01; // Invalid command
+pub const ERR_INVALID_PAR: u8 = 0x02; // Invalid parameter
+pub const ERR_INVALID_LEN: u8 = 0x03; // Invalid message length
+pub const ERR_INVALID_SEQ: u8 = 0x04; // Invalid message sequencing
+pub const ERR_MSG_TIMEOUT: u8 = 0x05; // Message has timed out
+pub const ERR_CHANNEL_BUSY: u8 = 0x06; // Channel busy
+pub const ERR_LOCK_REQUIRED: u8 = 0x0a; // Command requires channel lock
+pub const ERR_INVALID_CID: u8 = 0x0b; // Command not allowed on this cid
+pub const ERR_OTHER: u8 = 0x7f; // Other unspecified error
+
+// These are ISO 7816-4 defined response status words.
+pub const SW_NO_ERROR: [u8; 2] = [0x90, 0x00];
+pub const SW_CONDITIONS_NOT_SATISFIED: [u8; 2] = [0x69, 0x85];
+pub const SW_WRONG_DATA: [u8; 2] = [0x6A, 0x80];
+pub const SW_WRONG_LENGTH: [u8; 2] = [0x67, 0x00];
diff --git a/third_party/rust/authenticator/src/crypto/dummy.rs b/third_party/rust/authenticator/src/crypto/dummy.rs
new file mode 100644
index 0000000000..c37b2cbc21
--- /dev/null
+++ b/third_party/rust/authenticator/src/crypto/dummy.rs
@@ -0,0 +1,42 @@
+use super::{ByteBuf, COSEKey, ECDHSecret, ECDSACurve};
+use serde::Serialize;
+/*
+This is a dummy implementation for CI, to avoid having to install NSS or openSSL in the CI-pipeline
+*/
+
+pub type Result<T> = std::result::Result<T, BackendError>;
+
+#[derive(Clone, Debug, PartialEq, Serialize)]
+pub enum BackendError {}
+
+pub(crate) fn serialize_key(_curve: ECDSACurve, key: &[u8]) -> Result<(ByteBuf, ByteBuf)> {
+ // Copy from NSS
+ let length = key[1..].len() / 2;
+ let chunks: Vec<_> = key[1..].chunks_exact(length).collect();
+ Ok((
+ ByteBuf::from(chunks[0].to_vec()),
+ ByteBuf::from(chunks[1].to_vec()),
+ ))
+}
+
+pub(crate) fn encapsulate(_key: &COSEKey) -> Result<ECDHSecret> {
+ unimplemented!()
+}
+
+pub(crate) fn encrypt(
+ _key: &[u8],
+ _plain_text: &[u8], /*PlainText*/
+) -> Result<Vec<u8> /*CypherText*/> {
+ unimplemented!()
+}
+
+pub(crate) fn decrypt(
+ _key: &[u8],
+ _cypher_text: &[u8], /*CypherText*/
+) -> Result<Vec<u8> /*PlainText*/> {
+ unimplemented!()
+}
+
+pub(crate) fn authenticate(_token: &[u8], _input: &[u8]) -> Result<Vec<u8>> {
+ unimplemented!()
+}
diff --git a/third_party/rust/authenticator/src/crypto/mod.rs b/third_party/rust/authenticator/src/crypto/mod.rs
new file mode 100644
index 0000000000..627ab32088
--- /dev/null
+++ b/third_party/rust/authenticator/src/crypto/mod.rs
@@ -0,0 +1,924 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::errors::AuthenticatorError;
+use crate::{ctap2::commands::CommandError, transport::errors::HIDError};
+use serde::{
+ de::{Error as SerdeError, MapAccess, Unexpected, Visitor},
+ ser::SerializeMap,
+ Deserialize, Deserializer, Serialize, Serializer,
+};
+use serde_bytes::ByteBuf;
+use serde_cbor::Value;
+use std::convert::TryFrom;
+use std::fmt;
+
+cfg_if::cfg_if! {
+ if #[cfg(feature = "crypto_ring")] {
+ #[path = "ring.rs"]
+ pub mod imp;
+ } else if #[cfg(feature = "crypto_openssl")] {
+ #[path = "openssl.rs"]
+ pub mod imp;
+ } else if #[cfg(feature = "crypto_dummy")] {
+ #[path = "dummy.rs"]
+ pub mod imp;
+ } else {
+ #[path = "nss.rs"]
+ pub mod imp;
+ }
+}
+
+pub(crate) use imp::{authenticate, decrypt, encapsulate, encrypt, serialize_key, BackendError};
+
+/// An ECDSACurve identifier. You probably will never need to alter
+/// or use this value, as it is set inside the Credential for you.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
+pub enum ECDSACurve {
+ // +---------+-------+----------+------------------------------------+
+ // | Name | Value | Key Type | Description |
+ // +---------+-------+----------+------------------------------------+
+ // | P-256 | 1 | EC2 | NIST P-256 also known as secp256r1 |
+ // | P-384 | 2 | EC2 | NIST P-384 also known as secp384r1 |
+ // | P-521 | 3 | EC2 | NIST P-521 also known as secp521r1 |
+ // | X25519 | 4 | OKP | X25519 for use w/ ECDH only |
+ // | X448 | 5 | OKP | X448 for use w/ ECDH only |
+ // | Ed25519 | 6 | OKP | Ed25519 for use w/ EdDSA only |
+ // | Ed448 | 7 | OKP | Ed448 for use w/ EdDSA only |
+ // +---------+-------+----------+------------------------------------+
+ /// Identifies this curve as SECP256R1 (X9_62_PRIME256V1 in OpenSSL)
+ SECP256R1 = 1,
+ /// Identifies this curve as SECP384R1
+ SECP384R1 = 2,
+ /// Identifies this curve as SECP521R1
+ SECP521R1 = 3,
+ /// Identifieds this as OKP X25519 for use w/ ECDH only
+ X25519 = 4,
+ /// Identifieds this as OKP X448 for use w/ ECDH only
+ X448 = 5,
+ /// Identifieds this as OKP Ed25519 for use w/ EdDSA only
+ Ed25519 = 6,
+ /// Identifieds this as OKP Ed448 for use w/ EdDSA only
+ Ed448 = 7,
+}
+
+impl TryFrom<u64> for ECDSACurve {
+ type Error = CryptoError;
+ fn try_from(i: u64) -> Result<Self, Self::Error> {
+ match i {
+ 1 => Ok(ECDSACurve::SECP256R1),
+ 2 => Ok(ECDSACurve::SECP384R1),
+ 3 => Ok(ECDSACurve::SECP521R1),
+ 4 => Ok(ECDSACurve::X25519),
+ 5 => Ok(ECDSACurve::X448),
+ 6 => Ok(ECDSACurve::Ed25519),
+ 7 => Ok(ECDSACurve::Ed448),
+ _ => Err(CryptoError::UnknownKeyType),
+ }
+ }
+}
+/// A COSE signature algorithm, indicating the type of key and hash type
+/// that should be used.
+/// see: https://www.iana.org/assignments/cose/cose.xhtml#table-algorithms
+#[allow(non_camel_case_types)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum COSEAlgorithm {
+ // /// Identifies this key as ECDSA (recommended SECP256R1) with SHA256 hashing
+ // //#[serde(alias = "ECDSA_SHA256")]
+ // ES256 = -7, // recommends curve SECP256R1
+ // /// Identifies this key as ECDSA (recommended SECP384R1) with SHA384 hashing
+ // //#[serde(alias = "ECDSA_SHA384")]
+ // ES384 = -35, // recommends curve SECP384R1
+ // /// Identifies this key as ECDSA (recommended SECP521R1) with SHA512 hashing
+ // //#[serde(alias = "ECDSA_SHA512")]
+ // ES512 = -36, // recommends curve SECP521R1
+ // /// Identifies this key as RS256 aka RSASSA-PKCS1-v1_5 w/ SHA-256
+ // RS256 = -257,
+ // /// Identifies this key as RS384 aka RSASSA-PKCS1-v1_5 w/ SHA-384
+ // RS384 = -258,
+ // /// Identifies this key as RS512 aka RSASSA-PKCS1-v1_5 w/ SHA-512
+ // RS512 = -259,
+ // /// Identifies this key as PS256 aka RSASSA-PSS w/ SHA-256
+ // PS256 = -37,
+ // /// Identifies this key as PS384 aka RSASSA-PSS w/ SHA-384
+ // PS384 = -38,
+ // /// Identifies this key as PS512 aka RSASSA-PSS w/ SHA-512
+ // PS512 = -39,
+ // /// Identifies this key as EdDSA (likely curve ed25519)
+ // EDDSA = -8,
+ // /// Identifies this as an INSECURE RS1 aka RSASSA-PKCS1-v1_5 using SHA-1. This is not
+ // /// used by validators, but can exist in some windows hello tpm's
+ // INSECURE_RS1 = -65535,
+ INSECURE_RS1 = -65535, // RSASSA-PKCS1-v1_5 using SHA-1
+ RS512 = -259, // RSASSA-PKCS1-v1_5 using SHA-512
+ RS384 = -258, // RSASSA-PKCS1-v1_5 using SHA-384
+ RS256 = -257, // RSASSA-PKCS1-v1_5 using SHA-256
+ ES256K = -47, // ECDSA using secp256k1 curve and SHA-256
+ HSS_LMS = -46, // HSS/LMS hash-based digital signature
+ SHAKE256 = -45, // SHAKE-256 512-bit Hash Value
+ SHA512 = -44, // SHA-2 512-bit Hash
+ SHA384 = -43, // SHA-2 384-bit Hash
+ RSAES_OAEP_SHA_512 = -42, // RSAES-OAEP w/ SHA-512
+ RSAES_OAEP_SHA_256 = -41, // RSAES-OAEP w/ SHA-256
+ RSAES_OAEP_RFC_8017_default = -40, // RSAES-OAEP w/ SHA-1
+ PS512 = -39, // RSASSA-PSS w/ SHA-512
+ PS384 = -38, // RSASSA-PSS w/ SHA-384
+ PS256 = -37, // RSASSA-PSS w/ SHA-256
+ ES512 = -36, // ECDSA w/ SHA-512
+ ES384 = -35, // ECDSA w/ SHA-384
+ ECDH_SS_A256KW = -34, // ECDH SS w/ Concat KDF and AES Key Wrap w/ 256-bit key
+ ECDH_SS_A192KW = -33, // ECDH SS w/ Concat KDF and AES Key Wrap w/ 192-bit key
+ ECDH_SS_A128KW = -32, // ECDH SS w/ Concat KDF and AES Key Wrap w/ 128-bit key
+ ECDH_ES_A256KW = -31, // ECDH ES w/ Concat KDF and AES Key Wrap w/ 256-bit key
+ ECDH_ES_A192KW = -30, // ECDH ES w/ Concat KDF and AES Key Wrap w/ 192-bit key
+ ECDH_ES_A128KW = -29, // ECDH ES w/ Concat KDF and AES Key Wrap w/ 128-bit key
+ ECDH_SS_HKDF512 = -28, // ECDH SS w/ HKDF - generate key directly
+ ECDH_SS_HKDF256 = -27, // ECDH SS w/ HKDF - generate key directly
+ ECDH_ES_HKDF512 = -26, // ECDH ES w/ HKDF - generate key directly
+ ECDH_ES_HKDF256 = -25, // ECDH ES w/ HKDF - generate key directly
+ SHAKE128 = -18, // SHAKE-128 256-bit Hash Value
+ SHA512_256 = -17, // SHA-2 512-bit Hash truncated to 256-bits
+ SHA256 = -16, // SHA-2 256-bit Hash
+ SHA256_64 = -15, // SHA-2 256-bit Hash truncated to 64-bits
+ SHA1 = -14, // SHA-1 Hash
+ Direct_HKDF_AES256 = -13, // Shared secret w/ AES-MAC 256-bit key
+ Direct_HKDF_AES128 = -12, // Shared secret w/ AES-MAC 128-bit key
+ Direct_HKDF_SHA512 = -11, // Shared secret w/ HKDF and SHA-512
+ Direct_HKDF_SHA256 = -10, // Shared secret w/ HKDF and SHA-256
+ EDDSA = -8, // EdDSA
+ ES256 = -7, // ECDSA w/ SHA-256
+ Direct = -6, // Direct use of CEK
+ A256KW = -5, // AES Key Wrap w/ 256-bit key
+ A192KW = -4, // AES Key Wrap w/ 192-bit key
+ A128KW = -3, // AES Key Wrap w/ 128-bit key
+ A128GCM = 1, // AES-GCM mode w/ 128-bit key, 128-bit tag
+ A192GCM = 2, // AES-GCM mode w/ 192-bit key, 128-bit tag
+ A256GCM = 3, // AES-GCM mode w/ 256-bit key, 128-bit tag
+ HMAC256_64 = 4, // HMAC w/ SHA-256 truncated to 64 bits
+ HMAC256_256 = 5, // HMAC w/ SHA-256
+ HMAC384_384 = 6, // HMAC w/ SHA-384
+ HMAC512_512 = 7, // HMAC w/ SHA-512
+ AES_CCM_16_64_128 = 10, // AES-CCM mode 128-bit key, 64-bit tag, 13-byte nonce
+ AES_CCM_16_64_256 = 11, // AES-CCM mode 256-bit key, 64-bit tag, 13-byte nonce
+ AES_CCM_64_64_128 = 12, // AES-CCM mode 128-bit key, 64-bit tag, 7-byte nonce
+ AES_CCM_64_64_256 = 13, // AES-CCM mode 256-bit key, 64-bit tag, 7-byte nonce
+ AES_MAC_128_64 = 14, // AES-MAC 128-bit key, 64-bit tag
+ AES_MAC_256_64 = 15, // AES-MAC 256-bit key, 64-bit tag
+ ChaCha20_Poly1305 = 24, // ChaCha20/Poly1305 w/ 256-bit key, 128-bit tag
+ AES_MAC_128_128 = 25, // AES-MAC 128-bit key, 128-bit tag
+ AES_MAC_256_128 = 26, // AES-MAC 256-bit key, 128-bit tag
+ AES_CCM_16_128_128 = 30, // AES-CCM mode 128-bit key, 128-bit tag, 13-byte nonce
+ AES_CCM_16_128_256 = 31, // AES-CCM mode 256-bit key, 128-bit tag, 13-byte nonce
+ AES_CCM_64_128_128 = 32, // AES-CCM mode 128-bit key, 128-bit tag, 7-byte nonce
+ AES_CCM_64_128_256 = 33, // AES-CCM mode 256-bit key, 128-bit tag, 7-byte nonce
+ IV_GENERATION = 34, // For doing IV generation for symmetric algorithms.
+}
+
+impl Serialize for COSEAlgorithm {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ match *self {
+ COSEAlgorithm::RS512 => serializer.serialize_i16(-259),
+ COSEAlgorithm::RS384 => serializer.serialize_i16(-258),
+ COSEAlgorithm::RS256 => serializer.serialize_i16(-257),
+ COSEAlgorithm::ES256K => serializer.serialize_i8(-47),
+ COSEAlgorithm::HSS_LMS => serializer.serialize_i8(-46),
+ COSEAlgorithm::SHAKE256 => serializer.serialize_i8(-45),
+ COSEAlgorithm::SHA512 => serializer.serialize_i8(-44),
+ COSEAlgorithm::SHA384 => serializer.serialize_i8(-43),
+ COSEAlgorithm::RSAES_OAEP_SHA_512 => serializer.serialize_i8(-42),
+ COSEAlgorithm::RSAES_OAEP_SHA_256 => serializer.serialize_i8(-41),
+ COSEAlgorithm::RSAES_OAEP_RFC_8017_default => serializer.serialize_i8(-40),
+ COSEAlgorithm::PS512 => serializer.serialize_i8(-39),
+ COSEAlgorithm::PS384 => serializer.serialize_i8(-38),
+ COSEAlgorithm::PS256 => serializer.serialize_i8(-37),
+ COSEAlgorithm::ES512 => serializer.serialize_i8(-36),
+ COSEAlgorithm::ES384 => serializer.serialize_i8(-35),
+ COSEAlgorithm::ECDH_SS_A256KW => serializer.serialize_i8(-34),
+ COSEAlgorithm::ECDH_SS_A192KW => serializer.serialize_i8(-33),
+ COSEAlgorithm::ECDH_SS_A128KW => serializer.serialize_i8(-32),
+ COSEAlgorithm::ECDH_ES_A256KW => serializer.serialize_i8(-31),
+ COSEAlgorithm::ECDH_ES_A192KW => serializer.serialize_i8(-30),
+ COSEAlgorithm::ECDH_ES_A128KW => serializer.serialize_i8(-29),
+ COSEAlgorithm::ECDH_SS_HKDF512 => serializer.serialize_i8(-28),
+ COSEAlgorithm::ECDH_SS_HKDF256 => serializer.serialize_i8(-27),
+ COSEAlgorithm::ECDH_ES_HKDF512 => serializer.serialize_i8(-26),
+ COSEAlgorithm::ECDH_ES_HKDF256 => serializer.serialize_i8(-25),
+ COSEAlgorithm::SHAKE128 => serializer.serialize_i8(-18),
+ COSEAlgorithm::SHA512_256 => serializer.serialize_i8(-17),
+ COSEAlgorithm::SHA256 => serializer.serialize_i8(-16),
+ COSEAlgorithm::SHA256_64 => serializer.serialize_i8(-15),
+ COSEAlgorithm::SHA1 => serializer.serialize_i8(-14),
+ COSEAlgorithm::Direct_HKDF_AES256 => serializer.serialize_i8(-13),
+ COSEAlgorithm::Direct_HKDF_AES128 => serializer.serialize_i8(-12),
+ COSEAlgorithm::Direct_HKDF_SHA512 => serializer.serialize_i8(-11),
+ COSEAlgorithm::Direct_HKDF_SHA256 => serializer.serialize_i8(-10),
+ COSEAlgorithm::EDDSA => serializer.serialize_i8(-8),
+ COSEAlgorithm::ES256 => serializer.serialize_i8(-7),
+ COSEAlgorithm::Direct => serializer.serialize_i8(-6),
+ COSEAlgorithm::A256KW => serializer.serialize_i8(-5),
+ COSEAlgorithm::A192KW => serializer.serialize_i8(-4),
+ COSEAlgorithm::A128KW => serializer.serialize_i8(-3),
+ COSEAlgorithm::A128GCM => serializer.serialize_i8(1),
+ COSEAlgorithm::A192GCM => serializer.serialize_i8(2),
+ COSEAlgorithm::A256GCM => serializer.serialize_i8(3),
+ COSEAlgorithm::HMAC256_64 => serializer.serialize_i8(4),
+ COSEAlgorithm::HMAC256_256 => serializer.serialize_i8(5),
+ COSEAlgorithm::HMAC384_384 => serializer.serialize_i8(6),
+ COSEAlgorithm::HMAC512_512 => serializer.serialize_i8(7),
+ COSEAlgorithm::AES_CCM_16_64_128 => serializer.serialize_i8(10),
+ COSEAlgorithm::AES_CCM_16_64_256 => serializer.serialize_i8(11),
+ COSEAlgorithm::AES_CCM_64_64_128 => serializer.serialize_i8(12),
+ COSEAlgorithm::AES_CCM_64_64_256 => serializer.serialize_i8(13),
+ COSEAlgorithm::AES_MAC_128_64 => serializer.serialize_i8(14),
+ COSEAlgorithm::AES_MAC_256_64 => serializer.serialize_i8(15),
+ COSEAlgorithm::ChaCha20_Poly1305 => serializer.serialize_i8(24),
+ COSEAlgorithm::AES_MAC_128_128 => serializer.serialize_i8(25),
+ COSEAlgorithm::AES_MAC_256_128 => serializer.serialize_i8(26),
+ COSEAlgorithm::AES_CCM_16_128_128 => serializer.serialize_i8(30),
+ COSEAlgorithm::AES_CCM_16_128_256 => serializer.serialize_i8(31),
+ COSEAlgorithm::AES_CCM_64_128_128 => serializer.serialize_i8(32),
+ COSEAlgorithm::AES_CCM_64_128_256 => serializer.serialize_i8(33),
+ COSEAlgorithm::IV_GENERATION => serializer.serialize_i8(34),
+ COSEAlgorithm::INSECURE_RS1 => serializer.serialize_i32(-65535),
+ }
+ }
+}
+
+impl<'de> Deserialize<'de> for COSEAlgorithm {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct COSEAlgorithmVisitor;
+
+ impl<'de> Visitor<'de> for COSEAlgorithmVisitor {
+ type Value = COSEAlgorithm;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a signed integer")
+ }
+
+ fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
+ where
+ E: SerdeError,
+ {
+ COSEAlgorithm::try_from(v).map_err(|_| {
+ SerdeError::invalid_value(Unexpected::Signed(v), &"valid COSEAlgorithm")
+ })
+ }
+ }
+
+ deserializer.deserialize_any(COSEAlgorithmVisitor)
+ }
+}
+
+impl TryFrom<i64> for COSEAlgorithm {
+ type Error = CryptoError;
+ fn try_from(i: i64) -> Result<Self, Self::Error> {
+ match i {
+ -259 => Ok(COSEAlgorithm::RS512),
+ -258 => Ok(COSEAlgorithm::RS384),
+ -257 => Ok(COSEAlgorithm::RS256),
+ -47 => Ok(COSEAlgorithm::ES256K),
+ -46 => Ok(COSEAlgorithm::HSS_LMS),
+ -45 => Ok(COSEAlgorithm::SHAKE256),
+ -44 => Ok(COSEAlgorithm::SHA512),
+ -43 => Ok(COSEAlgorithm::SHA384),
+ -42 => Ok(COSEAlgorithm::RSAES_OAEP_SHA_512),
+ -41 => Ok(COSEAlgorithm::RSAES_OAEP_SHA_256),
+ -40 => Ok(COSEAlgorithm::RSAES_OAEP_RFC_8017_default),
+ -39 => Ok(COSEAlgorithm::PS512),
+ -38 => Ok(COSEAlgorithm::PS384),
+ -37 => Ok(COSEAlgorithm::PS256),
+ -36 => Ok(COSEAlgorithm::ES512),
+ -35 => Ok(COSEAlgorithm::ES384),
+ -34 => Ok(COSEAlgorithm::ECDH_SS_A256KW),
+ -33 => Ok(COSEAlgorithm::ECDH_SS_A192KW),
+ -32 => Ok(COSEAlgorithm::ECDH_SS_A128KW),
+ -31 => Ok(COSEAlgorithm::ECDH_ES_A256KW),
+ -30 => Ok(COSEAlgorithm::ECDH_ES_A192KW),
+ -29 => Ok(COSEAlgorithm::ECDH_ES_A128KW),
+ -28 => Ok(COSEAlgorithm::ECDH_SS_HKDF512),
+ -27 => Ok(COSEAlgorithm::ECDH_SS_HKDF256),
+ -26 => Ok(COSEAlgorithm::ECDH_ES_HKDF512),
+ -25 => Ok(COSEAlgorithm::ECDH_ES_HKDF256),
+ -18 => Ok(COSEAlgorithm::SHAKE128),
+ -17 => Ok(COSEAlgorithm::SHA512_256),
+ -16 => Ok(COSEAlgorithm::SHA256),
+ -15 => Ok(COSEAlgorithm::SHA256_64),
+ -14 => Ok(COSEAlgorithm::SHA1),
+ -13 => Ok(COSEAlgorithm::Direct_HKDF_AES256),
+ -12 => Ok(COSEAlgorithm::Direct_HKDF_AES128),
+ -11 => Ok(COSEAlgorithm::Direct_HKDF_SHA512),
+ -10 => Ok(COSEAlgorithm::Direct_HKDF_SHA256),
+ -8 => Ok(COSEAlgorithm::EDDSA),
+ -7 => Ok(COSEAlgorithm::ES256),
+ -6 => Ok(COSEAlgorithm::Direct),
+ -5 => Ok(COSEAlgorithm::A256KW),
+ -4 => Ok(COSEAlgorithm::A192KW),
+ -3 => Ok(COSEAlgorithm::A128KW),
+ 1 => Ok(COSEAlgorithm::A128GCM),
+ 2 => Ok(COSEAlgorithm::A192GCM),
+ 3 => Ok(COSEAlgorithm::A256GCM),
+ 4 => Ok(COSEAlgorithm::HMAC256_64),
+ 5 => Ok(COSEAlgorithm::HMAC256_256),
+ 6 => Ok(COSEAlgorithm::HMAC384_384),
+ 7 => Ok(COSEAlgorithm::HMAC512_512),
+ 10 => Ok(COSEAlgorithm::AES_CCM_16_64_128),
+ 11 => Ok(COSEAlgorithm::AES_CCM_16_64_256),
+ 12 => Ok(COSEAlgorithm::AES_CCM_64_64_128),
+ 13 => Ok(COSEAlgorithm::AES_CCM_64_64_256),
+ 14 => Ok(COSEAlgorithm::AES_MAC_128_64),
+ 15 => Ok(COSEAlgorithm::AES_MAC_256_64),
+ 24 => Ok(COSEAlgorithm::ChaCha20_Poly1305),
+ 25 => Ok(COSEAlgorithm::AES_MAC_128_128),
+ 26 => Ok(COSEAlgorithm::AES_MAC_256_128),
+ 30 => Ok(COSEAlgorithm::AES_CCM_16_128_128),
+ 31 => Ok(COSEAlgorithm::AES_CCM_16_128_256),
+ 32 => Ok(COSEAlgorithm::AES_CCM_64_128_128),
+ 33 => Ok(COSEAlgorithm::AES_CCM_64_128_256),
+ 34 => Ok(COSEAlgorithm::IV_GENERATION),
+ -65535 => Ok(COSEAlgorithm::INSECURE_RS1),
+ _ => Err(CryptoError::UnknownAlgorithm),
+ }
+ }
+}
+/// A COSE Elliptic Curve Public Key. This is generally the provided credential
+/// that an authenticator registers, and is used to authenticate the user.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct COSEEC2Key {
+ /// The curve that this key references.
+ pub curve: ECDSACurve,
+ /// The key's public X coordinate.
+ pub x: Vec<u8>,
+ /// The key's public Y coordinate.
+ pub y: Vec<u8>,
+}
+
+/// A Octet Key Pair (OKP).
+/// The other version uses only the x-coordinate as the y-coordinate is
+/// either to be recomputed or not needed for the key agreement operation ('OKP').
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct COSEOKPKey {
+ /// The curve that this key references.
+ pub curve: ECDSACurve,
+ /// The key's public X coordinate.
+ pub x: Vec<u8>,
+}
+
+/// A COSE RSA PublicKey. This is a provided credential from a registered
+/// authenticator.
+/// You will likely never need to interact with this value, as it is part of the Credential
+/// API.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct COSERSAKey {
+ /// An RSA modulus
+ pub n: Vec<u8>,
+ /// An RSA exponent
+ pub e: Vec<u8>,
+}
+
+/// A Octet Key Pair (OKP).
+/// The other version uses only the x-coordinate as the y-coordinate is
+/// either to be recomputed or not needed for the key agreement operation ('OKP').
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct COSESymmetricKey {
+ /// The key
+ pub key: Vec<u8>,
+}
+
+// https://tools.ietf.org/html/rfc8152#section-13
+#[allow(non_camel_case_types)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[repr(i64)]
+pub enum COSEKeyTypeId {
+ // Reserved is invalid
+ // Reserved = 0,
+ /// Octet Key Pair
+ OKP = 1,
+ /// Elliptic Curve Keys w/ x- and y-coordinate
+ EC2 = 2,
+ /// RSA
+ RSA = 3,
+ /// Symmetric
+ Symmetric = 4,
+}
+
+impl TryFrom<u64> for COSEKeyTypeId {
+ type Error = CryptoError;
+ fn try_from(i: u64) -> Result<Self, Self::Error> {
+ match i {
+ 1 => Ok(COSEKeyTypeId::OKP),
+ 2 => Ok(COSEKeyTypeId::EC2),
+ 3 => Ok(COSEKeyTypeId::RSA),
+ 4 => Ok(COSEKeyTypeId::Symmetric),
+ _ => Err(CryptoError::UnknownKeyType),
+ }
+ }
+}
+
+/// The type of Key contained within a COSE value. You should never need
+/// to alter or change this type.
+#[allow(non_camel_case_types)]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum COSEKeyType {
+ // +-----------+-------+-----------------------------------------------+
+ // | Name | Value | Description |
+ // +-----------+-------+-----------------------------------------------+
+ // | OKP | 1 | Octet Key Pair |
+ // | EC2 | 2 | Elliptic Curve Keys w/ x- and y-coordinate |
+ // | | | pair |
+ // | Symmetric | 4 | Symmetric Keys |
+ // | Reserved | 0 | This value is reserved |
+ // +-----------+-------+-----------------------------------------------+
+ // Reserved, // should always be invalid.
+ /// Identifies this as an Elliptic Curve octet key pair
+ OKP(COSEOKPKey), // Not used here
+ /// Identifies this as an Elliptic Curve EC2 key
+ EC2(COSEEC2Key),
+ /// Identifies this as an RSA key
+ RSA(COSERSAKey), // Not used here
+ /// Identifies this as a Symmetric key
+ Symmetric(COSESymmetricKey), // Not used here
+}
+
+/// A COSE Key as provided by the Authenticator. You should never need
+/// to alter or change these values.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct COSEKey {
+ /// COSE signature algorithm, indicating the type of key and hash type
+ /// that should be used.
+ pub alg: COSEAlgorithm,
+ /// The public key
+ pub key: COSEKeyType,
+}
+
+impl<'de> Deserialize<'de> for COSEKey {
+ fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct COSEKeyVisitor;
+
+ impl<'de> Visitor<'de> for COSEKeyVisitor {
+ type Value = COSEKey;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a map")
+ }
+
+ fn visit_map<M>(self, mut map: M) -> std::result::Result<Self::Value, M::Error>
+ where
+ M: MapAccess<'de>,
+ {
+ let mut curve: Option<ECDSACurve> = None;
+ let mut key_type: Option<COSEKeyTypeId> = None;
+ let mut alg: Option<COSEAlgorithm> = None;
+ let mut x: Option<Vec<u8>> = None;
+ let mut y: Option<Vec<u8>> = None;
+
+ while let Some(key) = map.next_key()? {
+ trace!("cose key {:?}", key);
+ match key {
+ 1 => {
+ if key_type.is_some() {
+ return Err(SerdeError::duplicate_field("key_type"));
+ }
+ let value: u64 = map.next_value()?;
+ let val = COSEKeyTypeId::try_from(value).map_err(|_| {
+ SerdeError::custom(format!("unsupported key_type {}", value))
+ })?;
+ key_type = Some(val);
+ // key_type = Some(map.next_value()?);
+ }
+ -1 => {
+ let key_type = key_type.ok_or(SerdeError::missing_field("key_type"))?;
+ if key_type == COSEKeyTypeId::RSA {
+ if y.is_some() {
+ return Err(SerdeError::duplicate_field("y"));
+ }
+ let value: ByteBuf = map.next_value()?;
+ y = Some(value.to_vec());
+ } else {
+ if curve.is_some() {
+ return Err(SerdeError::duplicate_field("curve"));
+ }
+ let value: u64 = map.next_value()?;
+ let val = ECDSACurve::try_from(value).map_err(|_| {
+ SerdeError::custom(format!("unsupported curve {}", value))
+ })?;
+ curve = Some(val);
+ // curve = Some(map.next_value()?);
+ }
+ }
+ -2 => {
+ if x.is_some() {
+ return Err(SerdeError::duplicate_field("x"));
+ }
+ let value: ByteBuf = map.next_value()?;
+ x = Some(value.to_vec());
+ }
+ -3 => {
+ if y.is_some() {
+ return Err(SerdeError::duplicate_field("y"));
+ }
+ let value: ByteBuf = map.next_value()?;
+ y = Some(value.to_vec());
+ }
+ 3 => {
+ if alg.is_some() {
+ return Err(SerdeError::duplicate_field("alg"));
+ }
+ let value: i64 = map.next_value()?;
+ let val = COSEAlgorithm::try_from(value).map_err(|_| {
+ SerdeError::custom(format!("unsupported algorithm {}", value))
+ })?;
+ alg = Some(val);
+ // alg = map.next_value()?;
+ }
+ _ => {
+ // This unknown field should raise an error, but
+ // there is a couple of field I(baloo) do not understand
+ // yet. I(baloo) chose to ignore silently the
+ // error instead because of that
+ let value: Value = map.next_value()?;
+ trace!("cose unknown value {:?}:{:?}", key, value);
+ }
+ };
+ }
+
+ let key_type = key_type.ok_or(SerdeError::missing_field("key_type"))?;
+ let x = x.ok_or(SerdeError::missing_field("x"))?;
+ let alg = alg.ok_or(SerdeError::missing_field("alg"))?;
+
+ let res = match key_type {
+ COSEKeyTypeId::OKP => {
+ let curve = curve.ok_or(SerdeError::missing_field("curve"))?;
+ COSEKeyType::OKP(COSEOKPKey { curve, x })
+ }
+ COSEKeyTypeId::EC2 => {
+ let curve = curve.ok_or(SerdeError::missing_field("curve"))?;
+ let y = y.ok_or(SerdeError::missing_field("y"))?;
+ COSEKeyType::EC2(COSEEC2Key { curve, x, y })
+ }
+ COSEKeyTypeId::RSA => {
+ let e = y.ok_or(SerdeError::missing_field("y"))?;
+ COSEKeyType::RSA(COSERSAKey { e, n: x })
+ }
+ COSEKeyTypeId::Symmetric => COSEKeyType::Symmetric(COSESymmetricKey { key: x }),
+ };
+ Ok(COSEKey { alg, key: res })
+ }
+ }
+
+ deserializer.deserialize_bytes(COSEKeyVisitor)
+ }
+}
+
+impl Serialize for COSEKey {
+ fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let map_len = match &self.key {
+ COSEKeyType::OKP(_) => 3,
+ COSEKeyType::EC2(_) => 5,
+ COSEKeyType::RSA(_) => 4,
+ COSEKeyType::Symmetric(_) => 3,
+ };
+ let mut map = serializer.serialize_map(Some(map_len))?;
+ match &self.key {
+ COSEKeyType::OKP(key) => {
+ map.serialize_entry(&1, &COSEKeyTypeId::OKP)?;
+ map.serialize_entry(&3, &self.alg)?;
+ map.serialize_entry(&-1, &key.curve)?;
+ map.serialize_entry(&-2, &key.x)?;
+ }
+ COSEKeyType::EC2(key) => {
+ map.serialize_entry(&1, &(COSEKeyTypeId::EC2 as u8))?;
+ map.serialize_entry(&3, &self.alg)?;
+ map.serialize_entry(&-1, &(key.curve as u8))?;
+ map.serialize_entry(&-2, &serde_bytes::Bytes::new(&key.x))?;
+ map.serialize_entry(&-3, &serde_bytes::Bytes::new(&key.y))?;
+ }
+ COSEKeyType::RSA(key) => {
+ map.serialize_entry(&1, &COSEKeyTypeId::RSA)?;
+ map.serialize_entry(&3, &self.alg)?;
+ map.serialize_entry(&-1, &key.n)?;
+ map.serialize_entry(&-2, &key.e)?;
+ }
+ COSEKeyType::Symmetric(key) => {
+ map.serialize_entry(&1, &COSEKeyTypeId::Symmetric)?;
+ map.serialize_entry(&3, &self.alg)?;
+ map.serialize_entry(&-1, &key.key)?;
+ }
+ }
+
+ map.end()
+ }
+}
+
+/// Errors that can be returned from COSE functions.
+#[derive(Debug)]
+pub enum CryptoError {
+ // DecodingFailure,
+ // LibraryFailure,
+ MalformedInput,
+ // MissingHeader,
+ // UnexpectedHeaderValue,
+ // UnexpectedTag,
+ // UnexpectedType,
+ // Unimplemented,
+ // VerificationFailed,
+ // SigningFailed,
+ // InvalidArgument,
+ UnknownKeyType,
+ UnknownSignatureScheme,
+ UnknownAlgorithm,
+ WrongSaltLength,
+ Backend(BackendError),
+}
+
+impl From<BackendError> for CryptoError {
+ fn from(e: BackendError) -> Self {
+ CryptoError::Backend(e)
+ }
+}
+
+impl From<CryptoError> for CommandError {
+ fn from(e: CryptoError) -> Self {
+ CommandError::Crypto(e)
+ }
+}
+
+impl From<CryptoError> for AuthenticatorError {
+ fn from(e: CryptoError) -> Self {
+ AuthenticatorError::HIDError(HIDError::Command(CommandError::Crypto(e)))
+ }
+}
+
+#[derive(Clone)]
+pub struct ECDHSecret {
+ remote: COSEKey,
+ my: COSEKey,
+ shared_secret: Vec<u8>,
+}
+
+impl ECDHSecret {
+ pub fn my_public_key(&self) -> &COSEKey {
+ &self.my
+ }
+
+ pub fn shared_secret(&self) -> &[u8] {
+ &self.shared_secret
+ }
+}
+
+impl fmt::Debug for ECDHSecret {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "ECDHSecret(remote: {:?}, my: {:?})",
+ self.remote,
+ self.my_public_key()
+ )
+ }
+}
+
+pub struct U2FRegisterAnswer<'a> {
+ pub certificate: &'a [u8],
+ pub signature: &'a [u8],
+}
+
+// We will only return MalformedInput here
+pub fn parse_u2f_der_certificate(data: &[u8]) -> Result<U2FRegisterAnswer, CryptoError> {
+ // So we don't panic below, when accessing individual bytes
+ if data.len() < 4 {
+ return Err(CryptoError::MalformedInput);
+ }
+ // Check if it is a SEQUENCE
+ if data[0] != 0x30 {
+ return Err(CryptoError::MalformedInput);
+ }
+
+ // This algorithm is taken from mozilla-central/security/nss/lib/mozpkix/lib/pkixder.cpp
+ // The short form of length is a single byte with the high order bit set
+ // to zero. The long form of length is one byte with the high order bit
+ // set, followed by N bytes, where N is encoded in the lowest 7 bits of
+ // the first byte.
+ let end = if (data[1] & 0x80) == 0 {
+ 2 + data[1] as usize
+ } else if data[1] == 0x81 {
+ // The next byte specifies the length
+
+ if data[2] < 128 {
+ // Not shortest possible encoding
+ // Forbidden by DER-format
+ return Err(CryptoError::MalformedInput);
+ }
+ 3 + data[2] as usize
+ } else if data[1] == 0x82 {
+ // The next 2 bytes specify the length
+ let l = u16::from_be_bytes([data[2], data[3]]);
+ if l < 256 {
+ // Not shortest possible encoding
+ // Forbidden by DER-format
+ return Err(CryptoError::MalformedInput);
+ }
+ 4 + l as usize
+ } else {
+ // We don't support lengths larger than 2^16 - 1.
+ return Err(CryptoError::MalformedInput);
+ };
+
+ if data.len() < end {
+ return Err(CryptoError::MalformedInput);
+ }
+
+ Ok(U2FRegisterAnswer {
+ certificate: &data[0..end],
+ signature: &data[end..],
+ })
+}
+
+#[cfg(all(test, not(feature = "crypto_dummy")))]
+mod test {
+ use super::{
+ authenticate, decrypt, encrypt, imp::parse_key, imp::test_encapsulate, serialize_key,
+ COSEAlgorithm, COSEKey, ECDSACurve,
+ };
+ use crate::crypto::{COSEEC2Key, COSEKeyType};
+ use crate::ctap2::commands::client_pin::Pin;
+ use crate::util::decode_hex;
+ use serde_cbor::de::from_slice;
+
+ #[test]
+ fn test_serialize_key() {
+ let x = [
+ 0xfc, 0x9e, 0xd3, 0x6f, 0x7c, 0x1a, 0xa9, 0x15, 0xce, 0x3e, 0xa1, 0x77, 0xf0, 0x75,
+ 0x67, 0xf0, 0x7f, 0x16, 0xf9, 0x47, 0x9d, 0x95, 0xad, 0x8e, 0xd4, 0x97, 0x1d, 0x33,
+ 0x05, 0xe3, 0x1a, 0x80,
+ ];
+ let y = [
+ 0x50, 0xb7, 0x33, 0xaf, 0x8c, 0x0b, 0x0e, 0xe1, 0xda, 0x8d, 0xe0, 0xac, 0xf9, 0xd8,
+ 0xe1, 0x32, 0x82, 0xf0, 0x63, 0xb7, 0xb3, 0x0d, 0x73, 0xd4, 0xd3, 0x2c, 0x9a, 0xad,
+ 0x6d, 0xfa, 0x8b, 0x27,
+ ];
+ let serialized_key = [
+ 0x04, 0xfc, 0x9e, 0xd3, 0x6f, 0x7c, 0x1a, 0xa9, 0x15, 0xce, 0x3e, 0xa1, 0x77, 0xf0,
+ 0x75, 0x67, 0xf0, 0x7f, 0x16, 0xf9, 0x47, 0x9d, 0x95, 0xad, 0x8e, 0xd4, 0x97, 0x1d,
+ 0x33, 0x05, 0xe3, 0x1a, 0x80, 0x50, 0xb7, 0x33, 0xaf, 0x8c, 0x0b, 0x0e, 0xe1, 0xda,
+ 0x8d, 0xe0, 0xac, 0xf9, 0xd8, 0xe1, 0x32, 0x82, 0xf0, 0x63, 0xb7, 0xb3, 0x0d, 0x73,
+ 0xd4, 0xd3, 0x2c, 0x9a, 0xad, 0x6d, 0xfa, 0x8b, 0x27,
+ ];
+
+ let (res_x, res_y) =
+ serialize_key(ECDSACurve::SECP256R1, &serialized_key).expect("Failed to serialize key");
+ assert_eq!(res_x, x);
+ assert_eq!(res_y, y);
+
+ let res_key = parse_key(ECDSACurve::SECP256R1, &x, &y).expect("Failed to parse key");
+
+ assert_eq!(res_key, serialized_key)
+ }
+
+ #[test]
+ fn test_parse_es256_serialize_key() {
+ // Test values taken from https://github.com/Yubico/python-fido2/blob/master/test/test_cose.py
+ let key_data = decode_hex("A5010203262001215820A5FD5CE1B1C458C530A54FA61B31BF6B04BE8B97AFDE54DD8CBB69275A8A1BE1225820FA3A3231DD9DEED9D1897BE5A6228C59501E4BCD12975D3DFF730F01278EA61C");
+ let key: COSEKey = from_slice(&key_data).unwrap();
+ assert_eq!(key.alg, COSEAlgorithm::ES256);
+ if let COSEKeyType::EC2(ec2key) = &key.key {
+ assert_eq!(ec2key.curve, ECDSACurve::SECP256R1);
+ assert_eq!(
+ ec2key.x,
+ decode_hex("A5FD5CE1B1C458C530A54FA61B31BF6B04BE8B97AFDE54DD8CBB69275A8A1BE1")
+ );
+ assert_eq!(
+ ec2key.y,
+ decode_hex("FA3A3231DD9DEED9D1897BE5A6228C59501E4BCD12975D3DFF730F01278EA61C")
+ );
+ } else {
+ panic!("Wrong key type!");
+ }
+
+ let serialized = serde_cbor::to_vec(&key).expect("Failed to serialize key");
+ assert_eq!(key_data, serialized);
+ }
+
+ #[test]
+ #[allow(non_snake_case)]
+ fn test_shared_secret() {
+ // Test values taken from https://github.com/Yubico/python-fido2/blob/master/test/test_ctap2.py
+ let EC_PRIV =
+ decode_hex("7452E599FEE739D8A653F6A507343D12D382249108A651402520B72F24FE7684");
+ let EC_PUB_X =
+ decode_hex("44D78D7989B97E62EA993496C9EF6E8FD58B8B00715F9A89153DDD9C4657E47F");
+ let EC_PUB_Y =
+ decode_hex("EC802EE7D22BD4E100F12E48537EB4E7E96ED3A47A0A3BD5F5EEAB65001664F9");
+ let DEV_PUB_X =
+ decode_hex("0501D5BC78DA9252560A26CB08FCC60CBE0B6D3B8E1D1FCEE514FAC0AF675168");
+ let DEV_PUB_Y =
+ decode_hex("D551B3ED46F665731F95B4532939C25D91DB7EB844BD96D4ABD4083785F8DF47");
+ let SHARED = decode_hex("c42a039d548100dfba521e487debcbbb8b66bb7496f8b1862a7a395ed83e1a1c");
+ let TOKEN_ENC = decode_hex("7A9F98E31B77BE90F9C64D12E9635040");
+ let TOKEN = decode_hex("aff12c6dcfbf9df52f7a09211e8865cd");
+ let PIN_HASH_ENC = decode_hex("afe8327ce416da8ee3d057589c2ce1a9");
+
+ // let peer_key = parse_key(ECDSACurve::SECP256R1, &DEV_PUB_X, &DEV_PUB_Y).unwrap();
+ let my_pub_key_data = parse_key(ECDSACurve::SECP256R1, &EC_PUB_X, &EC_PUB_Y).unwrap();
+
+ let peer_key = COSEEC2Key {
+ curve: ECDSACurve::SECP256R1,
+ x: DEV_PUB_X,
+ y: DEV_PUB_Y,
+ };
+
+ // let my_pub_key = COSEKey {
+ // alg: COSEAlgorithm::ES256,
+ // key: COSEKeyType::EC2(COSEEC2Key {
+ // curve: ECDSACurve::SECP256R1,
+ // x: EC_PUB_X,
+ // y: EC_PUB_Y,
+ // }),
+ // };
+ // We are using `test_encapsulate()` here, because we need a way to hand in the private key
+ // which would be generated on the fly otherwise (ephemeral keys), to predict the outputs
+ let shared_secret =
+ test_encapsulate(&peer_key, COSEAlgorithm::ES256, &my_pub_key_data, &EC_PRIV).unwrap();
+ assert_eq!(shared_secret.shared_secret, SHARED);
+
+ let token_enc = encrypt(&shared_secret.shared_secret(), &TOKEN).unwrap();
+ assert_eq!(token_enc, TOKEN_ENC);
+
+ let token = decrypt(&shared_secret.shared_secret(), &TOKEN_ENC).unwrap();
+ assert_eq!(token, TOKEN);
+
+ let pin = Pin::new("1234");
+ let pin_hash_enc =
+ encrypt(&shared_secret.shared_secret(), pin.for_pin_token().as_ref()).unwrap();
+ assert_eq!(pin_hash_enc, PIN_HASH_ENC);
+ }
+
+ #[test]
+ fn test_authenticate() {
+ let key = "key";
+ let message = "The quick brown fox jumps over the lazy dog";
+ let expected =
+ decode_hex("f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8");
+
+ let result =
+ authenticate(key.as_bytes(), message.as_bytes()).expect("Failed to authenticate");
+ assert_eq!(result, expected);
+
+ let key = "The quick brown fox jumps over the lazy dogThe quick brown fox jumps over the lazy dog";
+ let message = "message";
+ let expected =
+ decode_hex("5597b93a2843078cbb0c920ae41dfe20f1685e10c67e423c11ab91adfc319d12");
+
+ let result =
+ authenticate(key.as_bytes(), message.as_bytes()).expect("Failed to authenticate");
+ assert_eq!(result, expected);
+ }
+
+ #[test]
+ fn test_pin_encryption_and_hashing() {
+ let pin = "1234";
+
+ let shared_secret = vec![
+ 0x82, 0xE3, 0xD8, 0x41, 0xE2, 0x5C, 0x5C, 0x13, 0x46, 0x2C, 0x12, 0x3C, 0xC3, 0xD3,
+ 0x98, 0x78, 0x65, 0xBA, 0x3D, 0x20, 0x46, 0x74, 0xFB, 0xED, 0xD4, 0x7E, 0xF5, 0xAB,
+ 0xAB, 0x8D, 0x13, 0x72,
+ ];
+ let expected_new_pin_enc = vec![
+ 0x70, 0x66, 0x4B, 0xB5, 0x81, 0xE2, 0x57, 0x45, 0x1A, 0x3A, 0xB9, 0x1B, 0xF1, 0xAA,
+ 0xD8, 0xE4, 0x5F, 0x6C, 0xE9, 0xB5, 0xC3, 0xB0, 0xF3, 0x2B, 0x5E, 0xCD, 0x62, 0xD0,
+ 0xBA, 0x3B, 0x60, 0x5F, 0xD9, 0x18, 0x31, 0x66, 0xF6, 0xC5, 0xFA, 0xF3, 0xE4, 0xDA,
+ 0x24, 0x81, 0x50, 0x2C, 0xD0, 0xCE, 0xE0, 0x15, 0x8B, 0x35, 0x1F, 0xC3, 0x92, 0x08,
+ 0xA7, 0x7C, 0xB2, 0x74, 0x4B, 0xD4, 0x3C, 0xF9,
+ ];
+ let expected_pin_auth = vec![
+ 0x8E, 0x7F, 0x01, 0x69, 0x97, 0xF3, 0xB0, 0xA2, 0x7B, 0xA4, 0x34, 0x7A, 0x0E, 0x49,
+ 0xFD, 0xF5,
+ ];
+
+ // Padding to 64 bytes
+ let input: Vec<u8> = pin
+ .as_bytes()
+ .iter()
+ .chain(std::iter::repeat(&0x00))
+ .take(64)
+ .cloned()
+ .collect();
+
+ let new_pin_enc = encrypt(&shared_secret, &input).expect("Failed to encrypt pin");
+ assert_eq!(new_pin_enc, expected_new_pin_enc);
+
+ let pin_auth = authenticate(&shared_secret, &new_pin_enc).expect("Failed to authenticate");
+ assert_eq!(pin_auth[0..16], expected_pin_auth);
+ }
+}
diff --git a/third_party/rust/authenticator/src/crypto/nss.rs b/third_party/rust/authenticator/src/crypto/nss.rs
new file mode 100644
index 0000000000..c469fd7d06
--- /dev/null
+++ b/third_party/rust/authenticator/src/crypto/nss.rs
@@ -0,0 +1,543 @@
+use super::{COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, ECDHSecret, ECDSACurve};
+use nss_gk_api::p11::{
+ PK11Origin, PK11_CreateContextBySymKey, PK11_Decrypt, PK11_DigestFinal, PK11_DigestOp,
+ PK11_Encrypt, PK11_GenerateKeyPairWithOpFlags, PK11_HashBuf, PK11_ImportSymKey,
+ PK11_PubDeriveWithKDF, PrivateKey, PublicKey, SECKEY_DecodeDERSubjectPublicKeyInfo,
+ SECKEY_ExtractPublicKey, SECOidTag, Slot, SubjectPublicKeyInfo, AES_BLOCK_SIZE,
+ PK11_ATTR_SESSION, SHA256_LENGTH,
+};
+use nss_gk_api::{Error as NSSError, IntoResult, SECItem, SECItemBorrowed, PR_FALSE};
+use pkcs11_bindings::{
+ CKA_DERIVE, CKA_ENCRYPT, CKA_SIGN, CKD_NULL, CKF_DERIVE, CKM_AES_CBC, CKM_ECDH1_DERIVE,
+ CKM_EC_KEY_PAIR_GEN, CKM_SHA256_HMAC, CKM_SHA512_HMAC,
+};
+use serde::Serialize;
+use serde_bytes::ByteBuf;
+use std::convert::{TryFrom, TryInto};
+use std::num::TryFromIntError;
+use std::os::raw::c_uint;
+use std::ptr;
+
+#[cfg(test)]
+use nss_gk_api::p11::{PK11_ImportDERPrivateKeyInfoAndReturnKey, SECKEY_ConvertToPublicKey};
+
+/// Errors that can be returned from COSE functions.
+#[derive(Clone, Debug, Serialize)]
+pub enum BackendError {
+ NSSError(String),
+ TryFromError,
+ UnsupportedAlgorithm(COSEAlgorithm),
+ UnsupportedCurve(ECDSACurve),
+ UnsupportedKeyType,
+}
+
+impl From<NSSError> for BackendError {
+ fn from(e: NSSError) -> Self {
+ BackendError::NSSError(format!("{}", e))
+ }
+}
+
+impl From<TryFromIntError> for BackendError {
+ fn from(_: TryFromIntError) -> Self {
+ BackendError::TryFromError
+ }
+}
+
+pub type Result<T> = std::result::Result<T, BackendError>;
+
+// Object identifiers in DER tag-length-value form
+
+const DER_OID_EC_PUBLIC_KEY_BYTES: &[u8] = &[
+ 0x06, 0x07,
+ /* {iso(1) member-body(2) us(840) ansi-x962(10045) keyType(2) ecPublicKey(1)} */
+ 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01,
+];
+
+const DER_OID_P256_BYTES: &[u8] = &[
+ 0x06, 0x08,
+ /* {iso(1) member-body(2) us(840) ansi-x962(10045) curves(3) prime(1) prime256v1(7)} */
+ 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07,
+];
+const DER_OID_P384_BYTES: &[u8] = &[
+ 0x06, 0x05,
+ /* {iso(1) identified-organization(3) certicom(132) curve(0) ansip384r1(34)} */
+ 0x2b, 0x81, 0x04, 0x00, 0x22,
+];
+const DER_OID_P521_BYTES: &[u8] = &[
+ 0x06, 0x05,
+ /* {iso(1) identified-organization(3) certicom(132) curve(0) ansip521r1(35)} */
+ 0x2b, 0x81, 0x04, 0x00, 0x23,
+];
+
+/* From CTAP2.1 spec:
+
+initialize()
+
+ This is run by the platform when starting a series of transactions with a specific authenticator.
+encapsulate(peerCoseKey) → (coseKey, sharedSecret) | error
+
+ Generates an encapsulation for the authenticator’s public key and returns the message to transmit and the shared secret.
+encrypt(key, demPlaintext) → ciphertext
+
+ Encrypts a plaintext to produce a ciphertext, which may be longer than the plaintext. The plaintext is restricted to being a multiple of the AES block size (16 bytes) in length.
+decrypt(key, ciphertext) → plaintext | error
+
+ Decrypts a ciphertext and returns the plaintext.
+authenticate(key, message) → signature
+
+ Computes a MAC of the given message.
+*/
+
+// TODO(MS): Maybe remove ByteBuf and return Vec<u8>'s instead for a cleaner interface
+pub(crate) fn serialize_key(_curve: ECDSACurve, key: &[u8]) -> Result<(ByteBuf, ByteBuf)> {
+ // TODO(MS): I actually have NO idea how to do this with NSS
+ let length = key[1..].len() / 2;
+ let chunks: Vec<_> = key[1..].chunks_exact(length).collect();
+ Ok((
+ ByteBuf::from(chunks[0].to_vec()),
+ ByteBuf::from(chunks[1].to_vec()),
+ ))
+}
+
+pub(crate) fn parse_key(_curve: ECDSACurve, x: &[u8], y: &[u8]) -> Result<Vec<u8>> {
+ if x.len() != y.len() {
+ return Err(BackendError::NSSError(
+ "EC coordinates not equally long".to_string(),
+ ));
+ }
+ let mut buf = Vec::with_capacity(2 * x.len() + 1);
+ // The uncompressed point format is defined in Section 2.3.3 of "SEC 1: Elliptic Curve
+ // Cryptography" https://www.secg.org/sec1-v2.pdf.
+ buf.push(0x04);
+ buf.extend_from_slice(x);
+ buf.extend_from_slice(y);
+ Ok(buf)
+}
+
+fn der_spki_from_cose(cose_key: &COSEKey) -> Result<Vec<u8>> {
+ let ec2key = match cose_key.key {
+ COSEKeyType::EC2(ref ec2key) => ec2key,
+ _ => return Err(BackendError::UnsupportedKeyType),
+ };
+
+ let (curve_oid, seq_len, alg_len, spk_len) = match ec2key.curve {
+ ECDSACurve::SECP256R1 => (
+ DER_OID_P256_BYTES,
+ [0x59].as_slice(),
+ [0x13].as_slice(),
+ [0x42].as_slice(),
+ ),
+ ECDSACurve::SECP384R1 => (
+ DER_OID_P384_BYTES,
+ [0x76].as_slice(),
+ [0x10].as_slice(),
+ [0x62].as_slice(),
+ ),
+ ECDSACurve::SECP521R1 => (
+ DER_OID_P521_BYTES,
+ [0x81, 0x9b].as_slice(),
+ [0x10].as_slice(),
+ [0x8a, 0xdf].as_slice(),
+ ),
+ x => return Err(BackendError::UnsupportedCurve(x)),
+ };
+
+ let cose_key_sec1 = parse_key(ec2key.curve, &ec2key.x, &ec2key.y)?;
+
+ // [RFC 5280]
+ let mut spki: Vec<u8> = vec![];
+ // SubjectPublicKeyInfo
+ spki.push(0x30);
+ spki.extend_from_slice(seq_len);
+ // AlgorithmIdentifier
+ spki.push(0x30);
+ spki.extend_from_slice(alg_len);
+ // ObjectIdentifier
+ spki.extend_from_slice(DER_OID_EC_PUBLIC_KEY_BYTES);
+ // RFC 5480 ECParameters
+ spki.extend_from_slice(curve_oid);
+ // BIT STRING encoding uncompressed SEC1 public point
+ spki.push(0x03);
+ spki.extend_from_slice(spk_len);
+ spki.push(0x0); // no trailing zeros
+ spki.extend_from_slice(&cose_key_sec1);
+
+ Ok(spki)
+}
+
+/// This is run by the platform when starting a series of transactions with a specific authenticator.
+//pub(crate) fn initialize() { }
+
+/// Generates an encapsulation for the authenticator's public key and returns the message
+/// to transmit and the shared secret.
+///
+/// `peer_cose_key` is the authenticator's (peer's) public key.
+pub(crate) fn encapsulate(peer_cose_key: &COSEKey) -> Result<ECDHSecret> {
+ nss_gk_api::init();
+ // Generate an ephmeral keypair to do ECDH with the authenticator.
+ // This is "platformKeyAgreementKey".
+ let ec2key = match peer_cose_key.key {
+ COSEKeyType::EC2(ref ec2key) => ec2key,
+ _ => return Err(BackendError::UnsupportedKeyType),
+ };
+
+ let mut oid = match ec2key.curve {
+ ECDSACurve::SECP256R1 => SECItemBorrowed::wrap(DER_OID_P256_BYTES),
+ ECDSACurve::SECP384R1 => SECItemBorrowed::wrap(DER_OID_P384_BYTES),
+ ECDSACurve::SECP521R1 => SECItemBorrowed::wrap(DER_OID_P521_BYTES),
+ x => return Err(BackendError::UnsupportedCurve(x)),
+ };
+ let oid_ptr: *mut SECItem = oid.as_mut();
+
+ let slot = Slot::internal()?;
+
+ let mut client_public_ptr = ptr::null_mut();
+
+ // We have to be careful with error handling between the `PK11_GenerateKeyPairWithOpFlags` and
+ // `PublicKey::from_ptr` calls here, so I've wrapped them in the same unsafe block as a
+ // warning. TODO(jms) Replace this once there is a safer alternative.
+ // https://github.com/mozilla/nss-gk-api/issues/1
+ let (client_private, client_public) = unsafe {
+ let client_private =
+ // Type of `param` argument depends on mechanism. For EC keygen it is
+ // `SECKEYECParams *` which is a typedef for `SECItem *`.
+ PK11_GenerateKeyPairWithOpFlags(
+ *slot,
+ CKM_EC_KEY_PAIR_GEN,
+ oid_ptr.cast(),
+ &mut client_public_ptr,
+ PK11_ATTR_SESSION,
+ CKF_DERIVE,
+ CKF_DERIVE,
+ ptr::null_mut(),
+ )
+ .into_result()?;
+
+ let client_public = PublicKey::from_ptr(client_public_ptr)?;
+
+ (client_private, client_public)
+ };
+
+ let peer_spki = der_spki_from_cose(peer_cose_key)?;
+ let peer_public = nss_public_key_from_der_spki(&peer_spki)?;
+ let shared_secret = encapsulate_helper(peer_public, client_private)?;
+
+ let client_cose_key = cose_key_from_nss_public(peer_cose_key.alg, ec2key.curve, client_public)?;
+
+ Ok(ECDHSecret {
+ remote: COSEKey {
+ alg: peer_cose_key.alg,
+ key: COSEKeyType::EC2(ec2key.clone()),
+ },
+ my: client_cose_key,
+ shared_secret,
+ })
+}
+
+fn nss_public_key_from_der_spki(spki: &[u8]) -> Result<PublicKey> {
+ let mut spki_item = SECItemBorrowed::wrap(&spki);
+ let spki_item_ptr: *mut SECItem = spki_item.as_mut();
+ let nss_spki = unsafe {
+ SubjectPublicKeyInfo::from_ptr(SECKEY_DecodeDERSubjectPublicKeyInfo(spki_item_ptr))?
+ };
+ let public_key = unsafe { PublicKey::from_ptr(SECKEY_ExtractPublicKey(*nss_spki))? };
+ Ok(public_key)
+}
+
+fn cose_key_from_nss_public(
+ alg: COSEAlgorithm,
+ curve: ECDSACurve,
+ nss_public: PublicKey,
+) -> Result<COSEKey> {
+ let public_data = nss_public.key_data()?;
+ let (public_x, public_y) = serialize_key(curve, &public_data)?;
+ Ok(COSEKey {
+ alg,
+ key: COSEKeyType::EC2(COSEEC2Key {
+ curve,
+ x: public_x.to_vec(),
+ y: public_y.to_vec(),
+ }),
+ })
+}
+
+/// `peer_public`: The authenticator's public key.
+/// `client_private`: Our ephemeral private key.
+fn encapsulate_helper(peer_public: PublicKey, client_private: PrivateKey) -> Result<Vec<u8>> {
+ let ecdh_x_coord = unsafe {
+ PK11_PubDeriveWithKDF(
+ *client_private,
+ *peer_public,
+ PR_FALSE,
+ std::ptr::null_mut(),
+ std::ptr::null_mut(),
+ CKM_ECDH1_DERIVE,
+ CKM_SHA512_HMAC, // unused
+ CKA_DERIVE, // unused
+ 0,
+ CKD_NULL,
+ std::ptr::null_mut(),
+ std::ptr::null_mut(),
+ )
+ .into_result()?
+ };
+ let ecdh_x_coord_bytes = ecdh_x_coord.as_bytes()?;
+ let mut shared_secret = [0u8; SHA256_LENGTH];
+ unsafe {
+ PK11_HashBuf(
+ SECOidTag::SEC_OID_SHA256,
+ shared_secret.as_mut_ptr(),
+ ecdh_x_coord_bytes.as_ptr(),
+ ecdh_x_coord_bytes.len() as i32,
+ )
+ };
+ Ok(shared_secret.to_vec())
+}
+
+/// Encrypts a plaintext to produce a ciphertext, which may be longer than the plaintext.
+/// The plaintext is restricted to being a multiple of the AES block size (16 bytes) in length.
+pub(crate) fn encrypt(key: &[u8], data: &[u8]) -> Result<Vec<u8>> {
+ nss_gk_api::init();
+
+ if key.len() != 32 {
+ return Err(BackendError::NSSError(
+ "Invalid AES-256 key length".to_string(),
+ ));
+ }
+
+ // The input must be a multiple of the AES block size, 16
+ if data.len() % AES_BLOCK_SIZE != 0 {
+ return Err(BackendError::NSSError(
+ "Input to encrypt is too long".to_string(),
+ ));
+ }
+ let in_len = c_uint::try_from(data.len())?;
+
+ let slot = Slot::internal()?;
+
+ let sym_key = unsafe {
+ PK11_ImportSymKey(
+ *slot,
+ CKM_AES_CBC,
+ PK11Origin::PK11_OriginUnwrap,
+ CKA_ENCRYPT,
+ SECItemBorrowed::wrap(key).as_mut(),
+ ptr::null_mut(),
+ )
+ .into_result()?
+ };
+
+ let iv = [0u8; AES_BLOCK_SIZE];
+ let mut params = SECItemBorrowed::wrap(&iv);
+ let params_ptr: *mut SECItem = params.as_mut();
+ let mut out_len: c_uint = 0;
+ let mut out = vec![0; in_len as usize];
+ unsafe {
+ PK11_Encrypt(
+ *sym_key,
+ CKM_AES_CBC,
+ params_ptr,
+ out.as_mut_ptr(),
+ &mut out_len,
+ in_len,
+ data.as_ptr(),
+ in_len,
+ )
+ .into_result()?
+ }
+ // CKM_AES_CBC should have output length equal to input length.
+ debug_assert_eq!(out_len, in_len);
+
+ Ok(out)
+}
+
+/// Decrypts a ciphertext and returns the plaintext.
+pub(crate) fn decrypt(key: &[u8], data: &[u8]) -> Result<Vec<u8>> {
+ nss_gk_api::init();
+ let slot = Slot::internal()?;
+
+ if key.len() != 32 {
+ return Err(BackendError::NSSError(
+ "Invalid AES-256 key length".to_string(),
+ ));
+ }
+
+ // The input must be a multiple of the AES block size, 16
+ if data.len() % AES_BLOCK_SIZE != 0 {
+ return Err(BackendError::NSSError(
+ "Invalid input to decrypt".to_string(),
+ ));
+ }
+ let in_len = c_uint::try_from(data.len())?;
+
+ let sym_key = unsafe {
+ PK11_ImportSymKey(
+ *slot,
+ CKM_AES_CBC,
+ PK11Origin::PK11_OriginUnwrap,
+ CKA_ENCRYPT,
+ SECItemBorrowed::wrap(key).as_mut(),
+ ptr::null_mut(),
+ )
+ .into_result()?
+ };
+
+ let iv = [0u8; AES_BLOCK_SIZE];
+ let mut params = SECItemBorrowed::wrap(&iv);
+ let params_ptr: *mut SECItem = params.as_mut();
+ let mut out_len: c_uint = 0;
+ let mut out = vec![0; in_len as usize];
+ unsafe {
+ PK11_Decrypt(
+ *sym_key,
+ CKM_AES_CBC,
+ params_ptr,
+ out.as_mut_ptr(),
+ &mut out_len,
+ in_len,
+ data.as_ptr(),
+ in_len,
+ )
+ .into_result()?
+ }
+ // CKM_AES_CBC should have output length equal to input length.
+ debug_assert_eq!(out_len, in_len);
+
+ Ok(out)
+}
+
+/// Computes a MAC of the given message.
+pub(crate) fn authenticate(token: &[u8], input: &[u8]) -> Result<Vec<u8>> {
+ nss_gk_api::init();
+ let slot = Slot::internal()?;
+ let sym_key = unsafe {
+ PK11_ImportSymKey(
+ *slot,
+ CKM_SHA256_HMAC,
+ PK11Origin::PK11_OriginUnwrap,
+ CKA_SIGN,
+ SECItemBorrowed::wrap(token).as_mut(),
+ ptr::null_mut(),
+ )
+ .into_result()?
+ };
+ let param = SECItemBorrowed::make_empty();
+ let context = unsafe {
+ PK11_CreateContextBySymKey(CKM_SHA256_HMAC, CKA_SIGN, *sym_key, param.as_ref())
+ .into_result()?
+ };
+ unsafe { PK11_DigestOp(*context, input.as_ptr(), input.len().try_into()?).into_result()? };
+ let mut digest = vec![0u8; 32];
+ let mut digest_len = 0u32;
+ unsafe {
+ PK11_DigestFinal(
+ *context,
+ digest.as_mut_ptr(),
+ &mut digest_len,
+ digest.len() as u32,
+ )
+ .into_result()?
+ }
+ assert_eq!(digest_len, 32);
+ Ok(digest)
+}
+
+#[cfg(test)]
+pub(crate) fn test_encapsulate(
+ peer_coseec2_key: &COSEEC2Key,
+ alg: COSEAlgorithm,
+ my_pub_key: &[u8],
+ my_priv_key: &[u8],
+) -> Result<ECDHSecret> {
+ nss_gk_api::init();
+
+ let peer_cose_key = COSEKey {
+ alg: alg,
+ key: COSEKeyType::EC2(peer_coseec2_key.clone()),
+ };
+ let spki = der_spki_from_cose(&peer_cose_key)?;
+ let peer_public = nss_public_key_from_der_spki(&spki)?;
+
+ /* NSS has no mechanism to import a raw elliptic curve coordinate as a private key.
+ * We need to encode it in a key storage format such as PKCS#8. To avoid a dependency
+ * on an ASN.1 encoder for this test, we'll do it manually. */
+ let pkcs8_private_key_info_version = &[0x02, 0x01, 0x00];
+ let rfc5915_ec_private_key_version = &[0x02, 0x01, 0x01];
+
+ let (curve_oid, seq_len, alg_len, attr_len, ecpriv_len, param_len, spk_len) =
+ match peer_coseec2_key.curve {
+ ECDSACurve::SECP256R1 => (
+ DER_OID_P256_BYTES,
+ [0x81, 0x87].as_slice(),
+ [0x13].as_slice(),
+ [0x6d].as_slice(),
+ [0x6b].as_slice(),
+ [0x44].as_slice(),
+ [0x42].as_slice(),
+ ),
+ x => return Err(BackendError::UnsupportedCurve(x)),
+ };
+
+ let priv_len = my_priv_key.len() as u8; // < 127
+
+ let mut pkcs8_priv: Vec<u8> = vec![];
+ // RFC 5208 PrivateKeyInfo
+ pkcs8_priv.push(0x30);
+ pkcs8_priv.extend_from_slice(seq_len);
+ // Integer (0)
+ pkcs8_priv.extend_from_slice(pkcs8_private_key_info_version);
+ // AlgorithmIdentifier
+ pkcs8_priv.push(0x30);
+ pkcs8_priv.extend_from_slice(alg_len);
+ // ObjectIdentifier
+ pkcs8_priv.extend_from_slice(DER_OID_EC_PUBLIC_KEY_BYTES);
+ // RFC 5480 ECParameters
+ pkcs8_priv.extend_from_slice(DER_OID_P256_BYTES);
+ // Attributes
+ pkcs8_priv.push(0x04);
+ pkcs8_priv.extend_from_slice(attr_len);
+ // RFC 5915 ECPrivateKey
+ pkcs8_priv.push(0x30);
+ pkcs8_priv.extend_from_slice(ecpriv_len);
+ pkcs8_priv.extend_from_slice(rfc5915_ec_private_key_version);
+ pkcs8_priv.push(0x04);
+ pkcs8_priv.push(priv_len);
+ pkcs8_priv.extend_from_slice(my_priv_key);
+ pkcs8_priv.push(0xa1);
+ pkcs8_priv.extend_from_slice(param_len);
+ pkcs8_priv.push(0x03);
+ pkcs8_priv.extend_from_slice(spk_len);
+ pkcs8_priv.push(0x0);
+ pkcs8_priv.extend_from_slice(&my_pub_key);
+
+ // Now we can import the private key.
+ let slot = Slot::internal()?;
+ let mut pkcs8_priv_item = SECItemBorrowed::wrap(&pkcs8_priv);
+ let pkcs8_priv_item_ptr: *mut SECItem = pkcs8_priv_item.as_mut();
+ let mut client_private_ptr = ptr::null_mut();
+ unsafe {
+ PK11_ImportDERPrivateKeyInfoAndReturnKey(
+ *slot,
+ pkcs8_priv_item_ptr,
+ ptr::null_mut(),
+ ptr::null_mut(),
+ PR_FALSE,
+ PR_FALSE,
+ 255, /* todo: expose KU_ flags in nss-gk-api */
+ &mut client_private_ptr,
+ ptr::null_mut(),
+ )
+ };
+
+ let client_private = unsafe { PrivateKey::from_ptr(client_private_ptr) }?;
+ let client_public = unsafe { PublicKey::from_ptr(SECKEY_ConvertToPublicKey(*client_private))? };
+ let client_cose_key = cose_key_from_nss_public(alg, peer_coseec2_key.curve, client_public)?;
+
+ let shared_secret = encapsulate_helper(peer_public, client_private)?;
+
+ Ok(ECDHSecret {
+ remote: peer_cose_key,
+ my: client_cose_key,
+ shared_secret,
+ })
+}
diff --git a/third_party/rust/authenticator/src/crypto/openssl.rs b/third_party/rust/authenticator/src/crypto/openssl.rs
new file mode 100644
index 0000000000..28a09bd3eb
--- /dev/null
+++ b/third_party/rust/authenticator/src/crypto/openssl.rs
@@ -0,0 +1,283 @@
+use super::{
+ /*Signature,*/ COSEAlgorithm, COSEEC2Key, /*PlainText*/ COSEKey, COSEKeyType,
+ /*CypherText,*/ ECDHSecret, ECDSACurve,
+};
+use openssl::bn::{BigNum, BigNumContext};
+use openssl::derive::Deriver;
+#[cfg(test)]
+use openssl::ec::PointConversionForm;
+use openssl::ec::{EcGroup, EcKey, EcPoint};
+use openssl::error::ErrorStack;
+use openssl::hash::{hash, MessageDigest};
+use openssl::nid::Nid;
+use openssl::pkey::{PKey, Private};
+use openssl::sign::{Signer, Verifier};
+use openssl::symm::{Cipher, Crypter, Mode};
+use openssl::x509::X509;
+use serde::{Serialize, Serializer};
+use serde_bytes::ByteBuf;
+
+fn openssl_string<S>(_: &ErrorStack, s: S) -> std::result::Result<S::Ok, S::Error>
+where
+ S: Serializer,
+{
+ s.serialize_str("OpenSSLError")
+}
+
+/// Errors that can be returned from COSE functions.
+#[derive(Clone, Debug, Serialize)]
+pub enum BackendError {
+ #[serde(serialize_with = "openssl_string")]
+ OpenSSL(ErrorStack),
+ UnsupportedCurve(ECDSACurve),
+ UnsupportedKeyType,
+}
+
+impl From<ErrorStack> for BackendError {
+ fn from(e: ErrorStack) -> Self {
+ BackendError::OpenSSL(e)
+ }
+}
+
+impl From<&ErrorStack> for BackendError {
+ fn from(e: &ErrorStack) -> Self {
+ BackendError::OpenSSL(e.clone())
+ }
+}
+
+pub type Result<T> = std::result::Result<T, BackendError>;
+
+/* From CTAP2.1 spec:
+
+initialize()
+
+ This is run by the platform when starting a series of transactions with a specific authenticator.
+encapsulate(peerCoseKey) → (coseKey, sharedSecret) | error
+
+ Generates an encapsulation for the authenticator’s public key and returns the message to transmit and the shared secret.
+encrypt(key, demPlaintext) → ciphertext
+
+ Encrypts a plaintext to produce a ciphertext, which may be longer than the plaintext. The plaintext is restricted to being a multiple of the AES block size (16 bytes) in length.
+decrypt(key, ciphertext) → plaintext | error
+
+ Decrypts a ciphertext and returns the plaintext.
+authenticate(key, message) → signature
+
+ Computes a MAC of the given message.
+*/
+
+fn to_openssl_name(curve: ECDSACurve) -> Result<Nid> {
+ match curve {
+ ECDSACurve::SECP256R1 => Ok(Nid::X9_62_PRIME256V1),
+ ECDSACurve::SECP384R1 => Ok(Nid::SECP384R1),
+ ECDSACurve::SECP521R1 => Ok(Nid::SECP521R1),
+ x => Err(BackendError::UnsupportedCurve(x)),
+ }
+}
+
+fn affine_coordinates(curve: ECDSACurve, bytes: &[u8]) -> Result<(ByteBuf, ByteBuf)> {
+ let name = to_openssl_name(curve)?;
+ let group = EcGroup::from_curve_name(name)?;
+
+ let mut ctx = BigNumContext::new()?;
+ let point = EcPoint::from_bytes(&group, bytes, &mut ctx)?;
+
+ let mut x = BigNum::new()?;
+ let mut y = BigNum::new()?;
+
+ point.affine_coordinates_gfp(&group, &mut x, &mut y, &mut ctx)?;
+ //point.affine_coordinates_gf2m(&group, &mut x, &mut y, &mut ctx)?;
+
+ Ok((ByteBuf::from(x.to_vec()), ByteBuf::from(y.to_vec())))
+}
+
+// TODO(MS): Maybe remove ByteBuf and return Vec<u8>'s instead for a cleaner interface
+pub(crate) fn serialize_key(curve: ECDSACurve, key: &[u8]) -> Result<(ByteBuf, ByteBuf)> {
+ affine_coordinates(curve, key)
+}
+
+#[cfg(test)]
+pub(crate) fn parse_key(curve: ECDSACurve, x: &[u8], y: &[u8]) -> Result<Vec<u8>> {
+ let name = to_openssl_name(curve)?;
+ let group = EcGroup::from_curve_name(name)?;
+
+ let mut ctx = BigNumContext::new()?;
+ let x = BigNum::from_slice(x)?;
+ let y = BigNum::from_slice(y)?;
+
+ let key = EcKey::from_public_key_affine_coordinates(&group, &x, &y)?;
+ // TODO(baloo): what is uncompressed?!
+ let pub_key = key.public_key();
+
+ Ok(pub_key.to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)?)
+}
+
+/// This is run by the platform when starting a series of transactions with a specific authenticator.
+//pub(crate) fn initialize() {
+//
+//}
+
+/// Generates an encapsulation for the authenticator’s public key and returns the message
+/// to transmit and the shared secret.
+pub(crate) fn encapsulate(key: &COSEKey) -> Result<ECDHSecret> {
+ if let COSEKeyType::EC2(ec2key) = &key.key {
+ let curve_name = to_openssl_name(ec2key.curve)?;
+ let group = EcGroup::from_curve_name(curve_name)?;
+ let my_key = EcKey::generate(&group)?;
+
+ encapsulate_helper(&ec2key, key.alg, group, my_key)
+ } else {
+ Err(BackendError::UnsupportedKeyType)
+ }
+}
+
+pub(crate) fn encapsulate_helper(
+ key: &COSEEC2Key,
+ alg: COSEAlgorithm,
+ group: EcGroup,
+ my_key: EcKey<Private>,
+) -> Result<ECDHSecret> {
+ let mut ctx = BigNumContext::new()?;
+ let mut x = BigNum::new()?;
+ let mut y = BigNum::new()?;
+
+ my_key
+ .public_key()
+ .affine_coordinates_gfp(&group, &mut x, &mut y, &mut ctx)?;
+
+ let my_public_key = COSEKey {
+ alg,
+ key: COSEKeyType::EC2(COSEEC2Key {
+ curve: key.curve.clone(),
+ x: x.to_vec(),
+ y: y.to_vec(),
+ }),
+ };
+
+ // let point = EcPoint::from_bytes(&group, &key.key, &mut ctx)?;
+ let peer_public_key = PKey::from_ec_key(EcKey::from_public_key_affine_coordinates(
+ &group,
+ BigNum::from_slice(&key.x).as_ref()?,
+ BigNum::from_slice(&key.y).as_ref()?,
+ )?)?;
+
+ let my_ec_key = PKey::from_ec_key(my_key)?;
+ let mut deriver = Deriver::new(my_ec_key.as_ref())?;
+ deriver.set_peer(&peer_public_key)?;
+ let shared_sec = deriver.derive_to_vec()?;
+
+ // Hashing the key material
+ let digest = hash(MessageDigest::sha256(), &shared_sec)?;
+
+ Ok(ECDHSecret {
+ remote: COSEKey {
+ alg,
+ key: COSEKeyType::EC2(key.clone()),
+ },
+ my: my_public_key,
+ shared_secret: digest.as_ref().to_vec(),
+ })
+}
+
+#[cfg(test)]
+pub(crate) fn test_encapsulate(
+ key: &COSEEC2Key,
+ alg: COSEAlgorithm,
+ my_pub_key: &[u8],
+ my_priv_key: &[u8],
+) -> Result<ECDHSecret> {
+ let curve_name = to_openssl_name(key.curve)?;
+ let group = EcGroup::from_curve_name(curve_name)?;
+
+ let mut ctx = BigNumContext::new()?;
+ let my_pub_point = EcPoint::from_bytes(&group, &my_pub_key, &mut ctx)?;
+ let my_priv_bignum = BigNum::from_slice(my_priv_key)?;
+ let my_key = EcKey::from_private_components(&group, &my_priv_bignum, &my_pub_point)?;
+
+ encapsulate_helper(key, alg, group, my_key)
+}
+
+/// Encrypts a plaintext to produce a ciphertext, which may be longer than the plaintext.
+/// The plaintext is restricted to being a multiple of the AES block size (16 bytes) in length.
+pub(crate) fn encrypt(
+ key: &[u8],
+ plain_text: &[u8], /*PlainText*/
+) -> Result<Vec<u8> /*CypherText*/> {
+ let cipher = Cipher::aes_256_cbc();
+
+ // TODO(baloo): This might trigger a panic if size is not big enough
+ let mut cypher_text = vec![0; plain_text.len() * 2];
+ cypher_text.resize(plain_text.len() * 2, 0);
+ // Spec says explicitly IV=0
+ let iv = [0u8; 16];
+ let mut encrypter = Crypter::new(cipher, Mode::Encrypt, key, Some(&iv))?;
+ encrypter.pad(false);
+ let mut out_size = 0;
+ out_size += encrypter.update(plain_text, cypher_text.as_mut_slice())?;
+ out_size += encrypter.finalize(cypher_text.as_mut_slice())?;
+ cypher_text.truncate(out_size);
+ Ok(cypher_text)
+}
+
+/// Decrypts a ciphertext and returns the plaintext.
+pub(crate) fn decrypt(
+ key: &[u8],
+ cypher_text: &[u8], /*CypherText*/
+) -> Result<Vec<u8> /*PlainText*/> {
+ let cipher = Cipher::aes_256_cbc();
+
+ // TODO(baloo): This might trigger a panic if size is not big enough
+ let mut plain_text = vec![0; cypher_text.len() * 2];
+ plain_text.resize(cypher_text.len() * 2, 0);
+ // Spec says explicitly IV=0
+ let iv = [0u8; 16];
+ let mut encrypter = Crypter::new(cipher, Mode::Decrypt, key, Some(&iv))?;
+ encrypter.pad(false);
+ let mut out_size = 0;
+ out_size += encrypter.update(cypher_text, plain_text.as_mut_slice())?;
+ out_size += encrypter.finalize(plain_text.as_mut_slice())?;
+ plain_text.truncate(out_size);
+
+ Ok(plain_text)
+}
+
+/// Computes a MAC of the given message.
+pub(crate) fn authenticate(token: &[u8], input: &[u8]) -> Result<Vec<u8>> {
+ // Create a PKey
+ let key = PKey::hmac(token)?;
+
+ // Compute the HMAC
+ let mut signer = Signer::new(MessageDigest::sha256(), &key)?;
+ signer.update(input)?;
+ let hmac = signer.sign_to_vec()?;
+ Ok(hmac)
+}
+
+// Currently unsued, because rc_crypto does not expose PKCS12 of NSS, so we can't parse the cert there
+// To use it in statemachine.rs for example, do:
+// if let Ok(cdhash) = client_data.hash() {
+// let verification_data: Vec<u8> = attestation
+// .auth_data
+// .to_vec()
+// .iter()
+// .chain(cdhash.as_ref().iter())
+// .copied()
+// .collect();
+// let res = attestation.att_statement.verify(&verification_data);
+// ...
+// }
+#[allow(dead_code)]
+pub(crate) fn verify(
+ sig_alg: ECDSACurve,
+ pub_key: &[u8],
+ signature: &[u8],
+ data: &[u8],
+) -> Result<bool> {
+ let _alg = to_openssl_name(sig_alg)?; // TODO(MS): Actually use this to determine the right MessageDigest below
+ let pkey = X509::from_der(&pub_key)?;
+ let pubkey = pkey.public_key()?;
+ let mut verifier = Verifier::new(MessageDigest::sha256(), &pubkey)?;
+ verifier.update(data)?;
+ let res = verifier.verify(signature)?;
+ Ok(res)
+}
diff --git a/third_party/rust/authenticator/src/crypto/ring.rs b/third_party/rust/authenticator/src/crypto/ring.rs
new file mode 100644
index 0000000000..ff8178ae57
--- /dev/null
+++ b/third_party/rust/authenticator/src/crypto/ring.rs
@@ -0,0 +1,168 @@
+use super::{
+ /*Signature,*/ COSEAlgorithm, COSEEC2Key, /*PlainText*/ COSEKey, COSEKeyType,
+ /*CypherText,*/ ECDHSecret, ECDSACurve,
+};
+use ring::agreement::{
+ agree_ephemeral, Algorithm, EphemeralPrivateKey, UnparsedPublicKey, ECDH_P256, ECDH_P384,
+};
+use ring::digest;
+use ring::hmac;
+use ring::rand::SystemRandom;
+use ring::signature::KeyPair;
+use serde::Serialize;
+use serde_bytes::ByteBuf;
+/*
+initialize()
+
+ This is run by the platform when starting a series of transactions with a specific authenticator.
+encapsulate(peerCoseKey) → (coseKey, sharedSecret) | error
+
+ Generates an encapsulation for the authenticator’s public key and returns the message to transmit and the shared secret.
+encrypt(key, demPlaintext) → ciphertext
+
+ Encrypts a plaintext to produce a ciphertext, which may be longer than the plaintext. The plaintext is restricted to being a multiple of the AES block size (16 bytes) in length.
+decrypt(key, ciphertext) → plaintext | error
+
+ Decrypts a ciphertext and returns the plaintext.
+authenticate(key, message) → signature
+
+ Computes a MAC of the given message.
+*/
+
+pub type Result<T> = std::result::Result<T, BackendError>;
+
+#[derive(Clone, Debug, PartialEq, Serialize)]
+pub enum BackendError {
+ AgreementError,
+ UnspecifiedRingError,
+ KeyRejected,
+ UnsupportedKeyType,
+ UnsupportedCurve(ECDSACurve),
+}
+
+fn to_ring_curve(curve: ECDSACurve) -> Result<&'static Algorithm> {
+ match curve {
+ ECDSACurve::SECP256R1 => Ok(&ECDH_P256),
+ ECDSACurve::SECP384R1 => Ok(&ECDH_P384),
+ x => Err(BackendError::UnsupportedCurve(x)),
+ }
+}
+
+impl From<ring::error::Unspecified> for BackendError {
+ fn from(e: ring::error::Unspecified) -> Self {
+ BackendError::UnspecifiedRingError
+ }
+}
+
+impl From<ring::error::KeyRejected> for BackendError {
+ fn from(e: ring::error::KeyRejected) -> Self {
+ BackendError::KeyRejected
+ }
+}
+
+pub(crate) fn parse_key(curve: ECDSACurve, x: &[u8], y: &[u8]) -> Result<Vec<u8>> {
+ compile_error!(
+ "Ring-backend is not yet implemented. Compile with `--features crypto_openssl` for now."
+ )
+}
+
+// TODO(MS): Maybe remove ByteBuf and return Vec<u8>'s instead for a cleaner interface
+pub(crate) fn serialize_key(curve: ECDSACurve, key: &[u8]) -> Result<(ByteBuf, ByteBuf)> {
+ compile_error!(
+ "Ring-backend is not yet implemented. Compile with `--features crypto_openssl` for now."
+ )
+}
+
+/// This is run by the platform when starting a series of transactions with a specific authenticator.
+pub(crate) fn initialize() {
+ compile_error!(
+ "Ring-backend is not yet implemented. Compile with `--features crypto_openssl` for now."
+ )
+}
+
+/// Generates an encapsulation for the authenticator’s public key and returns the message
+/// to transmit and the shared secret.
+pub(crate) fn encapsulate(key: &COSEKey) -> Result<ECDHSecret> {
+ if let COSEKeyType::EC2(ec2key) = &key.key {
+ // let curve_name = to_openssl_name(ec2key.curve)?;
+ // let group = EcGroup::from_curve_name(curve_name)?;
+ // let my_key = EcKey::generate(&group)?;
+
+ // encapsulate_helper(&ec2key, key.alg, group, my_key)
+ let rng = SystemRandom::new();
+ let peer_public_key_alg = to_ring_curve(ec2key.curve)?;
+ let private_key = EphemeralPrivateKey::generate(peer_public_key_alg, &rng)?;
+ let my_public_key = private_key.compute_public_key()?;
+ let (x, y) = serialize_key(ec2key.curve, my_public_key.as_ref())?;
+ let my_public_key = COSEKey {
+ alg: key.alg,
+ key: COSEKeyType::EC2(COSEEC2Key {
+ curve: ec2key.curve,
+ x: x.to_vec(),
+ y: y.to_vec(),
+ }),
+ };
+
+ let key_bytes = parse_key(ec2key.curve, &ec2key.x, &ec2key.y)?;
+ let peer_public_key = UnparsedPublicKey::new(peer_public_key_alg, &key_bytes);
+
+ let shared_secret = agree_ephemeral(private_key, &peer_public_key, (), |key_material| {
+ let digest = digest::digest(&digest::SHA256, key_material);
+ Ok(Vec::from(digest.as_ref()))
+ })
+ .map_err(|_| BackendError::AgreementError)?;
+
+ Ok(ECDHSecret {
+ remote: COSEKey {
+ alg: key.alg,
+ key: COSEKeyType::EC2(ec2key.clone()),
+ },
+ my: my_public_key,
+ shared_secret,
+ })
+ } else {
+ Err(BackendError::UnsupportedKeyType)
+ }
+}
+
+#[cfg(test)]
+pub(crate) fn test_encapsulate(
+ key: &COSEEC2Key,
+ alg: COSEAlgorithm,
+ my_pub_key: &[u8],
+ my_priv_key: &[u8],
+) -> Result<ECDHSecret> {
+ compile_error!(
+ "Ring-backend is not yet implemented. Compile with `--features crypto_openssl` for now."
+ )
+}
+
+/// Encrypts a plaintext to produce a ciphertext, which may be longer than the plaintext.
+/// The plaintext is restricted to being a multiple of the AES block size (16 bytes) in length.
+pub(crate) fn encrypt(
+ key: &[u8],
+ plain_text: &[u8], /*PlainText*/
+) -> Result<Vec<u8> /*CypherText*/> {
+ // Ring doesn't support AES-CBC yet
+ compile_error!(
+ "Ring-backend is not yet implemented. Compile with `--features crypto_openssl` for now."
+ )
+}
+
+/// Decrypts a ciphertext and returns the plaintext.
+pub(crate) fn decrypt(
+ key: &[u8],
+ cypher_text: &[u8], /*CypherText*/
+) -> Result<Vec<u8> /*PlainText*/> {
+ // Ring doesn't support AES-CBC yet
+ compile_error!(
+ "Ring-backend is not yet implemented. Compile with `--features crypto_openssl` for now."
+ )
+}
+
+/// Computes a MAC of the given message.
+pub(crate) fn authenticate(token: &[u8], input: &[u8]) -> Result<Vec<u8>> {
+ let s_key = hmac::Key::new(hmac::HMAC_SHA256, token);
+ let tag = hmac::sign(&s_key, input);
+ Ok(tag.as_ref().to_vec())
+}
diff --git a/third_party/rust/authenticator/src/ctap2-capi.h b/third_party/rust/authenticator/src/ctap2-capi.h
new file mode 100644
index 0000000000..d9f5b903b9
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2-capi.h
@@ -0,0 +1,254 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __CTAP2_CAPI
+#define __CTAP2_CAPI
+#include <stdlib.h>
+#include "nsString.h"
+
+extern "C" {
+const uint8_t CTAP2_SIGN_RESULT_PUBKEY_CRED_ID = 1;
+const uint8_t CTAP2_SIGN_RESULT_AUTH_DATA = 2;
+const uint8_t CTAP2_SIGN_RESULT_SIGNATURE = 3;
+const uint8_t CTAP2_SIGN_RESULT_USER_ID = 4;
+const uint8_t CTAP2_SIGN_RESULT_USER_NAME = 5;
+
+typedef struct {
+ const uint8_t *id_ptr;
+ size_t id_len;
+ const char *name;
+} AuthenticatorArgsUser;
+
+typedef struct {
+ const uint8_t *ptr;
+ size_t len;
+} AuthenticatorArgsChallenge;
+
+typedef struct {
+ const int32_t *ptr;
+ size_t len;
+} AuthenticatorArgsPubCred;
+
+typedef struct {
+ bool resident_key;
+ bool user_verification;
+ bool user_presence;
+ bool force_none_attestation;
+} AuthenticatorArgsOptions;
+
+// NOTE: Preconditions
+// * All rust_u2f_mgr* pointers must refer to pointers which are returned
+// by rust_u2f_mgr_new, and must be freed with rust_u2f_mgr_free.
+// * All rust_u2f_khs* pointers must refer to pointers which are returned
+// by rust_u2f_pkcd_new, and must be freed with rust_u2f_pkcd_free.
+// * All rust_u2f_res* pointers must refer to pointers passed to the
+// register() and sign() callbacks. They can be null on failure.
+
+// The `rust_u2f_key_handles` opaque type is equivalent to the rust type
+// `Ctap2PubKeyCredDescriptors`
+struct rust_ctap2_pub_key_cred_descriptors;
+
+/// Ctap2PubKeyCredDescriptors functions.
+rust_ctap2_pub_key_cred_descriptors* rust_ctap2_pkcd_new();
+void rust_ctap2_pkcd_add(rust_ctap2_pub_key_cred_descriptors* pkcd, const uint8_t* id_ptr,
+ size_t id_len, uint8_t transports);
+/* unsafe */ void rust_ctap2_pkcd_free(rust_ctap2_pub_key_cred_descriptors* khs);
+
+// The `rust_ctap2_mgr` opaque type is equivalent to the rust type `Ctap2Manager`
+// struct rust_ctap_manager;
+
+// The `rust_ctap2_result` opaque type is equivalent to the rust type `RegisterResult`
+struct rust_ctap2_register_result;
+
+// The `rust_ctap2_result` opaque type is equivalent to the rust type `RegisterResult`
+struct rust_ctap2_sign_result;
+
+// Ctap2 exposes the results directly without repackaging them. Use getter-functions.
+typedef void (*rust_ctap2_register_callback)(uint64_t, rust_ctap2_register_result*);
+typedef void (*rust_ctap2_sign_callback)(uint64_t, rust_ctap2_sign_result*);
+
+// Status updates get sent, if a device needs a PIN, if a device needs to be selected, etc.
+struct rust_ctap2_status_update_res;
+// May be called with NULL, in case of an error
+typedef void (*rust_ctap2_status_update_callback)(rust_ctap2_status_update_res*);
+
+rust_ctap_manager* rust_ctap2_mgr_new();
+/* unsafe */ void rust_ctap2_mgr_free(rust_ctap_manager* mgr);
+
+/* unsafe */ void rust_ctap2_register_res_free(rust_ctap2_register_result* res);
+/* unsafe */ void rust_ctap2_sign_res_free(rust_ctap2_sign_result* res);
+
+uint64_t rust_ctap2_mgr_register(
+ rust_ctap_manager* mgr, uint64_t timeout, rust_ctap2_register_callback, rust_ctap2_status_update_callback,
+ AuthenticatorArgsChallenge challenge,
+ const char* relying_party_id, const char *origin_ptr,
+ AuthenticatorArgsUser user, AuthenticatorArgsPubCred pub_cred_params,
+ const rust_ctap2_pub_key_cred_descriptors* exclude_list, AuthenticatorArgsOptions options,
+ const char *pin
+);
+
+uint64_t rust_ctap2_mgr_sign(
+ rust_ctap_manager* mgr, uint64_t timeout, rust_ctap2_sign_callback, rust_ctap2_status_update_callback,
+ AuthenticatorArgsChallenge challenge,
+ const char* relying_party_id, const char *origin_ptr,
+ const rust_ctap2_pub_key_cred_descriptors* allow_list, AuthenticatorArgsOptions options,
+ const char *pin
+);
+
+void rust_ctap2_mgr_cancel(rust_ctap_manager* mgr);
+
+// Returns 0 for success, or the U2F_ERROR error code >= 1.
+uint8_t rust_ctap2_register_result_error(const rust_ctap2_register_result* res);
+uint8_t rust_ctap2_sign_result_error(const rust_ctap2_sign_result* res);
+
+/// # Safety
+///
+/// This function is used to get the length, prior to calling
+/// rust_ctap2_register_result_client_data_copy()
+bool rust_ctap2_register_result_client_data_len(
+ const rust_ctap2_register_result *res,
+ size_t *len
+);
+
+/// # Safety
+///
+/// This method does not ensure anything about dst before copying, so
+/// ensure it is long enough (using rust_ctap2_register_result_client_data_len)
+bool rust_ctap2_register_result_client_data_copy(
+ const rust_ctap2_register_result *res,
+ const char *dst
+);
+
+/// # Safety
+///
+/// This function is used to get the length, prior to calling
+/// rust_ctap2_register_result_item_copy()
+bool rust_ctap2_register_result_attestation_len(
+ const rust_ctap2_register_result *res,
+ size_t *len
+);
+
+/// # Safety
+///
+/// This method does not ensure anything about dst before copying, so
+/// ensure it is long enough (using rust_ctap2_register_result_item_len)
+bool rust_ctap2_register_result_attestation_copy(
+ const rust_ctap2_register_result* res,
+ uint8_t *dst
+);
+/// # Safety
+///
+/// This function is used to get the length, prior to calling
+/// rust_ctap2_register_result_client_data_copy()
+bool rust_ctap2_sign_result_client_data_len(
+ const rust_ctap2_sign_result *res,
+ size_t *len
+);
+
+/// # Safety
+///
+/// This method does not ensure anything about dst before copying, so
+/// ensure it is long enough (using rust_ctap2_sign_result_client_data_len)
+bool rust_ctap2_sign_result_client_data_copy(
+ const rust_ctap2_sign_result *res,
+ const char *dst
+);
+
+/// # Safety
+///
+/// This function is used to get the length, prior to calling
+/// rust_ctap2_register_result_client_data_copy()
+bool rust_ctap2_sign_result_assertions_len(
+ const rust_ctap2_sign_result *res,
+ size_t *len
+);
+
+bool rust_ctap2_sign_result_item_contains(
+ const rust_ctap2_sign_result *res,
+ size_t assertion_idx,
+ uint8_t item_idx
+);
+
+/// # Safety
+///
+/// This function is used to get the length, prior to calling
+/// rust_ctap2_sign_result_item_copy()
+bool rust_ctap2_sign_result_item_len(
+ const rust_ctap2_sign_result *res,
+ size_t assertion_idx,
+ uint8_t item_idx,
+ size_t *len
+);
+
+/// # Safety
+///
+/// This method does not ensure anything about dst before copying, so
+/// ensure it is long enough (using rust_ctap2_sign_result_item_len)
+bool rust_ctap2_sign_result_item_copy(
+ const rust_ctap2_sign_result* res,
+ size_t assertion_idx,
+ uint8_t item_idx,
+ uint8_t *dst
+);
+
+bool rust_ctap2_sign_result_contains_username(
+ const rust_ctap2_sign_result *res,
+ size_t assertion_idx
+);
+
+/// # Safety
+///
+/// This function is used to get the length, prior to calling
+/// rust_ctap2_sign_result_username_copy()
+bool rust_ctap2_sign_result_username_len(
+ const rust_ctap2_sign_result *res,
+ size_t assertion_idx,
+ size_t *len
+);
+
+/// # Safety
+///
+/// This method does not ensure anything about dst before copying, so
+/// ensure it is long enough (using rust_ctap2_sign_result_username_len)
+bool rust_ctap2_sign_result_username_copy(
+ const rust_ctap2_sign_result* res,
+ size_t assertion_idx,
+ const char *dst
+);
+
+/// # Safety
+///
+/// This function is used to get the length, prior to calling
+/// rust_ctap2_status_update_copy_json()
+bool rust_ctap2_status_update_len(
+ const rust_ctap2_status_update_res *res,
+ size_t *len
+);
+
+/// # Safety
+///
+/// This method does not ensure anything about dst before copying, so
+/// ensure it is long enough (using rust_ctap2_status_update_len)
+bool rust_ctap2_status_update_copy_json(
+ const rust_ctap2_status_update_res *res,
+ const char *dst
+);
+
+bool rust_ctap2_status_update_send_pin(
+ const rust_ctap2_status_update_res *res,
+ const char *pin
+);
+
+
+/// # Safety
+/// This frees the memory of a status_update_res
+bool rust_ctap2_destroy_status_update_res(
+ rust_ctap2_status_update_res *res
+);
+
+
+}
+#endif // __CTAP2_CAPI
diff --git a/third_party/rust/authenticator/src/ctap2/attestation.rs b/third_party/rust/authenticator/src/ctap2/attestation.rs
new file mode 100644
index 0000000000..896db0dee3
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/attestation.rs
@@ -0,0 +1,799 @@
+use super::utils::from_slice_stream;
+use crate::crypto::COSEAlgorithm;
+use crate::ctap2::commands::CommandError;
+use crate::ctap2::server::RpIdHash;
+use crate::{crypto::COSEKey, errors::AuthenticatorError};
+use nom::{
+ bytes::complete::take,
+ combinator::{cond, map},
+ error::Error as NomError,
+ number::complete::{be_u16, be_u32, be_u8},
+ Err as NomErr, IResult,
+};
+use serde::ser::{Error as SerError, SerializeMap, Serializer};
+use serde::{
+ de::{Error as SerdeError, MapAccess, Visitor},
+ Deserialize, Deserializer, Serialize,
+};
+use serde_bytes::ByteBuf;
+use serde_cbor;
+use std::fmt;
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum HmacSecretResponse {
+ /// This is returned by MakeCredential calls to display if CredRandom was
+ /// successfully generated
+ Confirmed(bool),
+ /// This is returned by GetAssertion:
+ /// AES256-CBC(shared_secret, HMAC-SHA265(CredRandom, salt1) || HMAC-SHA265(CredRandom, salt2))
+ Secret(Vec<u8>),
+}
+
+impl Serialize for HmacSecretResponse {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ match self {
+ HmacSecretResponse::Confirmed(x) => serializer.serialize_bool(*x),
+ HmacSecretResponse::Secret(x) => serializer.serialize_bytes(&x),
+ }
+ }
+}
+impl<'de> Deserialize<'de> for HmacSecretResponse {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct HmacSecretResponseVisitor;
+
+ impl<'de> Visitor<'de> for HmacSecretResponseVisitor {
+ type Value = HmacSecretResponse;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a byte array or a boolean")
+ }
+
+ fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
+ where
+ E: SerdeError,
+ {
+ Ok(HmacSecretResponse::Secret(v.to_vec()))
+ }
+
+ fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
+ where
+ E: SerdeError,
+ {
+ Ok(HmacSecretResponse::Confirmed(v))
+ }
+ }
+ deserializer.deserialize_any(HmacSecretResponseVisitor)
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
+pub struct Extension {
+ #[serde(rename = "pinMinLength", skip_serializing_if = "Option::is_none")]
+ pub pin_min_length: Option<u64>,
+ #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")]
+ pub hmac_secret: Option<HmacSecretResponse>,
+}
+
+fn parse_extensions<'a>(input: &'a [u8]) -> IResult<&'a [u8], Extension, NomError<&'a [u8]>> {
+ serde_to_nom(input)
+}
+
+#[derive(Serialize, PartialEq, Default, Eq, Clone)]
+pub struct AAGuid(pub [u8; 16]);
+
+impl AAGuid {
+ pub fn from(src: &[u8]) -> Result<AAGuid, ()> {
+ let mut payload = [0u8; 16];
+ if src.len() != payload.len() {
+ Err(())
+ } else {
+ payload.copy_from_slice(src);
+ Ok(AAGuid(payload))
+ }
+ }
+}
+
+impl fmt::Debug for AAGuid {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "AAGuid({:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x})",
+ self.0[0],
+ self.0[1],
+ self.0[2],
+ self.0[3],
+ self.0[4],
+ self.0[5],
+ self.0[6],
+ self.0[7],
+ self.0[8],
+ self.0[9],
+ self.0[10],
+ self.0[11],
+ self.0[12],
+ self.0[13],
+ self.0[14],
+ self.0[15]
+ )
+ }
+}
+
+impl<'de> Deserialize<'de> for AAGuid {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct AAGuidVisitor;
+
+ impl<'de> Visitor<'de> for AAGuidVisitor {
+ type Value = AAGuid;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a byte array")
+ }
+
+ fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
+ where
+ E: SerdeError,
+ {
+ let mut buf = [0u8; 16];
+ if v.len() != buf.len() {
+ return Err(E::invalid_length(v.len(), &"16"));
+ }
+
+ buf.copy_from_slice(v);
+
+ Ok(AAGuid(buf))
+ }
+ }
+
+ deserializer.deserialize_bytes(AAGuidVisitor)
+ }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct AttestedCredentialData {
+ pub aaguid: AAGuid,
+ pub credential_id: Vec<u8>,
+ pub credential_public_key: COSEKey,
+}
+
+fn serde_to_nom<'a, Output>(input: &'a [u8]) -> IResult<&'a [u8], Output>
+where
+ Output: Deserialize<'a>,
+{
+ from_slice_stream(input)
+ .map_err(|_e| nom::Err::Error(nom::error::make_error(input, nom::error::ErrorKind::NoneOf)))
+ // can't use custom errorkind because of error type mismatch in parse_attested_cred_data
+ //.map_err(|e| NomErr::Error(Context::Code(input, ErrorKind::Custom(e))))
+ // .map_err(|_| NomErr::Error(Context::Code(input, ErrorKind::Custom(42))))
+}
+
+fn parse_attested_cred_data<'a>(
+ input: &'a [u8],
+) -> IResult<&'a [u8], AttestedCredentialData, NomError<&'a [u8]>> {
+ let (rest, aaguid_res) = map(take(16u8), AAGuid::from)(input)?;
+ // // We can unwrap here, since we _know_ the input will be 16 bytes error out before calling from()
+ let aaguid = aaguid_res.unwrap();
+ let (rest, cred_len) = be_u16(rest)?;
+ let (rest, credential_id) = map(take(cred_len), Vec::from)(rest)?;
+ let (rest, credential_public_key) = serde_to_nom(rest)?;
+ Ok((
+ rest,
+ (AttestedCredentialData {
+ aaguid,
+ credential_id,
+ credential_public_key: credential_public_key,
+ }),
+ ))
+}
+
+bitflags! {
+ pub struct AuthenticatorDataFlags: u8 {
+ const USER_PRESENT = 0x01;
+ const USER_VERIFIED = 0x04;
+ const ATTESTED = 0x40;
+ const EXTENSION_DATA = 0x80;
+ }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct AuthenticatorData {
+ pub rp_id_hash: RpIdHash,
+ pub flags: AuthenticatorDataFlags,
+ pub counter: u32,
+ pub credential_data: Option<AttestedCredentialData>,
+ pub extensions: Extension,
+}
+
+fn parse_ad<'a>(input: &'a [u8]) -> IResult<&'a [u8], AuthenticatorData, NomError<&'a [u8]>> {
+ let (rest, rp_id_hash_res) = map(take(32u8), RpIdHash::from)(input)?;
+ // We can unwrap here, since we _know_ the input to from() will be 32 bytes or error out before calling from()
+ let rp_id_hash = rp_id_hash_res.unwrap();
+ // be conservative, there is a couple of reserved values in
+ // AuthenticatorDataFlags, just truncate the one we don't know
+ let (rest, flags) = map(be_u8, AuthenticatorDataFlags::from_bits_truncate)(rest)?;
+ let (rest, counter) = be_u32(rest)?;
+ let (rest, credential_data) = cond(
+ flags.contains(AuthenticatorDataFlags::ATTESTED),
+ parse_attested_cred_data,
+ )(rest)?;
+ let (rest, extensions) = cond(
+ flags.contains(AuthenticatorDataFlags::EXTENSION_DATA),
+ parse_extensions,
+ )(rest)?;
+ // TODO(baloo): we should check for end of buffer and raise a parse
+ // parse error if data is still in the buffer
+ //eof!() >>
+ Ok((
+ rest,
+ AuthenticatorData {
+ rp_id_hash,
+ flags,
+ counter,
+ credential_data,
+ extensions: extensions.unwrap_or_default(),
+ },
+ ))
+}
+
+impl<'de> Deserialize<'de> for AuthenticatorData {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct AuthenticatorDataVisitor;
+
+ impl<'de> Visitor<'de> for AuthenticatorDataVisitor {
+ type Value = AuthenticatorData;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a byte array")
+ }
+
+ fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
+ where
+ E: SerdeError,
+ {
+ parse_ad(v)
+ .map(|(_input, value)| value)
+ .map_err(|e| match e {
+ NomErr::Incomplete(nom::Needed::Size(len)) => {
+ E::invalid_length(v.len(), &format!("{}", v.len() + len.get()).as_ref())
+ }
+ NomErr::Incomplete(nom::Needed::Unknown) => {
+ E::invalid_length(v.len(), &"unknown") // We don't know the expected value
+ }
+ // TODO(baloo): is that enough? should we be more
+ // specific on the error type?
+ e => E::custom(e.to_string()),
+ })
+ }
+ }
+
+ deserializer.deserialize_bytes(AuthenticatorDataVisitor)
+ }
+}
+
+impl AuthenticatorData {
+ pub fn to_vec(&self) -> Result<Vec<u8>, AuthenticatorError> {
+ let mut data = Vec::new();
+ data.extend(&self.rp_id_hash.0);
+ data.extend(&[self.flags.bits()]);
+ data.extend(&self.counter.to_be_bytes());
+
+ // TODO(baloo): need to yield credential_data and extensions, but that dependends on flags,
+ // should we consider another type system?
+ if let Some(cred) = &self.credential_data {
+ data.extend(&cred.aaguid.0);
+ data.extend(&(cred.credential_id.len() as u16).to_be_bytes());
+ data.extend(&cred.credential_id);
+ data.extend(
+ &serde_cbor::to_vec(&cred.credential_public_key)
+ .map_err(CommandError::Serializing)?,
+ );
+ }
+ Ok(data)
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
+/// x509 encoded attestation certificate
+pub struct AttestationCertificate(#[serde(with = "serde_bytes")] pub(crate) Vec<u8>);
+
+impl AsRef<[u8]> for AttestationCertificate {
+ fn as_ref(&self) -> &[u8] {
+ self.0.as_ref()
+ }
+}
+
+#[derive(Serialize, Deserialize, PartialEq, Eq)]
+pub struct Signature(#[serde(with = "serde_bytes")] pub(crate) ByteBuf);
+
+impl fmt::Debug for Signature {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let value = base64::encode_config(&self.0, base64::URL_SAFE_NO_PAD);
+ write!(f, "Signature({})", value)
+ }
+}
+
+impl AsRef<[u8]> for Signature {
+ fn as_ref(&self) -> &[u8] {
+ self.0.as_ref()
+ }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum AttestationStatement {
+ None,
+ Packed(AttestationStatementPacked),
+ // TODO(baloo): there is a couple other options than None and Packed:
+ // https://w3c.github.io/webauthn/#generating-an-attestation-object
+ // https://w3c.github.io/webauthn/#defined-attestation-formats
+ //TPM,
+ //AndroidKey,
+ //AndroidSafetyNet,
+ FidoU2F(AttestationStatementFidoU2F),
+}
+
+// Not all crypto-backends currently provide "crypto::verify()", so we do not implement it yet.
+// Also not sure, if we really need it. Would be a sanity-check only, to verify the signature is valid,
+// before sendig it out.
+// impl AttestationStatement {
+// pub fn verify(&self, data: &[u8]) -> Result<bool, AuthenticatorError> {
+// match self {
+// AttestationStatement::None => Ok(true),
+// AttestationStatement::Unparsed(_) => Err(AuthenticatorError::Custom(
+// "Unparsed attestation object can't be used to verify signature.".to_string(),
+// )),
+// AttestationStatement::FidoU2F(att) => {
+// let res = crypto::verify(
+// crypto::SignatureAlgorithm::ES256,
+// &att.attestation_cert[0].as_ref(),
+// att.sig.as_ref(),
+// data,
+// )?;
+// Ok(res)
+// }
+// AttestationStatement::Packed(att) => {
+// if att.alg != Alg::ES256 {
+// return Err(AuthenticatorError::Custom(
+// "Verification only supported for ES256".to_string(),
+// ));
+// }
+// let res = crypto::verify(
+// crypto::SignatureAlgorithm::ES256,
+// att.attestation_cert[0].as_ref(),
+// att.sig.as_ref(),
+// data,
+// )?;
+// Ok(res)
+// }
+// }
+// }
+// }
+
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
+// See https://www.w3.org/TR/webauthn/#fido-u2f-attestation
+pub struct AttestationStatementFidoU2F {
+ pub sig: Signature,
+
+ #[serde(rename = "x5c")]
+ /// Certificate chain in x509 format
+ pub attestation_cert: Vec<AttestationCertificate>,
+}
+
+#[allow(dead_code)] // TODO(MS): Remove me, once we can parse AttestationStatements and use this function
+impl AttestationStatementFidoU2F {
+ pub fn new(cert: &[u8], signature: &[u8]) -> Self {
+ AttestationStatementFidoU2F {
+ sig: Signature(ByteBuf::from(signature)),
+ attestation_cert: vec![AttestationCertificate(Vec::from(cert))],
+ }
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
+// TODO(baloo): there is a couple other options than x5c:
+// https://www.w3.org/TR/webauthn/#packed-attestation
+pub struct AttestationStatementPacked {
+ pub alg: COSEAlgorithm,
+ pub sig: Signature,
+
+ #[serde(rename = "x5c")]
+ /// Certificate chain in x509 format
+ pub attestation_cert: Vec<AttestationCertificate>,
+}
+
+#[derive(Debug, Deserialize, PartialEq, Eq)]
+#[serde(rename_all = "lowercase")]
+enum AttestationFormat {
+ #[serde(rename = "fido-u2f")]
+ FidoU2F,
+ Packed,
+ None,
+ // TOOD(baloo): only packed is implemented for now, see spec:
+ // https://www.w3.org/TR/webauthn/#defined-attestation-formats
+ //TPM,
+ //AndroidKey,
+ //AndroidSafetyNet,
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct AttestationObject {
+ pub auth_data: AuthenticatorData,
+ pub att_statement: AttestationStatement,
+}
+
+impl<'de> Deserialize<'de> for AttestationObject {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct AttestationObjectVisitor;
+
+ impl<'de> Visitor<'de> for AttestationObjectVisitor {
+ type Value = AttestationObject;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a cbor map")
+ }
+
+ fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ where
+ M: MapAccess<'de>,
+ {
+ let mut format: Option<AttestationFormat> = None;
+ let mut auth_data = None;
+ let mut att_statement = None;
+
+ while let Some(key) = map.next_key()? {
+ match key {
+ // Spec for CTAP 2.0 is wrong and fmt should be numbered 1, and auth_data 2:
+ // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorMakeCredential
+ // Corrected in CTAP 2.1 and Webauthn spec
+ 1 => {
+ if format.is_some() {
+ return Err(SerdeError::duplicate_field("fmt"));
+ }
+ format = Some(map.next_value()?);
+ }
+ 2 => {
+ if auth_data.is_some() {
+ return Err(SerdeError::duplicate_field("auth_data"));
+ }
+ auth_data = Some(map.next_value()?);
+ }
+ 3 => {
+ let format = format
+ .as_ref()
+ .ok_or_else(|| SerdeError::missing_field("fmt"))?;
+ if att_statement.is_some() {
+ return Err(SerdeError::duplicate_field("att_statement"));
+ }
+ match format {
+ // This should not actually happen, but ...
+ AttestationFormat::None => {
+ att_statement = Some(AttestationStatement::None);
+ }
+ AttestationFormat::Packed => {
+ att_statement =
+ Some(AttestationStatement::Packed(map.next_value()?));
+ }
+ AttestationFormat::FidoU2F => {
+ att_statement =
+ Some(AttestationStatement::FidoU2F(map.next_value()?));
+ }
+ }
+ }
+ k => return Err(M::Error::custom(format!("unexpected key: {:?}", k))),
+ }
+ }
+
+ let auth_data =
+ auth_data.ok_or_else(|| M::Error::custom("found no auth_data".to_string()))?;
+ let att_statement = att_statement.unwrap_or(AttestationStatement::None);
+
+ Ok(AttestationObject {
+ auth_data,
+ att_statement,
+ })
+ }
+ }
+
+ deserializer.deserialize_bytes(AttestationObjectVisitor)
+ }
+}
+
+impl Serialize for AttestationObject {
+ /// Serialize can be used to repackage the CBOR answer we get from the token using CTAP-format
+ /// to webauthn-format (string-keys like "authData" instead of numbers). Yes, the specs are weird.
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let map_len = 3;
+ let mut map = serializer.serialize_map(Some(map_len))?;
+
+ let auth_data = self
+ .auth_data
+ .to_vec()
+ .map(|v| serde_cbor::Value::Bytes(v))
+ .map_err(|_| SerError::custom("Failed to serialize auth_data"))?;
+
+ map.serialize_entry(&"authData", &auth_data)?;
+ match self.att_statement {
+ AttestationStatement::None => {
+ // Even with Att None, an empty map is returned in the cbor!
+ map.serialize_entry(&"fmt", &"none")?;
+ let v = serde_cbor::Value::Map(std::collections::BTreeMap::new());
+ map.serialize_entry(&"attStmt", &v)?;
+ }
+ AttestationStatement::Packed(ref v) => {
+ map.serialize_entry(&"fmt", &"packed")?;
+ map.serialize_entry(&"attStmt", v)?;
+ }
+ AttestationStatement::FidoU2F(ref v) => {
+ map.serialize_entry(&"fmt", &"fido-u2f")?;
+ map.serialize_entry(&"attStmt", v)?;
+ }
+ }
+ map.end()
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::super::utils::from_slice_stream;
+ use super::*;
+ use serde_cbor::from_slice;
+
+ const SAMPLE_ATTESTATION: [u8; 1006] = [
+ 0xa3, 0x1, 0x66, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x64, 0x2, 0x58, 0xc4, 0x49, 0x96, 0xd,
+ 0xe5, 0x88, 0xe, 0x8c, 0x68, 0x74, 0x34, 0x17, 0xf, 0x64, 0x76, 0x60, 0x5b, 0x8f, 0xe4,
+ 0xae, 0xb9, 0xa2, 0x86, 0x32, 0xc7, 0x99, 0x5c, 0xf3, 0xba, 0x83, 0x1d, 0x97, 0x63, 0x41,
+ 0x0, 0x0, 0x0, 0x7, 0xcb, 0x69, 0x48, 0x1e, 0x8f, 0xf7, 0x40, 0x39, 0x93, 0xec, 0xa, 0x27,
+ 0x29, 0xa1, 0x54, 0xa8, 0x0, 0x40, 0xc3, 0xcf, 0x1, 0x3b, 0xc6, 0x26, 0x93, 0x28, 0xfb,
+ 0x7f, 0xa9, 0x76, 0xef, 0xa8, 0x4b, 0x66, 0x71, 0xad, 0xa9, 0x64, 0xea, 0xcb, 0x58, 0x76,
+ 0x54, 0x51, 0xa, 0xc8, 0x86, 0x4f, 0xbb, 0x53, 0x2d, 0xfb, 0x2, 0xfc, 0xdc, 0xa9, 0x84,
+ 0xc2, 0x5c, 0x67, 0x8a, 0x3a, 0xab, 0x57, 0xf3, 0x71, 0x77, 0xd3, 0xd4, 0x41, 0x64, 0x1,
+ 0x50, 0xca, 0x6c, 0x42, 0x73, 0x1c, 0x42, 0xcb, 0x81, 0xba, 0xa5, 0x1, 0x2, 0x3, 0x26,
+ 0x20, 0x1, 0x21, 0x58, 0x20, 0x9, 0x2e, 0x34, 0xfe, 0xa7, 0xd7, 0x32, 0xc8, 0xae, 0x4c,
+ 0xf6, 0x96, 0xbe, 0x7a, 0x12, 0xdc, 0x29, 0xd5, 0xf1, 0xd3, 0xf1, 0x55, 0x4d, 0xdc, 0x87,
+ 0xc4, 0xc, 0x9b, 0xd0, 0x17, 0xba, 0xf, 0x22, 0x58, 0x20, 0xc9, 0xf0, 0x97, 0x33, 0x55,
+ 0x36, 0x58, 0xd9, 0xdb, 0x76, 0xf5, 0xef, 0x95, 0xcf, 0x8a, 0xc7, 0xfc, 0xc1, 0xb6, 0x81,
+ 0x25, 0x5f, 0x94, 0x6b, 0x62, 0x13, 0x7d, 0xd0, 0xc4, 0x86, 0x53, 0xdb, 0x3, 0xa3, 0x63,
+ 0x61, 0x6c, 0x67, 0x26, 0x63, 0x73, 0x69, 0x67, 0x58, 0x48, 0x30, 0x46, 0x2, 0x21, 0x0,
+ 0xac, 0x2a, 0x78, 0xa8, 0xaf, 0x18, 0x80, 0x39, 0x73, 0x8d, 0x3, 0x5e, 0x4, 0x4d, 0x94,
+ 0x4f, 0x3f, 0x57, 0xce, 0x88, 0x41, 0xfa, 0x81, 0x50, 0x40, 0xb6, 0xd1, 0x95, 0xb5, 0xeb,
+ 0xe4, 0x6f, 0x2, 0x21, 0x0, 0x8f, 0xf4, 0x15, 0xc9, 0xb3, 0x6d, 0x1c, 0xd, 0x4c, 0xa3,
+ 0xcf, 0x99, 0x8a, 0x46, 0xd4, 0x4c, 0x8b, 0x5c, 0x26, 0x3f, 0xdf, 0x22, 0x6c, 0x9b, 0x23,
+ 0x83, 0x8b, 0x69, 0x47, 0x67, 0x48, 0x45, 0x63, 0x78, 0x35, 0x63, 0x81, 0x59, 0x2, 0xc1,
+ 0x30, 0x82, 0x2, 0xbd, 0x30, 0x82, 0x1, 0xa5, 0xa0, 0x3, 0x2, 0x1, 0x2, 0x2, 0x4, 0x18,
+ 0xac, 0x46, 0xc0, 0x30, 0xd, 0x6, 0x9, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0xd, 0x1, 0x1, 0xb,
+ 0x5, 0x0, 0x30, 0x2e, 0x31, 0x2c, 0x30, 0x2a, 0x6, 0x3, 0x55, 0x4, 0x3, 0x13, 0x23, 0x59,
+ 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x55, 0x32, 0x46, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20,
+ 0x43, 0x41, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x20, 0x34, 0x35, 0x37, 0x32, 0x30,
+ 0x30, 0x36, 0x33, 0x31, 0x30, 0x20, 0x17, 0xd, 0x31, 0x34, 0x30, 0x38, 0x30, 0x31, 0x30,
+ 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x18, 0xf, 0x32, 0x30, 0x35, 0x30, 0x30, 0x39, 0x30,
+ 0x34, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x30, 0x6e, 0x31, 0xb, 0x30, 0x9, 0x6, 0x3,
+ 0x55, 0x4, 0x6, 0x13, 0x2, 0x53, 0x45, 0x31, 0x12, 0x30, 0x10, 0x6, 0x3, 0x55, 0x4, 0xa,
+ 0xc, 0x9, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x41, 0x42, 0x31, 0x22, 0x30, 0x20,
+ 0x6, 0x3, 0x55, 0x4, 0xb, 0xc, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63,
+ 0x61, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f,
+ 0x6e, 0x31, 0x27, 0x30, 0x25, 0x6, 0x3, 0x55, 0x4, 0x3, 0xc, 0x1e, 0x59, 0x75, 0x62, 0x69,
+ 0x63, 0x6f, 0x20, 0x55, 0x32, 0x46, 0x20, 0x45, 0x45, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61,
+ 0x6c, 0x20, 0x34, 0x31, 0x33, 0x39, 0x34, 0x33, 0x34, 0x38, 0x38, 0x30, 0x59, 0x30, 0x13,
+ 0x6, 0x7, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x2, 0x1, 0x6, 0x8, 0x2a, 0x86, 0x48, 0xce, 0x3d,
+ 0x3, 0x1, 0x7, 0x3, 0x42, 0x0, 0x4, 0x79, 0xea, 0x3b, 0x2c, 0x7c, 0x49, 0x70, 0x10, 0x62,
+ 0x23, 0xc, 0xd2, 0x3f, 0xeb, 0x60, 0xe5, 0x29, 0x31, 0x71, 0xd4, 0x83, 0xf1, 0x0, 0xbe,
+ 0x85, 0x9d, 0x6b, 0xf, 0x83, 0x97, 0x3, 0x1, 0xb5, 0x46, 0xcd, 0xd4, 0x6e, 0xcf, 0xca,
+ 0xe3, 0xe3, 0xf3, 0xf, 0x81, 0xe9, 0xed, 0x62, 0xbd, 0x26, 0x8d, 0x4c, 0x1e, 0xbd, 0x37,
+ 0xb3, 0xbc, 0xbe, 0x92, 0xa8, 0xc2, 0xae, 0xeb, 0x4e, 0x3a, 0xa3, 0x6c, 0x30, 0x6a, 0x30,
+ 0x22, 0x6, 0x9, 0x2b, 0x6, 0x1, 0x4, 0x1, 0x82, 0xc4, 0xa, 0x2, 0x4, 0x15, 0x31, 0x2e,
+ 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x31, 0x34, 0x38, 0x32,
+ 0x2e, 0x31, 0x2e, 0x37, 0x30, 0x13, 0x6, 0xb, 0x2b, 0x6, 0x1, 0x4, 0x1, 0x82, 0xe5, 0x1c,
+ 0x2, 0x1, 0x1, 0x4, 0x4, 0x3, 0x2, 0x5, 0x20, 0x30, 0x21, 0x6, 0xb, 0x2b, 0x6, 0x1, 0x4,
+ 0x1, 0x82, 0xe5, 0x1c, 0x1, 0x1, 0x4, 0x4, 0x12, 0x4, 0x10, 0xcb, 0x69, 0x48, 0x1e, 0x8f,
+ 0xf7, 0x40, 0x39, 0x93, 0xec, 0xa, 0x27, 0x29, 0xa1, 0x54, 0xa8, 0x30, 0xc, 0x6, 0x3, 0x55,
+ 0x1d, 0x13, 0x1, 0x1, 0xff, 0x4, 0x2, 0x30, 0x0, 0x30, 0xd, 0x6, 0x9, 0x2a, 0x86, 0x48,
+ 0x86, 0xf7, 0xd, 0x1, 0x1, 0xb, 0x5, 0x0, 0x3, 0x82, 0x1, 0x1, 0x0, 0x97, 0x9d, 0x3, 0x97,
+ 0xd8, 0x60, 0xf8, 0x2e, 0xe1, 0x5d, 0x31, 0x1c, 0x79, 0x6e, 0xba, 0xfb, 0x22, 0xfa, 0xa7,
+ 0xe0, 0x84, 0xd9, 0xba, 0xb4, 0xc6, 0x1b, 0xbb, 0x57, 0xf3, 0xe6, 0xb4, 0xc1, 0x8a, 0x48,
+ 0x37, 0xb8, 0x5c, 0x3c, 0x4e, 0xdb, 0xe4, 0x83, 0x43, 0xf4, 0xd6, 0xa5, 0xd9, 0xb1, 0xce,
+ 0xda, 0x8a, 0xe1, 0xfe, 0xd4, 0x91, 0x29, 0x21, 0x73, 0x5, 0x8e, 0x5e, 0xe1, 0xcb, 0xdd,
+ 0x6b, 0xda, 0xc0, 0x75, 0x57, 0xc6, 0xa0, 0xe8, 0xd3, 0x68, 0x25, 0xba, 0x15, 0x9e, 0x7f,
+ 0xb5, 0xad, 0x8c, 0xda, 0xf8, 0x4, 0x86, 0x8c, 0xf9, 0xe, 0x8f, 0x1f, 0x8a, 0xea, 0x17,
+ 0xc0, 0x16, 0xb5, 0x5c, 0x2a, 0x7a, 0xd4, 0x97, 0xc8, 0x94, 0xfb, 0x71, 0xd7, 0x53, 0xd7,
+ 0x9b, 0x9a, 0x48, 0x4b, 0x6c, 0x37, 0x6d, 0x72, 0x3b, 0x99, 0x8d, 0x2e, 0x1d, 0x43, 0x6,
+ 0xbf, 0x10, 0x33, 0xb5, 0xae, 0xf8, 0xcc, 0xa5, 0xcb, 0xb2, 0x56, 0x8b, 0x69, 0x24, 0x22,
+ 0x6d, 0x22, 0xa3, 0x58, 0xab, 0x7d, 0x87, 0xe4, 0xac, 0x5f, 0x2e, 0x9, 0x1a, 0xa7, 0x15,
+ 0x79, 0xf3, 0xa5, 0x69, 0x9, 0x49, 0x7d, 0x72, 0xf5, 0x4e, 0x6, 0xba, 0xc1, 0xc3, 0xb4,
+ 0x41, 0x3b, 0xba, 0x5e, 0xaf, 0x94, 0xc3, 0xb6, 0x4f, 0x34, 0xf9, 0xeb, 0xa4, 0x1a, 0xcb,
+ 0x6a, 0xe2, 0x83, 0x77, 0x6d, 0x36, 0x46, 0x53, 0x78, 0x48, 0xfe, 0xe8, 0x84, 0xbd, 0xdd,
+ 0xf5, 0xb1, 0xba, 0x57, 0x98, 0x54, 0xcf, 0xfd, 0xce, 0xba, 0xc3, 0x44, 0x5, 0x95, 0x27,
+ 0xe5, 0x6d, 0xd5, 0x98, 0xf8, 0xf5, 0x66, 0x71, 0x5a, 0xbe, 0x43, 0x1, 0xdd, 0x19, 0x11,
+ 0x30, 0xe6, 0xb9, 0xf0, 0xc6, 0x40, 0x39, 0x12, 0x53, 0xe2, 0x29, 0x80, 0x3f, 0x3a, 0xef,
+ 0x27, 0x4b, 0xed, 0xbf, 0xde, 0x3f, 0xcb, 0xbd, 0x42, 0xea, 0xd6, 0x79,
+ ];
+
+ const SAMPLE_CERT_CHAIN: [u8; 709] = [
+ 0x81, 0x59, 0x2, 0xc1, 0x30, 0x82, 0x2, 0xbd, 0x30, 0x82, 0x1, 0xa5, 0xa0, 0x3, 0x2, 0x1,
+ 0x2, 0x2, 0x4, 0x18, 0xac, 0x46, 0xc0, 0x30, 0xd, 0x6, 0x9, 0x2a, 0x86, 0x48, 0x86, 0xf7,
+ 0xd, 0x1, 0x1, 0xb, 0x5, 0x0, 0x30, 0x2e, 0x31, 0x2c, 0x30, 0x2a, 0x6, 0x3, 0x55, 0x4, 0x3,
+ 0x13, 0x23, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x55, 0x32, 0x46, 0x20, 0x52, 0x6f,
+ 0x6f, 0x74, 0x20, 0x43, 0x41, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x20, 0x34, 0x35,
+ 0x37, 0x32, 0x30, 0x30, 0x36, 0x33, 0x31, 0x30, 0x20, 0x17, 0xd, 0x31, 0x34, 0x30, 0x38,
+ 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x18, 0xf, 0x32, 0x30, 0x35, 0x30,
+ 0x30, 0x39, 0x30, 0x34, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x30, 0x6e, 0x31, 0xb,
+ 0x30, 0x9, 0x6, 0x3, 0x55, 0x4, 0x6, 0x13, 0x2, 0x53, 0x45, 0x31, 0x12, 0x30, 0x10, 0x6,
+ 0x3, 0x55, 0x4, 0xa, 0xc, 0x9, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x41, 0x42, 0x31,
+ 0x22, 0x30, 0x20, 0x6, 0x3, 0x55, 0x4, 0xb, 0xc, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e,
+ 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61,
+ 0x74, 0x69, 0x6f, 0x6e, 0x31, 0x27, 0x30, 0x25, 0x6, 0x3, 0x55, 0x4, 0x3, 0xc, 0x1e, 0x59,
+ 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x55, 0x32, 0x46, 0x20, 0x45, 0x45, 0x20, 0x53, 0x65,
+ 0x72, 0x69, 0x61, 0x6c, 0x20, 0x34, 0x31, 0x33, 0x39, 0x34, 0x33, 0x34, 0x38, 0x38, 0x30,
+ 0x59, 0x30, 0x13, 0x6, 0x7, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x2, 0x1, 0x6, 0x8, 0x2a, 0x86,
+ 0x48, 0xce, 0x3d, 0x3, 0x1, 0x7, 0x3, 0x42, 0x0, 0x4, 0x79, 0xea, 0x3b, 0x2c, 0x7c, 0x49,
+ 0x70, 0x10, 0x62, 0x23, 0xc, 0xd2, 0x3f, 0xeb, 0x60, 0xe5, 0x29, 0x31, 0x71, 0xd4, 0x83,
+ 0xf1, 0x0, 0xbe, 0x85, 0x9d, 0x6b, 0xf, 0x83, 0x97, 0x3, 0x1, 0xb5, 0x46, 0xcd, 0xd4, 0x6e,
+ 0xcf, 0xca, 0xe3, 0xe3, 0xf3, 0xf, 0x81, 0xe9, 0xed, 0x62, 0xbd, 0x26, 0x8d, 0x4c, 0x1e,
+ 0xbd, 0x37, 0xb3, 0xbc, 0xbe, 0x92, 0xa8, 0xc2, 0xae, 0xeb, 0x4e, 0x3a, 0xa3, 0x6c, 0x30,
+ 0x6a, 0x30, 0x22, 0x6, 0x9, 0x2b, 0x6, 0x1, 0x4, 0x1, 0x82, 0xc4, 0xa, 0x2, 0x4, 0x15,
+ 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x31, 0x34,
+ 0x38, 0x32, 0x2e, 0x31, 0x2e, 0x37, 0x30, 0x13, 0x6, 0xb, 0x2b, 0x6, 0x1, 0x4, 0x1, 0x82,
+ 0xe5, 0x1c, 0x2, 0x1, 0x1, 0x4, 0x4, 0x3, 0x2, 0x5, 0x20, 0x30, 0x21, 0x6, 0xb, 0x2b, 0x6,
+ 0x1, 0x4, 0x1, 0x82, 0xe5, 0x1c, 0x1, 0x1, 0x4, 0x4, 0x12, 0x4, 0x10, 0xcb, 0x69, 0x48,
+ 0x1e, 0x8f, 0xf7, 0x40, 0x39, 0x93, 0xec, 0xa, 0x27, 0x29, 0xa1, 0x54, 0xa8, 0x30, 0xc,
+ 0x6, 0x3, 0x55, 0x1d, 0x13, 0x1, 0x1, 0xff, 0x4, 0x2, 0x30, 0x0, 0x30, 0xd, 0x6, 0x9, 0x2a,
+ 0x86, 0x48, 0x86, 0xf7, 0xd, 0x1, 0x1, 0xb, 0x5, 0x0, 0x3, 0x82, 0x1, 0x1, 0x0, 0x97, 0x9d,
+ 0x3, 0x97, 0xd8, 0x60, 0xf8, 0x2e, 0xe1, 0x5d, 0x31, 0x1c, 0x79, 0x6e, 0xba, 0xfb, 0x22,
+ 0xfa, 0xa7, 0xe0, 0x84, 0xd9, 0xba, 0xb4, 0xc6, 0x1b, 0xbb, 0x57, 0xf3, 0xe6, 0xb4, 0xc1,
+ 0x8a, 0x48, 0x37, 0xb8, 0x5c, 0x3c, 0x4e, 0xdb, 0xe4, 0x83, 0x43, 0xf4, 0xd6, 0xa5, 0xd9,
+ 0xb1, 0xce, 0xda, 0x8a, 0xe1, 0xfe, 0xd4, 0x91, 0x29, 0x21, 0x73, 0x5, 0x8e, 0x5e, 0xe1,
+ 0xcb, 0xdd, 0x6b, 0xda, 0xc0, 0x75, 0x57, 0xc6, 0xa0, 0xe8, 0xd3, 0x68, 0x25, 0xba, 0x15,
+ 0x9e, 0x7f, 0xb5, 0xad, 0x8c, 0xda, 0xf8, 0x4, 0x86, 0x8c, 0xf9, 0xe, 0x8f, 0x1f, 0x8a,
+ 0xea, 0x17, 0xc0, 0x16, 0xb5, 0x5c, 0x2a, 0x7a, 0xd4, 0x97, 0xc8, 0x94, 0xfb, 0x71, 0xd7,
+ 0x53, 0xd7, 0x9b, 0x9a, 0x48, 0x4b, 0x6c, 0x37, 0x6d, 0x72, 0x3b, 0x99, 0x8d, 0x2e, 0x1d,
+ 0x43, 0x6, 0xbf, 0x10, 0x33, 0xb5, 0xae, 0xf8, 0xcc, 0xa5, 0xcb, 0xb2, 0x56, 0x8b, 0x69,
+ 0x24, 0x22, 0x6d, 0x22, 0xa3, 0x58, 0xab, 0x7d, 0x87, 0xe4, 0xac, 0x5f, 0x2e, 0x9, 0x1a,
+ 0xa7, 0x15, 0x79, 0xf3, 0xa5, 0x69, 0x9, 0x49, 0x7d, 0x72, 0xf5, 0x4e, 0x6, 0xba, 0xc1,
+ 0xc3, 0xb4, 0x41, 0x3b, 0xba, 0x5e, 0xaf, 0x94, 0xc3, 0xb6, 0x4f, 0x34, 0xf9, 0xeb, 0xa4,
+ 0x1a, 0xcb, 0x6a, 0xe2, 0x83, 0x77, 0x6d, 0x36, 0x46, 0x53, 0x78, 0x48, 0xfe, 0xe8, 0x84,
+ 0xbd, 0xdd, 0xf5, 0xb1, 0xba, 0x57, 0x98, 0x54, 0xcf, 0xfd, 0xce, 0xba, 0xc3, 0x44, 0x5,
+ 0x95, 0x27, 0xe5, 0x6d, 0xd5, 0x98, 0xf8, 0xf5, 0x66, 0x71, 0x5a, 0xbe, 0x43, 0x1, 0xdd,
+ 0x19, 0x11, 0x30, 0xe6, 0xb9, 0xf0, 0xc6, 0x40, 0x39, 0x12, 0x53, 0xe2, 0x29, 0x80, 0x3f,
+ 0x3a, 0xef, 0x27, 0x4b, 0xed, 0xbf, 0xde, 0x3f, 0xcb, 0xbd, 0x42, 0xea, 0xd6, 0x79,
+ ];
+
+ const SAMPLE_AUTH_DATA_MAKE_CREDENTIAL: [u8; 164] = [
+ 0x58, 0xA2, // bytes(162)
+ // authData
+ 0xc2, 0x89, 0xc5, 0xca, 0x9b, 0x04, 0x60, 0xf9, 0x34, 0x6a, 0xb4, 0xe4, 0x2d, 0x84,
+ 0x27, // rp_id_hash
+ 0x43, 0x40, 0x4d, 0x31, 0xf4, 0x84, 0x68, 0x25, 0xa6, 0xd0, 0x65, 0xbe, 0x59, 0x7a,
+ 0x87, // rp_id_hash
+ 0x05, 0x1d, // rp_id_hash
+ 0xC1, // authData Flags
+ 0x00, 0x00, 0x00, 0x0b, // authData counter
+ 0xf8, 0xa0, 0x11, 0xf3, 0x8c, 0x0a, 0x4d, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1f, 0x9e, 0xdc,
+ 0x7d, // AAGUID
+ 0x00, 0x10, // credential id length
+ 0x89, 0x59, 0xce, 0xad, 0x5b, 0x5c, 0x48, 0x16, 0x4e, 0x8a, 0xbc, 0xd6, 0xd9, 0x43, 0x5c,
+ 0x6f, // credential id
+ // credential public key
+ 0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20, 0xa5, 0xfd, 0x5c, 0xe1, 0xb1,
+ 0xc4, 0x58, 0xc5, 0x30, 0xa5, 0x4f, 0xa6, 0x1b, 0x31, 0xbf, 0x6b, 0x04, 0xbe, 0x8b, 0x97,
+ 0xaf, 0xde, 0x54, 0xdd, 0x8c, 0xbb, 0x69, 0x27, 0x5a, 0x8a, 0x1b, 0xe1, 0x22, 0x58, 0x20,
+ 0xfa, 0x3a, 0x32, 0x31, 0xdd, 0x9d, 0xee, 0xd9, 0xd1, 0x89, 0x7b, 0xe5, 0xa6, 0x22, 0x8c,
+ 0x59, 0x50, 0x1e, 0x4b, 0xcd, 0x12, 0x97, 0x5d, 0x3d, 0xff, 0x73, 0x0f, 0x01, 0x27, 0x8e,
+ 0xa6, 0x1c, // pub key end
+ // Extensions
+ 0xA1, // map(1)
+ 0x6B, // text(11)
+ 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // "hmac-secret"
+ 0xF5, // true
+ ];
+
+ const SAMPLE_AUTH_DATA_GET_ASSERTION: [u8; 229] = [
+ 0x58, 0xE3, // bytes(227)
+ // authData
+ 0xc2, 0x89, 0xc5, 0xca, 0x9b, 0x04, 0x60, 0xf9, 0x34, 0x6a, 0xb4, 0xe4, 0x2d, 0x84,
+ 0x27, // rp_id_hash
+ 0x43, 0x40, 0x4d, 0x31, 0xf4, 0x84, 0x68, 0x25, 0xa6, 0xd0, 0x65, 0xbe, 0x59, 0x7a,
+ 0x87, // rp_id_hash
+ 0x05, 0x1d, // rp_id_hash
+ 0xC1, // authData Flags
+ 0x00, 0x00, 0x00, 0x0b, // authData counter
+ 0xf8, 0xa0, 0x11, 0xf3, 0x8c, 0x0a, 0x4d, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1f, 0x9e, 0xdc,
+ 0x7d, // AAGUID
+ 0x00, 0x10, // credential id length
+ 0x89, 0x59, 0xce, 0xad, 0x5b, 0x5c, 0x48, 0x16, 0x4e, 0x8a, 0xbc, 0xd6, 0xd9, 0x43, 0x5c,
+ 0x6f, // credential id
+ // credential public key
+ 0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20, 0xa5, 0xfd, 0x5c, 0xe1, 0xb1,
+ 0xc4, 0x58, 0xc5, 0x30, 0xa5, 0x4f, 0xa6, 0x1b, 0x31, 0xbf, 0x6b, 0x04, 0xbe, 0x8b, 0x97,
+ 0xaf, 0xde, 0x54, 0xdd, 0x8c, 0xbb, 0x69, 0x27, 0x5a, 0x8a, 0x1b, 0xe1, 0x22, 0x58, 0x20,
+ 0xfa, 0x3a, 0x32, 0x31, 0xdd, 0x9d, 0xee, 0xd9, 0xd1, 0x89, 0x7b, 0xe5, 0xa6, 0x22, 0x8c,
+ 0x59, 0x50, 0x1e, 0x4b, 0xcd, 0x12, 0x97, 0x5d, 0x3d, 0xff, 0x73, 0x0f, 0x01, 0x27, 0x8e,
+ 0xa6, 0x1c, // pub key end
+ // Extensions
+ 0xA1, // map(1)
+ 0x6B, // text(11)
+ 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // "hmac-secret"
+ 0x58, 0x40, // bytes(64)
+ 0x1F, 0x91, 0x52, 0x6C, 0xAE, 0x45, 0x6E, 0x4C, 0xBB, 0x71, 0xC4, 0xDD, 0xE7, 0xBB, 0x87,
+ 0x71, 0x57, 0xE6, 0xE5, 0x4D, 0xFE, 0xD3, 0x01, 0x5D, 0x7D, 0x4D, 0xBB, 0x22, 0x69, 0xAF,
+ 0xCD, 0xE6, 0xA9, 0x1B, 0x8D, 0x26, 0x7E, 0xBB, 0xF8, 0x48, 0xEB, 0x95, 0xA6, 0x8E, 0x79,
+ 0xC7, 0xAC, 0x70, 0x5E, 0x35, 0x1D, 0x54, 0x3D, 0xB0, 0x16, 0x58, 0x87, 0xD6, 0x29, 0x0F,
+ 0xD4, 0x7A, 0x40, 0xC4,
+ ];
+
+ #[test]
+ fn parse_cert_chain() {
+ let cert: AttestationCertificate = from_slice(&SAMPLE_CERT_CHAIN[1..]).unwrap();
+ assert_eq!(&cert.0, &SAMPLE_CERT_CHAIN[4..]);
+
+ let _cert: Vec<AttestationCertificate> = from_slice(&SAMPLE_CERT_CHAIN).unwrap();
+ }
+
+ #[test]
+ fn parse_attestation_object() {
+ let value: AttestationObject = from_slice(&SAMPLE_ATTESTATION).unwrap();
+ println!("{:?}", value);
+
+ //assert_eq!(true, false);
+ }
+
+ #[test]
+ fn parse_reader() {
+ let v: Vec<u8> = vec![
+ 0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72, 0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72,
+ ];
+ let (rest, value): (&[u8], String) = from_slice_stream(&v).unwrap();
+ assert_eq!(value, "foobar");
+ assert_eq!(rest, &[0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72]);
+ let (rest, value): (&[u8], String) = from_slice_stream(rest).unwrap();
+ assert_eq!(value, "foobar");
+ assert!(rest.is_empty());
+ }
+
+ #[test]
+ fn parse_extensions() {
+ let auth_make: AuthenticatorData = from_slice(&SAMPLE_AUTH_DATA_MAKE_CREDENTIAL).unwrap();
+ assert_eq!(
+ auth_make.extensions.hmac_secret,
+ Some(HmacSecretResponse::Confirmed(true))
+ );
+ let auth_get: AuthenticatorData = from_slice(&SAMPLE_AUTH_DATA_GET_ASSERTION).unwrap();
+ assert_eq!(
+ auth_get.extensions.hmac_secret,
+ Some(HmacSecretResponse::Secret(vec![
+ 0x1F, 0x91, 0x52, 0x6C, 0xAE, 0x45, 0x6E, 0x4C, 0xBB, 0x71, 0xC4, 0xDD, 0xE7, 0xBB,
+ 0x87, 0x71, 0x57, 0xE6, 0xE5, 0x4D, 0xFE, 0xD3, 0x01, 0x5D, 0x7D, 0x4D, 0xBB, 0x22,
+ 0x69, 0xAF, 0xCD, 0xE6, 0xA9, 0x1B, 0x8D, 0x26, 0x7E, 0xBB, 0xF8, 0x48, 0xEB, 0x95,
+ 0xA6, 0x8E, 0x79, 0xC7, 0xAC, 0x70, 0x5E, 0x35, 0x1D, 0x54, 0x3D, 0xB0, 0x16, 0x58,
+ 0x87, 0xD6, 0x29, 0x0F, 0xD4, 0x7A, 0x40, 0xC4,
+ ]))
+ );
+ }
+
+ /// See: https://github.com/mozilla/authenticator-rs/issues/187
+ #[test]
+ fn test_aaguid_output() {
+ let input = [
+ 0xcb, 0x69, 0x48, 0x1e, 0x8f, 0xf0, 0x00, 0x39, 0x93, 0xec, 0x0a, 0x27, 0x29, 0xa1,
+ 0x54, 0xa8,
+ ];
+ let expected = "AAGuid(cb69481e-8ff0-0039-93ec-0a2729a154a8)";
+ let result = AAGuid::from(&input).expect("Failed to parse AAGuid");
+ let res_str = format!("{:?}", result);
+ assert_eq!(expected, &res_str);
+ }
+}
diff --git a/third_party/rust/authenticator/src/ctap2/client_data.rs b/third_party/rust/authenticator/src/ctap2/client_data.rs
new file mode 100644
index 0000000000..6e214b17ff
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/client_data.rs
@@ -0,0 +1,399 @@
+use super::commands::CommandError;
+use crate::transport::errors::HIDError;
+use serde::de::{self, Deserializer, Error as SerdeError, MapAccess, Visitor};
+use serde::ser::SerializeMap;
+use serde::{Deserialize, Serialize, Serializer};
+use serde_json as json;
+use sha2::{Digest, Sha256};
+use std::fmt;
+
+/// https://w3c.github.io/webauthn/#dom-collectedclientdata-tokenbinding
+// tokenBinding, of type TokenBinding
+//
+// This OPTIONAL member contains information about the state of the Token
+// Binding protocol [TokenBinding] used when communicating with the Relying
+// Party. Its absence indicates that the client doesn’t support token
+// binding.
+//
+// status, of type TokenBindingStatus
+//
+// This member is one of the following:
+//
+// supported
+//
+// Indicates the client supports token binding, but it was not
+// negotiated when communicating with the Relying Party.
+//
+// present
+//
+// Indicates token binding was used when communicating with the
+// Relying Party. In this case, the id member MUST be present.
+//
+// id, of type DOMString
+//
+// This member MUST be present if status is present, and MUST be a
+// base64url encoding of the Token Binding ID that was used when
+// communicating with the Relying Party.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum TokenBinding {
+ Present(String),
+ Supported,
+}
+
+impl Serialize for TokenBinding {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let mut map = serializer.serialize_map(Some(2))?;
+ match *self {
+ TokenBinding::Supported => {
+ map.serialize_entry(&"status", &"supported")?;
+ }
+ TokenBinding::Present(ref v) => {
+ map.serialize_entry(&"status", "present")?;
+ // Verify here, that `v` is valid base64 encoded?
+ // base64::decode_config(&v, base64::URL_SAFE_NO_PAD);
+ // For now: Let the token do that.
+ map.serialize_entry(&"id", &v)?;
+ }
+ }
+ map.end()
+ }
+}
+
+impl<'de> Deserialize<'de> for TokenBinding {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct TokenBindingVisitor;
+
+ impl<'de> Visitor<'de> for TokenBindingVisitor {
+ type Value = TokenBinding;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a byte string")
+ }
+
+ fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ where
+ M: MapAccess<'de>,
+ {
+ let mut id = None;
+ let mut status = None;
+
+ while let Some(key) = map.next_key()? {
+ match key {
+ "status" => {
+ status = Some(map.next_value()?);
+ }
+ "id" => {
+ id = Some(map.next_value()?);
+ }
+ k => {
+ return Err(M::Error::custom(format!("unexpected key: {:?}", k)));
+ }
+ }
+ }
+
+ if let Some(stat) = status {
+ match stat {
+ "present" => {
+ if let Some(id) = id {
+ Ok(TokenBinding::Present(id))
+ } else {
+ Err(SerdeError::missing_field("id"))
+ }
+ }
+ "supported" => Ok(TokenBinding::Supported),
+ k => {
+ return Err(M::Error::custom(format!(
+ "unexpected status key: {:?}",
+ k
+ )));
+ }
+ }
+ } else {
+ Err(SerdeError::missing_field("status"))
+ }
+ }
+ }
+
+ deserializer.deserialize_map(TokenBindingVisitor)
+ }
+}
+
+/// https://w3c.github.io/webauthn/#dom-collectedclientdata-type
+// type, of type DOMString
+//
+// This member contains the string "webauthn.create" when creating new
+// credentials, and "webauthn.get" when getting an assertion from an
+// existing credential. The purpose of this member is to prevent certain
+// types of signature confusion attacks (where an attacker substitutes one
+// legitimate signature for another).
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum WebauthnType {
+ Create,
+ Get,
+}
+
+impl Serialize for WebauthnType {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ match *self {
+ WebauthnType::Create => serializer.serialize_str(&"webauthn.create"),
+ WebauthnType::Get => serializer.serialize_str(&"webauthn.get"),
+ }
+ }
+}
+
+impl<'de> Deserialize<'de> for WebauthnType {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct WebauthnTypeVisitor;
+
+ impl<'de> Visitor<'de> for WebauthnTypeVisitor {
+ type Value = WebauthnType;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a string")
+ }
+
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: de::Error,
+ {
+ match v {
+ "webauthn.create" => Ok(WebauthnType::Create),
+ "webauthn.get" => Ok(WebauthnType::Get),
+ _ => Err(E::custom("unexpected webauthn_type")),
+ }
+ }
+ }
+
+ deserializer.deserialize_str(WebauthnTypeVisitor)
+ }
+}
+
+#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
+pub struct Challenge(pub String);
+
+impl Challenge {
+ pub fn new(input: Vec<u8>) -> Self {
+ let value = base64::encode_config(&input, base64::URL_SAFE_NO_PAD);
+ Challenge(value)
+ }
+}
+
+impl From<Vec<u8>> for Challenge {
+ fn from(v: Vec<u8>) -> Challenge {
+ Challenge::new(v)
+ }
+}
+
+impl AsRef<[u8]> for Challenge {
+ fn as_ref(&self) -> &[u8] {
+ self.0.as_bytes()
+ }
+}
+
+pub type Origin = String;
+
+#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
+pub struct CollectedClientData {
+ #[serde(rename = "type")]
+ pub webauthn_type: WebauthnType,
+ pub challenge: Challenge,
+ pub origin: Origin,
+ // It is optional, according to https://www.w3.org/TR/webauthn/#collectedclientdata-hash-of-the-serialized-client-data
+ // But we are serializing it, so we *have to* set crossOrigin (if not given, we have to set it to false)
+ // Thus, on our side, it is not optional. For deserializing, we provide a default (bool's default == False)
+ #[serde(rename = "crossOrigin", default)]
+ pub cross_origin: bool,
+ #[serde(rename = "tokenBinding", skip_serializing_if = "Option::is_none")]
+ pub token_binding: Option<TokenBinding>,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct ClientDataHash([u8; 32]);
+
+impl PartialEq<[u8]> for ClientDataHash {
+ fn eq(&self, other: &[u8]) -> bool {
+ self.0.eq(other)
+ }
+}
+
+impl AsRef<[u8]> for ClientDataHash {
+ fn as_ref(&self) -> &[u8] {
+ &self.0
+ }
+}
+
+impl Serialize for ClientDataHash {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_bytes(&self.0)
+ }
+}
+
+#[cfg(test)]
+impl<'de> Deserialize<'de> for ClientDataHash {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct ClientDataHashVisitor;
+
+ impl<'de> Visitor<'de> for ClientDataHashVisitor {
+ type Value = ClientDataHash;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a byte string")
+ }
+
+ fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
+ where
+ E: de::Error,
+ {
+ let mut out = [0u8; 32];
+ if out.len() != v.len() {
+ return Err(E::invalid_length(v.len(), &"32"));
+ }
+ out.copy_from_slice(v);
+ Ok(ClientDataHash(out))
+ }
+ }
+
+ deserializer.deserialize_bytes(ClientDataHashVisitor)
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct CollectedClientDataWrapper {
+ pub client_data: CollectedClientData,
+ pub serialized_data: Vec<u8>,
+}
+
+impl CollectedClientDataWrapper {
+ pub fn new(client_data: CollectedClientData) -> Result<Self, HIDError> {
+ let serialized_data = json::to_vec(&client_data).map_err(CommandError::Json)?;
+ Ok(CollectedClientDataWrapper {
+ client_data,
+ serialized_data,
+ })
+ }
+
+ pub fn hash(&self) -> ClientDataHash {
+ // WebIDL's dictionary definition specifies that the order of the struct
+ // is exactly as the WebIDL specification declares it, with an algorithm
+ // for partial dictionaries, so that's how interop works for these
+ // things.
+ // See: https://heycam.github.io/webidl/#dfn-dictionary
+ let mut hasher = Sha256::new();
+ hasher.update(&self.serialized_data);
+
+ let mut output = [0u8; 32];
+ output.copy_from_slice(hasher.finalize().as_slice());
+
+ ClientDataHash(output)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::CollectedClientDataWrapper;
+
+ use super::{Challenge, ClientDataHash, CollectedClientData, TokenBinding, WebauthnType};
+ use serde_json as json;
+
+ #[test]
+ fn test_token_binding_status() {
+ let tok = TokenBinding::Present("AAECAw".to_string());
+
+ let json_value = json::to_string(&tok).unwrap();
+ assert_eq!(json_value, "{\"status\":\"present\",\"id\":\"AAECAw\"}");
+
+ let tok = TokenBinding::Supported;
+
+ let json_value = json::to_string(&tok).unwrap();
+ assert_eq!(json_value, "{\"status\":\"supported\"}");
+ }
+
+ #[test]
+ fn test_webauthn_type() {
+ let t = WebauthnType::Create;
+
+ let json_value = json::to_string(&t).unwrap();
+ assert_eq!(json_value, "\"webauthn.create\"");
+
+ let t = WebauthnType::Get;
+ let json_value = json::to_string(&t).unwrap();
+ assert_eq!(json_value, "\"webauthn.get\"");
+ }
+
+ #[test]
+ fn test_collected_client_data_parsing() {
+ let original_str = "{\"type\":\"webauthn.create\",\"challenge\":\"AAECAw\",\"origin\":\"example.com\",\"crossOrigin\":false,\"tokenBinding\":{\"status\":\"present\",\"id\":\"AAECAw\"}}";
+ let parsed: CollectedClientData = serde_json::from_str(&original_str).unwrap();
+ let expected = CollectedClientData {
+ webauthn_type: WebauthnType::Create,
+ challenge: Challenge::new(vec![0x00, 0x01, 0x02, 0x03]),
+ origin: String::from("example.com"),
+ cross_origin: false,
+ token_binding: Some(TokenBinding::Present("AAECAw".to_string())),
+ };
+ assert_eq!(parsed, expected);
+
+ let back_again = serde_json::to_string(&expected).unwrap();
+ assert_eq!(back_again, original_str);
+ }
+
+ #[test]
+ fn test_collected_client_data_defaults() {
+ let cross_origin_str = "{\"type\":\"webauthn.create\",\"challenge\":\"AAECAw\",\"origin\":\"example.com\",\"crossOrigin\":false,\"tokenBinding\":{\"status\":\"present\",\"id\":\"AAECAw\"}}";
+ let no_cross_origin_str = "{\"type\":\"webauthn.create\",\"challenge\":\"AAECAw\",\"origin\":\"example.com\",\"tokenBinding\":{\"status\":\"present\",\"id\":\"AAECAw\"}}";
+ let parsed: CollectedClientData = serde_json::from_str(&no_cross_origin_str).unwrap();
+ let expected = CollectedClientData {
+ webauthn_type: WebauthnType::Create,
+ challenge: Challenge::new(vec![0x00, 0x01, 0x02, 0x03]),
+ origin: String::from("example.com"),
+ cross_origin: false,
+ token_binding: Some(TokenBinding::Present("AAECAw".to_string())),
+ };
+ assert_eq!(parsed, expected);
+
+ let back_again = serde_json::to_string(&expected).unwrap();
+ assert_eq!(back_again, cross_origin_str);
+ }
+
+ #[test]
+ fn test_collected_client_data() {
+ let client_data = CollectedClientData {
+ webauthn_type: WebauthnType::Create,
+ challenge: Challenge::new(vec![0x00, 0x01, 0x02, 0x03]),
+ origin: String::from("example.com"),
+ cross_origin: false,
+ token_binding: Some(TokenBinding::Present("AAECAw".to_string())),
+ };
+ let c =
+ CollectedClientDataWrapper::new(client_data).expect("Failed to serialize client_data");
+ assert_eq!(
+ c.hash(),
+ // echo -n '{"type":"webauthn.create","challenge":"AAECAw","origin":"example.com","crossOrigin":false,"tokenBinding":{"status":"present","id":"AAECAw"}}' | sha256sum -t
+ ClientDataHash {
+ 0: [
+ 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11,
+ 0x32, 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f,
+ 0x10, 0x87, 0x54, 0xc3, 0x2d, 0x80
+ ]
+ }
+ );
+ }
+}
diff --git a/third_party/rust/authenticator/src/ctap2/commands/client_pin.rs b/third_party/rust/authenticator/src/ctap2/commands/client_pin.rs
new file mode 100644
index 0000000000..c70ce73cb0
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/client_pin.rs
@@ -0,0 +1,786 @@
+use super::{get_info::AuthenticatorInfo, Command, CommandError, RequestCtap2, StatusCode};
+use crate::crypto::{
+ authenticate, decrypt, encapsulate, encrypt, BackendError, COSEKey, CryptoError, ECDHSecret,
+};
+use crate::transport::errors::HIDError;
+use crate::u2ftypes::U2FDevice;
+use serde::{
+ de::{Error as SerdeError, IgnoredAny, MapAccess, Visitor},
+ ser::SerializeMap,
+ Deserialize, Deserializer, Serialize, Serializer,
+};
+use serde_bytes::ByteBuf;
+use serde_cbor::de::from_slice;
+use serde_cbor::ser::to_vec;
+use serde_cbor::Value;
+use sha2::{Digest, Sha256};
+use std::error::Error as StdErrorT;
+use std::fmt;
+
+#[derive(Debug, Copy, Clone)]
+#[repr(u8)]
+pub enum PINSubcommand {
+ GetRetries = 0x01,
+ GetKeyAgreement = 0x02,
+ SetPIN = 0x03,
+ ChangePIN = 0x04,
+ GetPINToken = 0x05, // superseded by GetPinUvAuth*
+ GetPinUvAuthTokenUsingUvWithPermissions = 0x06,
+ GetUVRetries = 0x07,
+ GetPinUvAuthTokenUsingPinWithPermissions = 0x09, // Yes, 0x08 is missing
+}
+
+#[derive(Debug, Copy, Clone)]
+#[repr(u8)]
+pub enum PinUvAuthTokenPermission {
+ MakeCredential = 0x01, // rp_id required
+ GetAssertion = 0x02, // rp_id required
+ CredentialManagement = 0x04, // rp_id optional
+ BioEnrollment = 0x08, // rp_id ignored
+ LargeBlobWrite = 0x10, // rp_id ignored
+ AuthenticatorConfiguration = 0x20, // rp_id ignored
+}
+
+#[derive(Debug)]
+pub struct ClientPIN {
+ pin_protocol: Option<u8>,
+ subcommand: PINSubcommand,
+ key_agreement: Option<COSEKey>,
+ pin_auth: Option<PinAuth>,
+ new_pin_enc: Option<ByteBuf>,
+ pin_hash_enc: Option<ByteBuf>,
+ permissions: Option<u8>,
+ rp_id: Option<String>,
+}
+
+impl Default for ClientPIN {
+ fn default() -> Self {
+ ClientPIN {
+ pin_protocol: None,
+ subcommand: PINSubcommand::GetRetries,
+ key_agreement: None,
+ pin_auth: None,
+ new_pin_enc: None,
+ pin_hash_enc: None,
+ permissions: None,
+ rp_id: None,
+ }
+ }
+}
+
+impl Serialize for ClientPIN {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ // Need to define how many elements are going to be in the map
+ // beforehand
+ let mut map_len = 1;
+ if self.pin_protocol.is_some() {
+ map_len += 1;
+ }
+ if self.key_agreement.is_some() {
+ map_len += 1;
+ }
+ if self.pin_auth.is_some() {
+ map_len += 1;
+ }
+ if self.new_pin_enc.is_some() {
+ map_len += 1;
+ }
+ if self.pin_hash_enc.is_some() {
+ map_len += 1;
+ }
+ if self.permissions.is_some() {
+ map_len += 1;
+ }
+ if self.rp_id.is_some() {
+ map_len += 1;
+ }
+
+ let mut map = serializer.serialize_map(Some(map_len))?;
+ if let Some(ref pin_protocol) = self.pin_protocol {
+ map.serialize_entry(&1, pin_protocol)?;
+ }
+ let command: u8 = self.subcommand as u8;
+ map.serialize_entry(&2, &command)?;
+ if let Some(ref key_agreement) = self.key_agreement {
+ map.serialize_entry(&3, key_agreement)?;
+ }
+ if let Some(ref pin_auth) = self.pin_auth {
+ map.serialize_entry(&4, &ByteBuf::from(pin_auth.as_ref()))?;
+ }
+ if let Some(ref new_pin_enc) = self.new_pin_enc {
+ map.serialize_entry(&5, new_pin_enc)?;
+ }
+ if let Some(ref pin_hash_enc) = self.pin_hash_enc {
+ map.serialize_entry(&6, pin_hash_enc)?;
+ }
+ if let Some(ref permissions) = self.permissions {
+ map.serialize_entry(&9, permissions)?;
+ }
+ if let Some(ref rp_id) = self.rp_id {
+ map.serialize_entry(&0x0A, rp_id)?;
+ }
+
+ map.end()
+ }
+}
+
+pub trait ClientPINSubCommand {
+ type Output;
+ fn as_client_pin(&self) -> Result<ClientPIN, CommandError>;
+ fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError>;
+}
+
+struct ClientPinResponse {
+ key_agreement: Option<COSEKey>,
+ pin_token: Option<EncryptedPinToken>,
+ /// Number of PIN attempts remaining before lockout.
+ pin_retries: Option<u8>,
+ power_cycle_state: Option<bool>,
+ uv_retries: Option<u8>,
+}
+
+impl<'de> Deserialize<'de> for ClientPinResponse {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct ClientPinResponseVisitor;
+
+ impl<'de> Visitor<'de> for ClientPinResponseVisitor {
+ type Value = ClientPinResponse;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a map")
+ }
+
+ fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ where
+ M: MapAccess<'de>,
+ {
+ let mut key_agreement = None;
+ let mut pin_token = None;
+ let mut pin_retries = None;
+ let mut power_cycle_state = None;
+ let mut uv_retries = None;
+ while let Some(key) = map.next_key()? {
+ match key {
+ 0x01 => {
+ if key_agreement.is_some() {
+ return Err(SerdeError::duplicate_field("key_agreement"));
+ }
+ key_agreement = map.next_value()?;
+ }
+ 0x02 => {
+ if pin_token.is_some() {
+ return Err(SerdeError::duplicate_field("pin_token"));
+ }
+ pin_token = map.next_value()?;
+ }
+ 0x03 => {
+ if pin_retries.is_some() {
+ return Err(SerdeError::duplicate_field("pin_retries"));
+ }
+ pin_retries = Some(map.next_value()?);
+ }
+ 0x04 => {
+ if power_cycle_state.is_some() {
+ return Err(SerdeError::duplicate_field("power_cycle_state"));
+ }
+ power_cycle_state = Some(map.next_value()?);
+ }
+ 0x05 => {
+ if uv_retries.is_some() {
+ return Err(SerdeError::duplicate_field("uv_retries"));
+ }
+ uv_retries = Some(map.next_value()?);
+ }
+ k => {
+ warn!("ClientPinResponse: unexpected key: {:?}", k);
+ let _ = map.next_value::<IgnoredAny>()?;
+ continue;
+ }
+ }
+ }
+ Ok(ClientPinResponse {
+ key_agreement,
+ pin_token,
+ pin_retries,
+ power_cycle_state,
+ uv_retries,
+ })
+ }
+ }
+ deserializer.deserialize_bytes(ClientPinResponseVisitor)
+ }
+}
+
+#[derive(Debug)]
+pub struct GetKeyAgreement {
+ pin_protocol: u8,
+}
+
+impl GetKeyAgreement {
+ pub fn new(info: &AuthenticatorInfo) -> Result<Self, CommandError> {
+ if info.pin_protocols.contains(&1) {
+ Ok(GetKeyAgreement { pin_protocol: 1 })
+ } else {
+ Err(CommandError::UnsupportedPinProtocol)
+ }
+ }
+}
+
+impl ClientPINSubCommand for GetKeyAgreement {
+ type Output = KeyAgreement;
+
+ fn as_client_pin(&self) -> Result<ClientPIN, CommandError> {
+ Ok(ClientPIN {
+ pin_protocol: Some(self.pin_protocol),
+ subcommand: PINSubcommand::GetKeyAgreement,
+ ..ClientPIN::default()
+ })
+ }
+
+ fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> {
+ let value: Value = from_slice(input).map_err(CommandError::Deserializing)?;
+ debug!("GetKeyAgreement::parse_response_payload {:?}", value);
+
+ let get_pin_response: ClientPinResponse =
+ from_slice(input).map_err(CommandError::Deserializing)?;
+ if let Some(key_agreement) = get_pin_response.key_agreement {
+ Ok(KeyAgreement(key_agreement))
+ } else {
+ Err(CommandError::MissingRequiredField("key_agreement"))
+ }
+ }
+}
+
+#[derive(Debug)]
+/// Superseded by GetPinUvAuthTokenUsingUvWithPermissions or GetPinUvAuthTokenUsingPinWithPermissions,
+/// thus for backwards compatibility only
+pub struct GetPinToken<'sc, 'pin> {
+ pin_protocol: u8,
+ shared_secret: &'sc ECDHSecret,
+ pin: &'pin Pin,
+}
+
+impl<'sc, 'pin> GetPinToken<'sc, 'pin> {
+ pub fn new(
+ info: &AuthenticatorInfo,
+ shared_secret: &'sc ECDHSecret,
+ pin: &'pin Pin,
+ ) -> Result<Self, CommandError> {
+ if info.pin_protocols.contains(&1) {
+ Ok(GetPinToken {
+ pin_protocol: 1,
+ shared_secret,
+ pin,
+ })
+ } else {
+ Err(CommandError::UnsupportedPinProtocol)
+ }
+ }
+}
+
+impl<'sc, 'pin> ClientPINSubCommand for GetPinToken<'sc, 'pin> {
+ type Output = PinToken;
+
+ fn as_client_pin(&self) -> Result<ClientPIN, CommandError> {
+ let input = self.pin.for_pin_token();
+ trace!("pin_hash = {:#04X?}", &input.as_ref());
+ let pin_hash_enc = encrypt(self.shared_secret.shared_secret(), input.as_ref())
+ .map_err(|e| CryptoError::Backend(e))?;
+ trace!("pin_hash_enc = {:#04X?}", &pin_hash_enc);
+
+ Ok(ClientPIN {
+ pin_protocol: Some(self.pin_protocol),
+ subcommand: PINSubcommand::GetPINToken,
+ key_agreement: Some(self.shared_secret.my_public_key().clone()),
+ pin_hash_enc: Some(ByteBuf::from(pin_hash_enc)),
+ ..ClientPIN::default()
+ })
+ }
+
+ fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> {
+ let value: Value = from_slice(input).map_err(CommandError::Deserializing)?;
+ debug!("GetKeyAgreement::parse_response_payload {:?}", value);
+
+ let get_pin_response: ClientPinResponse =
+ from_slice(input).map_err(CommandError::Deserializing)?;
+ match get_pin_response.pin_token {
+ Some(encrypted_pin_token) => {
+ let pin_token = decrypt(
+ self.shared_secret.shared_secret(),
+ encrypted_pin_token.as_ref(),
+ )
+ .map_err(|e| CryptoError::Backend(e))?;
+ let pin_token = PinToken(pin_token);
+ Ok(pin_token)
+ }
+ None => Err(CommandError::MissingRequiredField("key_agreement")),
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct GetPinUvAuthTokenUsingPinWithPermissions<'sc, 'pin> {
+ pin_protocol: u8,
+ shared_secret: &'sc ECDHSecret,
+ pin: &'pin Pin,
+ permissions: PinUvAuthTokenPermission,
+ rp_id: Option<String>,
+}
+
+impl<'sc, 'pin> GetPinUvAuthTokenUsingPinWithPermissions<'sc, 'pin> {
+ pub fn new(
+ info: &AuthenticatorInfo,
+ shared_secret: &'sc ECDHSecret,
+ pin: &'pin Pin,
+ permissions: PinUvAuthTokenPermission,
+ rp_id: Option<String>,
+ ) -> Result<Self, CommandError> {
+ // TODO(MS): Actually handle protocol 2!
+ if info.pin_protocols.contains(&1) {
+ Ok(GetPinUvAuthTokenUsingPinWithPermissions {
+ pin_protocol: 1,
+ shared_secret,
+ pin,
+ permissions,
+ rp_id,
+ })
+ } else {
+ Err(CommandError::UnsupportedPinProtocol)
+ }
+ }
+}
+
+impl<'sc, 'pin> ClientPINSubCommand for GetPinUvAuthTokenUsingPinWithPermissions<'sc, 'pin> {
+ type Output = PinToken;
+
+ fn as_client_pin(&self) -> Result<ClientPIN, CommandError> {
+ let input = self.pin.for_pin_token();
+ let pin_hash_enc = encrypt(self.shared_secret.shared_secret(), input.as_ref())
+ .map_err(|e| CryptoError::Backend(e))?;
+
+ Ok(ClientPIN {
+ pin_protocol: Some(self.pin_protocol),
+ subcommand: PINSubcommand::GetPinUvAuthTokenUsingPinWithPermissions,
+ key_agreement: Some(self.shared_secret.my_public_key().clone()),
+ pin_hash_enc: Some(ByteBuf::from(pin_hash_enc)),
+ permissions: Some(self.permissions as u8),
+ rp_id: self.rp_id.clone(), // TODO: This could probably be done less wasteful with &str all the way
+ ..ClientPIN::default()
+ })
+ }
+
+ fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> {
+ let value: Value = from_slice(input).map_err(CommandError::Deserializing)?;
+ debug!("GetKeyAgreement::parse_response_payload {:?}", value);
+
+ let get_pin_response: ClientPinResponse =
+ from_slice(input).map_err(CommandError::Deserializing)?;
+ match get_pin_response.pin_token {
+ Some(encrypted_pin_token) => {
+ let pin_token = decrypt(
+ self.shared_secret.shared_secret(),
+ encrypted_pin_token.as_ref(),
+ )
+ .map_err(|e| CryptoError::Backend(e))?;
+ let pin_token = PinToken(pin_token);
+ Ok(pin_token)
+ }
+ None => Err(CommandError::MissingRequiredField("key_agreement")),
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct GetRetries {}
+
+impl GetRetries {
+ pub fn new() -> Self {
+ GetRetries {}
+ }
+}
+
+impl ClientPINSubCommand for GetRetries {
+ type Output = u8;
+
+ fn as_client_pin(&self) -> Result<ClientPIN, CommandError> {
+ Ok(ClientPIN {
+ subcommand: PINSubcommand::GetRetries,
+ ..ClientPIN::default()
+ })
+ }
+
+ fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> {
+ let value: Value = from_slice(input).map_err(CommandError::Deserializing)?;
+ debug!("GetKeyAgreement::parse_response_payload {:?}", value);
+
+ let get_pin_response: ClientPinResponse =
+ from_slice(input).map_err(CommandError::Deserializing)?;
+ match get_pin_response.pin_retries {
+ Some(pin_retries) => Ok(pin_retries),
+ None => Err(CommandError::MissingRequiredField("pin_retries")),
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct SetNewPin<'sc, 'pin> {
+ pin_protocol: u8,
+ shared_secret: &'sc ECDHSecret,
+ new_pin: &'pin Pin,
+}
+
+impl<'sc, 'pin> SetNewPin<'sc, 'pin> {
+ pub fn new(
+ info: &AuthenticatorInfo,
+ shared_secret: &'sc ECDHSecret,
+ new_pin: &'pin Pin,
+ ) -> Result<Self, CommandError> {
+ if info.pin_protocols.contains(&1) {
+ Ok(SetNewPin {
+ pin_protocol: 1,
+ shared_secret,
+ new_pin,
+ })
+ } else {
+ Err(CommandError::UnsupportedPinProtocol)
+ }
+ }
+}
+
+impl<'sc, 'pin> ClientPINSubCommand for SetNewPin<'sc, 'pin> {
+ type Output = ();
+
+ fn as_client_pin(&self) -> Result<ClientPIN, CommandError> {
+ if self.new_pin.as_bytes().len() > 63 {
+ return Err(CommandError::StatusCode(
+ StatusCode::PinPolicyViolation,
+ None,
+ ));
+ }
+ // Padding the PIN with trailing zeros, according to spec
+ let input: Vec<u8> = self
+ .new_pin
+ .as_bytes()
+ .iter()
+ .chain(std::iter::repeat(&0x00))
+ .take(64)
+ .cloned()
+ .collect();
+
+ let shared_secret = self.shared_secret.shared_secret();
+ // AES256-CBC(sharedSecret, IV=0, newPin)
+ let new_pin_enc =
+ encrypt(shared_secret, input.as_ref()).map_err(|e| CryptoError::Backend(e))?;
+
+ // LEFT(HMAC-SHA-265(sharedSecret, newPinEnc), 16)
+ let pin_auth = PinToken(shared_secret.to_vec())
+ .auth(&new_pin_enc)
+ .map_err(CommandError::Crypto)?;
+
+ Ok(ClientPIN {
+ pin_protocol: Some(self.pin_protocol),
+ subcommand: PINSubcommand::SetPIN,
+ key_agreement: Some(self.shared_secret.my_public_key().clone()),
+ new_pin_enc: Some(ByteBuf::from(new_pin_enc)),
+ pin_auth: Some(pin_auth),
+ ..ClientPIN::default()
+ })
+ }
+
+ fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> {
+ // Should be an empty response or a valid cbor-value (which we ignore)
+ if input.is_empty() {
+ Ok(())
+ } else {
+ let _: Value = from_slice(input).map_err(CommandError::Deserializing)?;
+ Ok(())
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct ChangeExistingPin<'sc, 'pin> {
+ pin_protocol: u8,
+ shared_secret: &'sc ECDHSecret,
+ current_pin: &'pin Pin,
+ new_pin: &'pin Pin,
+}
+
+impl<'sc, 'pin> ChangeExistingPin<'sc, 'pin> {
+ pub fn new(
+ info: &AuthenticatorInfo,
+ shared_secret: &'sc ECDHSecret,
+ current_pin: &'pin Pin,
+ new_pin: &'pin Pin,
+ ) -> Result<Self, CommandError> {
+ if info.pin_protocols.contains(&1) {
+ Ok(ChangeExistingPin {
+ pin_protocol: 1,
+ shared_secret,
+ current_pin,
+ new_pin,
+ })
+ } else {
+ Err(CommandError::UnsupportedPinProtocol)
+ }
+ }
+}
+
+impl<'sc, 'pin> ClientPINSubCommand for ChangeExistingPin<'sc, 'pin> {
+ type Output = ();
+
+ fn as_client_pin(&self) -> Result<ClientPIN, CommandError> {
+ if self.new_pin.as_bytes().len() > 63 {
+ return Err(CommandError::StatusCode(
+ StatusCode::PinPolicyViolation,
+ None,
+ ));
+ }
+ // Padding the PIN with trailing zeros, according to spec
+ let input: Vec<u8> = self
+ .new_pin
+ .as_bytes()
+ .iter()
+ .chain(std::iter::repeat(&0x00))
+ .take(64)
+ .cloned()
+ .collect();
+
+ let shared_secret = self.shared_secret.shared_secret();
+ // AES256-CBC(sharedSecret, IV=0, newPin)
+ let new_pin_enc =
+ encrypt(shared_secret, input.as_ref()).map_err(|e| CryptoError::Backend(e))?;
+
+ // AES256-CBC(sharedSecret, IV=0, LEFT(SHA-256(oldPin), 16))
+ let input = self.current_pin.for_pin_token();
+ let pin_hash_enc = encrypt(self.shared_secret.shared_secret(), input.as_ref())
+ .map_err(|e| CryptoError::Backend(e))?;
+
+ // LEFT(HMAC-SHA-265(sharedSecret, newPinEnc), 16)
+ let pin_auth = PinToken(shared_secret.to_vec())
+ .auth(&[new_pin_enc.as_slice(), pin_hash_enc.as_slice()].concat())
+ .map_err(CommandError::Crypto)?;
+
+ Ok(ClientPIN {
+ pin_protocol: Some(self.pin_protocol),
+ subcommand: PINSubcommand::ChangePIN,
+ key_agreement: Some(self.shared_secret.my_public_key().clone()),
+ new_pin_enc: Some(ByteBuf::from(new_pin_enc)),
+ pin_hash_enc: Some(ByteBuf::from(pin_hash_enc)),
+ pin_auth: Some(pin_auth),
+ permissions: None,
+ rp_id: None,
+ })
+ }
+
+ fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> {
+ // Should be an empty response or a valid cbor-value (which we ignore)
+ if input.is_empty() {
+ Ok(())
+ } else {
+ let _: Value = from_slice(input).map_err(CommandError::Deserializing)?;
+ Ok(())
+ }
+ }
+}
+
+impl<T> RequestCtap2 for T
+where
+ T: ClientPINSubCommand,
+ T: fmt::Debug,
+{
+ type Output = <T as ClientPINSubCommand>::Output;
+
+ fn command() -> Command {
+ Command::ClientPin
+ }
+
+ fn wire_format<Dev>(&self, _dev: &mut Dev) -> Result<Vec<u8>, HIDError>
+ where
+ Dev: U2FDevice,
+ {
+ let client_pin = self.as_client_pin()?;
+ let output = to_vec(&client_pin).map_err(CommandError::Serializing)?;
+ trace!("client subcommmand: {:04X?}", &output);
+
+ Ok(output)
+ }
+
+ fn handle_response_ctap2<Dev>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError>
+ where
+ Dev: U2FDevice,
+ {
+ trace!("Client pin subcomand response:{:04X?}", &input);
+ if input.is_empty() {
+ return Err(CommandError::InputTooSmall.into());
+ }
+
+ let status: StatusCode = input[0].into();
+ debug!("response status code: {:?}", status);
+ if status.is_ok() {
+ let res = <T as ClientPINSubCommand>::parse_response_payload(self, &input[1..])
+ .map_err(HIDError::Command);
+ res
+ } else {
+ let add_data = if input.len() > 1 {
+ let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ Some(data)
+ } else {
+ None
+ };
+ Err(CommandError::StatusCode(status, add_data).into())
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct KeyAgreement(COSEKey);
+
+impl KeyAgreement {
+ pub fn shared_secret(&self) -> Result<ECDHSecret, CommandError> {
+ encapsulate(&self.0).map_err(|e| CommandError::Crypto(CryptoError::Backend(e)))
+ }
+}
+
+#[derive(Debug, Deserialize)]
+pub struct EncryptedPinToken(ByteBuf);
+
+impl AsRef<[u8]> for EncryptedPinToken {
+ fn as_ref(&self) -> &[u8] {
+ self.0.as_ref()
+ }
+}
+
+#[derive(Debug)]
+pub struct PinToken(Vec<u8>);
+
+impl PinToken {
+ pub fn auth(&self, payload: &[u8]) -> Result<PinAuth, CryptoError> {
+ let hmac = authenticate(self.as_ref(), payload)?;
+
+ let mut out = [0u8; 16];
+ out.copy_from_slice(&hmac[0..16]);
+
+ Ok(PinAuth(out.to_vec()))
+ }
+}
+
+impl AsRef<[u8]> for PinToken {
+ fn as_ref(&self) -> &[u8] {
+ self.0.as_ref()
+ }
+}
+
+#[derive(Debug, Clone)]
+#[cfg_attr(test, derive(Deserialize))]
+pub struct PinAuth(Vec<u8>);
+
+impl PinAuth {
+ pub(crate) fn empty_pin_auth() -> Self {
+ PinAuth(vec![])
+ }
+}
+
+impl AsRef<[u8]> for PinAuth {
+ fn as_ref(&self) -> &[u8] {
+ &self.0
+ }
+}
+
+impl Serialize for PinAuth {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serde_bytes::serialize(&self.0[..], serializer)
+ }
+}
+
+#[derive(Clone)]
+pub struct Pin(String);
+
+impl fmt::Debug for Pin {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "Pin(redacted)")
+ }
+}
+
+impl Pin {
+ pub fn new(value: &str) -> Pin {
+ Pin(String::from(value))
+ }
+
+ pub fn for_pin_token(&self) -> PinAuth {
+ let mut hasher = Sha256::new();
+ hasher.update(&self.0.as_bytes());
+
+ let mut output = [0u8; 16];
+ let len = output.len();
+ output.copy_from_slice(&hasher.finalize().as_slice()[..len]);
+
+ PinAuth(output.to_vec())
+ }
+
+ pub fn as_bytes(&self) -> &[u8] {
+ self.0.as_bytes()
+ }
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub enum PinError {
+ PinRequired,
+ PinIsTooShort,
+ PinIsTooLong(usize),
+ InvalidKeyLen,
+ InvalidPin(Option<u8>),
+ PinAuthBlocked,
+ PinBlocked,
+ PinNotSet,
+ Backend(BackendError),
+}
+
+impl fmt::Display for PinError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ PinError::PinRequired => write!(f, "PinError: Pin required."),
+ PinError::PinIsTooShort => write!(f, "PinError: pin is too short"),
+ PinError::PinIsTooLong(len) => write!(f, "PinError: pin is too long ({})", len),
+ PinError::InvalidKeyLen => write!(f, "PinError: invalid key len"),
+ PinError::InvalidPin(ref e) => {
+ let mut res = write!(f, "PinError: Invalid Pin.");
+ if let Some(pin_retries) = e {
+ res = write!(f, " Retries left: {:?}", pin_retries)
+ }
+ res
+ }
+ PinError::PinAuthBlocked => write!(
+ f,
+ "PinError: Pin authentication blocked. Device needs power cycle."
+ ),
+ PinError::PinBlocked => write!(
+ f,
+ "PinError: No retries left. Pin blocked. Device needs reset."
+ ),
+ PinError::PinNotSet => write!(f, "PinError: Pin needed but not set on device."),
+ PinError::Backend(ref e) => write!(f, "PinError: Crypto backend error: {:?}", e),
+ }
+ }
+}
+
+impl StdErrorT for PinError {}
+
+impl From<BackendError> for PinError {
+ fn from(e: BackendError) -> Self {
+ PinError::Backend(e)
+ }
+}
diff --git a/third_party/rust/authenticator/src/ctap2/commands/get_assertion.rs b/third_party/rust/authenticator/src/ctap2/commands/get_assertion.rs
new file mode 100644
index 0000000000..16704f0ba8
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/get_assertion.rs
@@ -0,0 +1,1282 @@
+use super::{
+ Command, CommandError, PinAuthCommand, Request, RequestCtap1, RequestCtap2, Retryable,
+ StatusCode,
+};
+use crate::consts::{
+ PARAMETER_SIZE, U2F_AUTHENTICATE, U2F_CHECK_IS_REGISTERED, U2F_REQUEST_USER_PRESENCE,
+};
+use crate::crypto::{authenticate, encrypt, COSEKey, CryptoError, ECDHSecret};
+use crate::ctap2::attestation::{AuthenticatorData, AuthenticatorDataFlags};
+use crate::ctap2::client_data::{ClientDataHash, CollectedClientData, CollectedClientDataWrapper};
+use crate::ctap2::commands::client_pin::{Pin, PinAuth};
+use crate::ctap2::commands::get_next_assertion::GetNextAssertion;
+use crate::ctap2::commands::make_credentials::UserVerification;
+use crate::ctap2::server::{PublicKeyCredentialDescriptor, RelyingPartyWrapper, User};
+use crate::errors::AuthenticatorError;
+use crate::transport::errors::{ApduErrorStatus, HIDError};
+use crate::transport::FidoDevice;
+use crate::u2ftypes::{CTAP1RequestAPDU, U2FDevice};
+use nom::{
+ error::VerboseError,
+ number::complete::{be_u32, be_u8},
+ sequence::tuple,
+};
+use serde::{
+ de::{Error as DesError, MapAccess, Visitor},
+ ser::{Error as SerError, SerializeMap},
+ Deserialize, Deserializer, Serialize, Serializer,
+};
+use serde_bytes::ByteBuf;
+use serde_cbor::{de::from_slice, ser, Value};
+use std::convert::TryInto;
+use std::fmt;
+use std::io;
+
+#[derive(Debug, PartialEq)]
+pub enum GetAssertionResult {
+ CTAP1(Vec<u8>),
+ CTAP2(AssertionObject, CollectedClientDataWrapper),
+}
+
+#[derive(Clone, Copy, Debug, Serialize)]
+#[cfg_attr(test, derive(Deserialize))]
+pub struct GetAssertionOptions {
+ #[serde(rename = "uv", skip_serializing_if = "Option::is_none")]
+ pub user_verification: Option<bool>,
+ #[serde(rename = "up", skip_serializing_if = "Option::is_none")]
+ pub user_presence: Option<bool>,
+}
+
+impl Default for GetAssertionOptions {
+ fn default() -> Self {
+ Self {
+ user_presence: Some(true),
+ user_verification: None,
+ }
+ }
+}
+
+impl GetAssertionOptions {
+ pub(crate) fn has_some(&self) -> bool {
+ self.user_presence.is_some() || self.user_verification.is_some()
+ }
+}
+
+impl UserVerification for GetAssertionOptions {
+ fn ask_user_verification(&self) -> bool {
+ if let Some(e) = self.user_verification {
+ e
+ } else {
+ false
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct CalculatedHmacSecretExtension {
+ pub public_key: COSEKey,
+ pub salt_enc: Vec<u8>,
+ pub salt_auth: [u8; 16],
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct HmacSecretExtension {
+ pub salt1: Vec<u8>,
+ pub salt2: Option<Vec<u8>>,
+ calculated_hmac: Option<CalculatedHmacSecretExtension>,
+}
+
+impl HmacSecretExtension {
+ pub fn new(salt1: Vec<u8>, salt2: Option<Vec<u8>>) -> Self {
+ HmacSecretExtension {
+ salt1,
+ salt2,
+ calculated_hmac: None,
+ }
+ }
+
+ pub fn calculate(&mut self, secret: &ECDHSecret) -> Result<(), AuthenticatorError> {
+ if self.salt1.len() < 32 {
+ return Err(CryptoError::WrongSaltLength.into());
+ }
+ let salt_enc = match &self.salt2 {
+ Some(salt2) => {
+ if salt2.len() < 32 {
+ return Err(CryptoError::WrongSaltLength.into());
+ }
+ let salts = [&self.salt1[..32], &salt2[..32]].concat(); // salt1 || salt2
+ encrypt(secret.shared_secret(), &salts)
+ }
+ None => encrypt(secret.shared_secret(), &self.salt1[..32]),
+ }
+ .map_err(|e| CryptoError::Backend(e))?;
+ let salt_auth_full =
+ authenticate(secret.shared_secret(), &salt_enc).map_err(|e| CryptoError::Backend(e))?;
+ let salt_auth = salt_auth_full
+ .windows(16)
+ .next()
+ .ok_or(AuthenticatorError::InternalError(String::from(
+ "salt_auth too short",
+ )))?
+ .try_into()
+ .map_err(|_| {
+ AuthenticatorError::InternalError(String::from(
+ "salt_auth conversion failed. Should never happen.",
+ ))
+ })?;
+ let public_key = secret.my_public_key().clone();
+ self.calculated_hmac = Some(CalculatedHmacSecretExtension {
+ public_key,
+ salt_enc,
+ salt_auth,
+ });
+
+ Ok(())
+ }
+}
+
+impl Serialize for HmacSecretExtension {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ if let Some(calc) = &self.calculated_hmac {
+ let mut map = serializer.serialize_map(Some(3))?;
+ map.serialize_entry(&1, &calc.public_key)?;
+ map.serialize_entry(&2, serde_bytes::Bytes::new(&calc.salt_enc))?;
+ map.serialize_entry(&3, serde_bytes::Bytes::new(&calc.salt_auth))?;
+ map.end()
+ } else {
+ Err(SerError::custom(
+ "hmac secret has not been calculated before being serialized",
+ ))
+ }
+ }
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct GetAssertionExtensions {
+ pub hmac_secret: Option<HmacSecretExtension>,
+}
+
+impl Serialize for GetAssertionExtensions {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let mut map = serializer.serialize_map(Some(1))?;
+ map.serialize_entry(&"hmac-secret", &self.hmac_secret)?;
+ map.end()
+ }
+}
+
+impl GetAssertionExtensions {
+ fn has_extensions(&self) -> bool {
+ self.hmac_secret.is_some()
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct GetAssertion {
+ pub(crate) client_data_wrapper: CollectedClientDataWrapper,
+ pub(crate) rp: RelyingPartyWrapper,
+ pub(crate) allow_list: Vec<PublicKeyCredentialDescriptor>,
+
+ // https://www.w3.org/TR/webauthn/#client-extension-input
+ // The client extension input, which is a value that can be encoded in JSON,
+ // is passed from the WebAuthn Relying Party to the client in the get() or
+ // create() call, while the CBOR authenticator extension input is passed
+ // from the client to the authenticator for authenticator extensions during
+ // the processing of these calls.
+ pub(crate) extensions: GetAssertionExtensions,
+ pub(crate) options: GetAssertionOptions,
+ pub(crate) pin: Option<Pin>,
+ pub(crate) pin_auth: Option<PinAuth>,
+ //TODO(MS): pinProtocol
+}
+
+impl GetAssertion {
+ pub fn new(
+ client_data_wrapper: CollectedClientData,
+ rp: RelyingPartyWrapper,
+ allow_list: Vec<PublicKeyCredentialDescriptor>,
+ options: GetAssertionOptions,
+ extensions: GetAssertionExtensions,
+ pin: Option<Pin>,
+ ) -> Result<Self, HIDError> {
+ let client_data_wrapper = CollectedClientDataWrapper::new(client_data_wrapper)?;
+ Ok(Self {
+ client_data_wrapper,
+ rp,
+ allow_list,
+ extensions,
+ options,
+ pin,
+ pin_auth: None,
+ })
+ }
+}
+
+impl PinAuthCommand for GetAssertion {
+ fn pin(&self) -> &Option<Pin> {
+ &self.pin
+ }
+
+ fn set_pin(&mut self, pin: Option<Pin>) {
+ self.pin = pin;
+ }
+
+ fn pin_auth(&self) -> &Option<PinAuth> {
+ &self.pin_auth
+ }
+
+ fn set_pin_auth(&mut self, pin_auth: Option<PinAuth>, _pint_auth_protocol: Option<u64>) {
+ self.pin_auth = pin_auth;
+ }
+
+ fn client_data_hash(&self) -> ClientDataHash {
+ self.client_data_wrapper.hash()
+ }
+
+ fn unset_uv_option(&mut self) {
+ self.options.user_verification = None;
+ }
+}
+
+impl Serialize for GetAssertion {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ // Need to define how many elements are going to be in the map
+ // beforehand
+ let mut map_len = 2;
+ if !self.allow_list.is_empty() {
+ map_len += 1;
+ }
+ if self.extensions.has_extensions() {
+ map_len += 1;
+ }
+ if self.options.has_some() {
+ map_len += 1;
+ }
+ if self.pin_auth.is_some() {
+ map_len += 2;
+ }
+
+ let mut map = serializer.serialize_map(Some(map_len))?;
+ match self.rp {
+ RelyingPartyWrapper::Data(ref d) => {
+ map.serialize_entry(&1, &d.id)?;
+ }
+ _ => {
+ return Err(S::Error::custom(
+ "Can't serialize a RelyingParty::Hash for CTAP2",
+ ));
+ }
+ }
+
+ let client_data_hash = self.client_data_hash();
+ map.serialize_entry(&2, &client_data_hash)?;
+ if !self.allow_list.is_empty() {
+ map.serialize_entry(&3, &self.allow_list)?;
+ }
+ if self.extensions.has_extensions() {
+ map.serialize_entry(&4, &self.extensions)?;
+ }
+ if self.options.has_some() {
+ map.serialize_entry(&5, &self.options)?;
+ }
+ if let Some(pin_auth) = &self.pin_auth {
+ map.serialize_entry(&6, &pin_auth)?;
+ map.serialize_entry(&7, &1)?;
+ }
+ map.end()
+ }
+}
+
+impl Request<GetAssertionResult> for GetAssertion {
+ fn is_ctap2_request(&self) -> bool {
+ match self.rp {
+ RelyingPartyWrapper::Data(_) => true,
+ RelyingPartyWrapper::Hash(_) => false,
+ }
+ }
+}
+
+/// This command is used to check which key_handle is valid for this
+/// token. This is sent before a GetAssertion command, to determine which
+/// is valid for a specific token and which key_handle GetAssertion
+/// should send to the token. Or before a MakeCredential command, to determine
+/// if this token is already registered or not.
+#[derive(Debug)]
+pub(crate) struct CheckKeyHandle<'assertion> {
+ pub(crate) key_handle: &'assertion [u8],
+ pub(crate) client_data_wrapper: &'assertion CollectedClientDataWrapper,
+ pub(crate) rp: &'assertion RelyingPartyWrapper,
+}
+
+impl<'assertion> RequestCtap1 for CheckKeyHandle<'assertion> {
+ type Output = ();
+
+ fn ctap1_format<Dev>(&self, _dev: &mut Dev) -> Result<Vec<u8>, HIDError>
+ where
+ Dev: U2FDevice + io::Read + io::Write + fmt::Debug,
+ {
+ let flags = U2F_CHECK_IS_REGISTERED;
+ // TODO(MS): Need to check "up" here. If up==false, set to 0x08? Or not? Spec is
+ // ambiguous
+ let mut auth_data = Vec::with_capacity(2 * PARAMETER_SIZE + 1 + self.key_handle.len());
+
+ auth_data.extend_from_slice(self.client_data_wrapper.hash().as_ref());
+ auth_data.extend_from_slice(self.rp.hash().as_ref());
+ auth_data.extend_from_slice(&[self.key_handle.len() as u8]);
+ auth_data.extend_from_slice(self.key_handle);
+ let cmd = U2F_AUTHENTICATE;
+ let apdu = CTAP1RequestAPDU::serialize(cmd, flags, &auth_data)?;
+ Ok(apdu)
+ }
+
+ fn handle_response_ctap1(
+ &self,
+ status: Result<(), ApduErrorStatus>,
+ _input: &[u8],
+ ) -> Result<Self::Output, Retryable<HIDError>> {
+ // From the U2F-spec: https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#registration-request-message---u2f_register
+ // if the control byte is set to 0x07 by the FIDO Client, the U2F token is supposed to
+ // simply check whether the provided key handle was originally created by this token,
+ // and whether it was created for the provided application parameter. If so, the U2F
+ // token MUST respond with an authentication response message:error:test-of-user-presence-required
+ // (note that despite the name this signals a success condition).
+ // If the key handle was not created by this U2F token, or if it was created for a different
+ // application parameter, the token MUST respond with an authentication response message:error:bad-key-handle.
+ match status {
+ Ok(_) | Err(ApduErrorStatus::ConditionsNotSatisfied) => Ok(()),
+ Err(e) => Err(Retryable::Error(HIDError::ApduStatus(e))),
+ }
+ }
+}
+
+impl RequestCtap1 for GetAssertion {
+ type Output = GetAssertionResult;
+
+ fn ctap1_format<Dev>(&self, dev: &mut Dev) -> Result<Vec<u8>, HIDError>
+ where
+ Dev: io::Read + io::Write + fmt::Debug + FidoDevice,
+ {
+ let key_handle = self
+ .allow_list
+ .iter()
+ // key-handles in CTAP1 are limited to 255 bytes, but are not limited in CTAP2.
+ // Filter out key-handles that are too long (can happen if this is a CTAP2-request,
+ // but the token only speaks CTAP1). If none is found, return an error.
+ .filter(|allowed_handle| allowed_handle.id.len() < 256)
+ .find_map(|allowed_handle| {
+ let check_command = CheckKeyHandle {
+ key_handle: allowed_handle.id.as_ref(),
+ client_data_wrapper: &self.client_data_wrapper,
+ rp: &self.rp,
+ };
+ let res = dev.send_ctap1(&check_command);
+ match res {
+ Ok(_) => Some(allowed_handle.id.clone()),
+ _ => None,
+ }
+ })
+ .ok_or(HIDError::DeviceNotSupported)?;
+
+ debug!("sending key_handle = {:?}", key_handle);
+
+ let flags = if self.options.user_presence.unwrap_or(false) {
+ U2F_REQUEST_USER_PRESENCE
+ } else {
+ 0
+ };
+ let mut auth_data =
+ Vec::with_capacity(2 * PARAMETER_SIZE + 1 /* key_handle_len */ + key_handle.len());
+
+ if self.is_ctap2_request() {
+ auth_data.extend_from_slice(self.client_data_hash().as_ref());
+ } else {
+ let decoded = base64::decode_config(
+ &self.client_data_wrapper.client_data.challenge.0,
+ base64::URL_SAFE_NO_PAD,
+ )
+ .map_err(|_| HIDError::DeviceError)?; // We encoded it, so this should never fail
+ auth_data.extend_from_slice(&decoded);
+ }
+ auth_data.extend_from_slice(self.rp.hash().as_ref());
+ auth_data.extend_from_slice(&[key_handle.len() as u8]);
+ auth_data.extend_from_slice(key_handle.as_ref());
+
+ let cmd = U2F_AUTHENTICATE;
+ let apdu = CTAP1RequestAPDU::serialize(cmd, flags, &auth_data)?;
+ Ok(apdu)
+ }
+
+ fn handle_response_ctap1(
+ &self,
+ status: Result<(), ApduErrorStatus>,
+ input: &[u8],
+ ) -> Result<Self::Output, Retryable<HIDError>> {
+ if Err(ApduErrorStatus::ConditionsNotSatisfied) == status {
+ return Err(Retryable::Retry);
+ }
+ if let Err(err) = status {
+ return Err(Retryable::Error(HIDError::ApduStatus(err)));
+ }
+
+ if self.is_ctap2_request() {
+ let parse_authentication = |input| {
+ // Parsing an u8, then a u32, and the rest is the signature
+ let (rest, (user_presence, counter)) = tuple((be_u8, be_u32))(input)?;
+ let signature = Vec::from(rest);
+ Ok((user_presence, counter, signature))
+ };
+ let (user_presence, counter, signature) = parse_authentication(input)
+ .map_err(|e: nom::Err<VerboseError<_>>| {
+ error!("error while parsing authentication: {:?}", e);
+ CommandError::Deserializing(DesError::custom("unable to parse authentication"))
+ })
+ .map_err(HIDError::Command)
+ .map_err(Retryable::Error)?;
+
+ let mut flags = AuthenticatorDataFlags::empty();
+ if user_presence == 1 {
+ flags |= AuthenticatorDataFlags::USER_PRESENT;
+ }
+ let auth_data = AuthenticatorData {
+ rp_id_hash: self.rp.hash(),
+ flags,
+ counter,
+ credential_data: None,
+ extensions: Default::default(),
+ };
+ let assertion = Assertion {
+ credentials: None,
+ signature,
+ user: None,
+ auth_data,
+ };
+
+ Ok(GetAssertionResult::CTAP2(
+ AssertionObject(vec![assertion]),
+ self.client_data_wrapper.clone(),
+ ))
+ } else {
+ Ok(GetAssertionResult::CTAP1(input.to_vec()))
+ }
+ }
+}
+
+impl RequestCtap2 for GetAssertion {
+ type Output = GetAssertionResult;
+
+ fn command() -> Command {
+ Command::GetAssertion
+ }
+
+ fn wire_format<Dev>(&self, _dev: &mut Dev) -> Result<Vec<u8>, HIDError>
+ where
+ Dev: FidoDevice + io::Read + io::Write + fmt::Debug,
+ {
+ Ok(ser::to_vec(&self).map_err(CommandError::Serializing)?)
+ }
+
+ fn handle_response_ctap2<Dev>(
+ &self,
+ dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError>
+ where
+ Dev: FidoDevice + io::Read + io::Write + fmt::Debug,
+ {
+ if input.is_empty() {
+ return Err(CommandError::InputTooSmall.into());
+ }
+
+ let status: StatusCode = input[0].into();
+ debug!(
+ "response status code: {:?}, rest: {:?}",
+ status,
+ &input[1..]
+ );
+ if input.len() > 1 {
+ if status.is_ok() {
+ let assertion: GetAssertionResponse =
+ from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ let number_of_credentials = assertion.number_of_credentials.unwrap_or(1);
+ let mut assertions = Vec::with_capacity(number_of_credentials);
+ assertions.push(assertion.into());
+
+ let msg = GetNextAssertion;
+ // We already have one, so skipping 0
+ for _ in 1..number_of_credentials {
+ let new_cred = dev.send_cbor(&msg)?;
+ assertions.push(new_cred.into());
+ }
+
+ Ok(GetAssertionResult::CTAP2(
+ AssertionObject(assertions),
+ self.client_data_wrapper.clone(),
+ ))
+ } else {
+ let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ Err(CommandError::StatusCode(status, Some(data)).into())
+ }
+ } else if status.is_ok() {
+ Err(CommandError::InputTooSmall.into())
+ } else {
+ Err(CommandError::StatusCode(status, None).into())
+ }
+ }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct Assertion {
+ pub credentials: Option<PublicKeyCredentialDescriptor>, /* Was optional in CTAP2.0, is
+ * mandatory in CTAP2.1 */
+ pub auth_data: AuthenticatorData,
+ pub signature: Vec<u8>,
+ pub user: Option<User>,
+}
+
+impl From<GetAssertionResponse> for Assertion {
+ fn from(r: GetAssertionResponse) -> Self {
+ Assertion {
+ credentials: r.credentials,
+ auth_data: r.auth_data,
+ signature: r.signature,
+ user: r.user,
+ }
+ }
+}
+
+// TODO(baloo): Move this to src/ctap2/mod.rs?
+#[derive(Debug, PartialEq)]
+pub struct AssertionObject(pub Vec<Assertion>);
+
+impl AssertionObject {
+ pub fn u2f_sign_data(&self) -> Vec<u8> {
+ if let Some(first) = self.0.first() {
+ let mut res = Vec::new();
+ res.push(first.auth_data.flags.bits());
+ res.extend(&first.auth_data.counter.to_be_bytes());
+ res.extend(&first.signature);
+ res
+ // first.signature.clone()
+ } else {
+ Vec::new()
+ }
+ }
+}
+
+pub(crate) struct GetAssertionResponse {
+ credentials: Option<PublicKeyCredentialDescriptor>,
+ auth_data: AuthenticatorData,
+ signature: Vec<u8>,
+ user: Option<User>,
+ number_of_credentials: Option<usize>,
+}
+
+impl<'de> Deserialize<'de> for GetAssertionResponse {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct GetAssertionResponseVisitor;
+
+ impl<'de> Visitor<'de> for GetAssertionResponseVisitor {
+ type Value = GetAssertionResponse;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a byte array")
+ }
+
+ fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ where
+ M: MapAccess<'de>,
+ {
+ let mut credentials = None;
+ let mut auth_data = None;
+ let mut signature = None;
+ let mut user = None;
+ let mut number_of_credentials = None;
+
+ while let Some(key) = map.next_key()? {
+ match key {
+ 1 => {
+ if credentials.is_some() {
+ return Err(M::Error::duplicate_field("credentials"));
+ }
+ credentials = Some(map.next_value()?);
+ }
+ 2 => {
+ if auth_data.is_some() {
+ return Err(M::Error::duplicate_field("auth_data"));
+ }
+ auth_data = Some(map.next_value()?);
+ }
+ 3 => {
+ if signature.is_some() {
+ return Err(M::Error::duplicate_field("signature"));
+ }
+ let signature_bytes: ByteBuf = map.next_value()?;
+ let signature_bytes: Vec<u8> = signature_bytes.into_vec();
+ signature = Some(signature_bytes);
+ }
+ 4 => {
+ if user.is_some() {
+ return Err(M::Error::duplicate_field("user"));
+ }
+ user = map.next_value()?;
+ }
+ 5 => {
+ if number_of_credentials.is_some() {
+ return Err(M::Error::duplicate_field("number_of_credentials"));
+ }
+ number_of_credentials = Some(map.next_value()?);
+ }
+ k => return Err(M::Error::custom(format!("unexpected key: {:?}", k))),
+ }
+ }
+
+ let auth_data = auth_data.ok_or_else(|| M::Error::missing_field("auth_data"))?;
+ let signature = signature.ok_or_else(|| M::Error::missing_field("signature"))?;
+
+ Ok(GetAssertionResponse {
+ credentials,
+ auth_data,
+ signature,
+ user,
+ number_of_credentials,
+ })
+ }
+ }
+
+ deserializer.deserialize_bytes(GetAssertionResponseVisitor)
+ }
+}
+
+#[cfg(test)]
+pub mod test {
+ use super::{Assertion, GetAssertion, GetAssertionOptions, GetAssertionResult, HIDError};
+ use crate::consts::{
+ HIDCmd, SW_CONDITIONS_NOT_SATISFIED, SW_NO_ERROR, U2F_CHECK_IS_REGISTERED,
+ U2F_REQUEST_USER_PRESENCE,
+ };
+ use crate::ctap2::attestation::{AuthenticatorData, AuthenticatorDataFlags};
+ use crate::ctap2::client_data::{
+ Challenge, CollectedClientData, CollectedClientDataWrapper, TokenBinding, WebauthnType,
+ };
+ use crate::ctap2::commands::get_assertion::AssertionObject;
+ use crate::ctap2::commands::RequestCtap1;
+ use crate::ctap2::server::{
+ PublicKeyCredentialDescriptor, RelyingParty, RelyingPartyWrapper, RpIdHash, Transport, User,
+ };
+ use crate::transport::device_selector::Device;
+ use crate::transport::hid::HIDDevice;
+ use crate::transport::FidoDevice;
+ use crate::u2ftypes::U2FDevice;
+ use rand::{thread_rng, RngCore};
+
+ #[test]
+ fn test_get_assertion_ctap2() {
+ let client_data = CollectedClientData {
+ webauthn_type: WebauthnType::Create,
+ challenge: Challenge::from(vec![0x00, 0x01, 0x02, 0x03]),
+ origin: String::from("example.com"),
+ cross_origin: false,
+ token_binding: Some(TokenBinding::Present(String::from("AAECAw"))),
+ };
+ let assertion = GetAssertion::new(
+ client_data.clone(),
+ RelyingPartyWrapper::Data(RelyingParty {
+ id: String::from("example.com"),
+ name: Some(String::from("Acme")),
+ icon: None,
+ }),
+ vec![PublicKeyCredentialDescriptor {
+ id: vec![
+ 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35,
+ 0xEF, 0xAA, 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D,
+ 0xA4, 0x85, 0x34, 0xC8, 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5,
+ 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD, 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5,
+ 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08, 0xFE, 0x42, 0x00, 0x38,
+ ],
+ transports: vec![Transport::USB],
+ }],
+ GetAssertionOptions {
+ user_presence: Some(true),
+ user_verification: None,
+ },
+ Default::default(),
+ None,
+ )
+ .expect("Failed to create GetAssertion");
+ let mut device = Device::new("commands/get_assertion").unwrap();
+ let mut cid = [0u8; 4];
+ thread_rng().fill_bytes(&mut cid);
+ device.set_cid(cid);
+
+ let mut msg = cid.to_vec();
+ msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x90]);
+ msg.extend(vec![0x2]); // u2f command
+ msg.extend(vec![
+ 0xa4, // map(4)
+ 0x1, // rpid
+ 0x6b, // text(11)
+ 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, // example.com
+ 0x2, // clientDataHash
+ 0x58, 0x20, //bytes(32)
+ 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, 0x32,
+ 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, 0x10, 0x87,
+ 0x54, 0xc3, 0x2d, 0x80, // hash
+ 0x3, //allowList
+ 0x81, // array(1)
+ 0xa2, // map(2)
+ 0x64, // text(4),
+ 0x74, 0x79, 0x70, // typ
+ ]);
+ device.add_write(&msg, 0);
+
+ msg = cid.to_vec();
+ msg.extend(&[0x0]); //SEQ
+ msg.extend(vec![
+ 0x65, // e (continuation of type)
+ 0x6a, // text(10)
+ 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // public-key
+ 0x62, // text(2)
+ 0x69, 0x64, // id
+ 0x58, 0x40, // bytes(64)
+ ]);
+ msg.extend(&assertion.allow_list[0].id[..42]);
+ device.add_write(&msg, 0);
+
+ msg = cid.to_vec();
+ msg.extend(&[0x1]); //SEQ
+ msg.extend(&assertion.allow_list[0].id[42..]);
+ msg.extend(vec![
+ 0x5, // options
+ 0xa1, // map(1)
+ 0x62, // text(2)
+ 0x75, 0x70, // up
+ 0xf5, // true
+ ]);
+ device.add_write(&msg, 0);
+
+ // fido response
+ let mut msg = cid.to_vec();
+ msg.extend(&[HIDCmd::Cbor.into(), 0x1, 0x5c]); // cmd + bcnt
+ msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[..57]);
+ device.add_read(&msg, 0);
+
+ let mut msg = cid.to_vec();
+ msg.extend(&[0x0]); // SEQ
+ msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[57..116]);
+ device.add_read(&msg, 0);
+
+ let mut msg = cid.to_vec();
+ msg.extend(&[0x1]); // SEQ
+ msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[116..175]);
+ device.add_read(&msg, 0);
+
+ let mut msg = cid.to_vec();
+ msg.extend(&[0x2]); // SEQ
+ msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[175..234]);
+ device.add_read(&msg, 0);
+
+ let mut msg = cid.to_vec();
+ msg.extend(&[0x3]); // SEQ
+ msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[234..293]);
+ device.add_read(&msg, 0);
+ let mut msg = cid.to_vec();
+ msg.extend(&[0x4]); // SEQ
+ msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[293..]);
+ device.add_read(&msg, 0);
+
+ // Check if response is correct
+ let expected_auth_data = AuthenticatorData {
+ rp_id_hash: RpIdHash([
+ 0x62, 0x5d, 0xda, 0xdf, 0x74, 0x3f, 0x57, 0x27, 0xe6, 0x6b, 0xba, 0x8c, 0x2e, 0x38,
+ 0x79, 0x22, 0xd1, 0xaf, 0x43, 0xc5, 0x03, 0xd9, 0x11, 0x4a, 0x8f, 0xba, 0x10, 0x4d,
+ 0x84, 0xd0, 0x2b, 0xfa,
+ ]),
+ flags: AuthenticatorDataFlags::USER_PRESENT,
+ counter: 0x11,
+ credential_data: None,
+ extensions: Default::default(),
+ };
+
+ let expected_assertion = Assertion {
+ credentials: Some(PublicKeyCredentialDescriptor {
+ id: vec![
+ 242, 32, 6, 222, 79, 144, 90, 246, 138, 67, 148, 47, 2, 79, 42, 94, 206, 96,
+ 61, 156, 109, 75, 61, 248, 190, 8, 237, 1, 252, 68, 38, 70, 208, 52, 133, 138,
+ 199, 91, 237, 63, 213, 128, 191, 152, 8, 217, 79, 203, 238, 130, 185, 178, 239,
+ 102, 119, 175, 10, 220, 195, 88, 82, 234, 107, 158,
+ ],
+ transports: vec![],
+ }),
+ signature: vec![
+ 0x30, 0x45, 0x02, 0x20, 0x4a, 0x5a, 0x9d, 0xd3, 0x92, 0x98, 0x14, 0x9d, 0x90, 0x47,
+ 0x69, 0xb5, 0x1a, 0x45, 0x14, 0x33, 0x00, 0x6f, 0x18, 0x2a, 0x34, 0xfb, 0xdf, 0x66,
+ 0xde, 0x5f, 0xc7, 0x17, 0xd7, 0x5f, 0xb3, 0x50, 0x02, 0x21, 0x00, 0xa4, 0x6b, 0x8e,
+ 0xa3, 0xc3, 0xb9, 0x33, 0x82, 0x1c, 0x6e, 0x7f, 0x5e, 0xf9, 0xda, 0xae, 0x94, 0xab,
+ 0x47, 0xf1, 0x8d, 0xb4, 0x74, 0xc7, 0x47, 0x90, 0xea, 0xab, 0xb1, 0x44, 0x11, 0xe7,
+ 0xa0,
+ ],
+ user: Some(User {
+ id: vec![
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02,
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02,
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82,
+ ],
+ icon: Some("https://pics.example.com/00/p/aBjjjpqPb.png".to_string()),
+ name: Some("johnpsmith@example.com".to_string()),
+ display_name: Some("John P. Smith".to_string()),
+ }),
+ auth_data: expected_auth_data,
+ };
+
+ let expected = GetAssertionResult::CTAP2(
+ AssertionObject(vec![expected_assertion]),
+ CollectedClientDataWrapper {
+ client_data,
+ serialized_data: CLIENT_DATA_VEC.to_vec(),
+ },
+ );
+ let response = device.send_cbor(&assertion).unwrap();
+ assert_eq!(response, expected);
+ }
+
+ fn fill_device_ctap1(device: &mut Device, cid: [u8; 4], flags: u8, answer_status: [u8; 2]) {
+ // ctap2 request
+ let mut msg = cid.to_vec();
+ msg.extend(&[HIDCmd::Msg.into(), 0x00, 0x8A]); // cmd + bcnt
+ msg.extend(&[0x00, 0x2]); // U2F_AUTHENTICATE
+ msg.extend(&[flags]);
+ msg.extend(&[0x00, 0x00, 0x00]);
+ msg.extend(&[0x81]); // Data len - 7
+ msg.extend(&CLIENT_DATA_HASH);
+ msg.extend(&RELYING_PARTY_HASH[..18]);
+ device.add_write(&msg, 0);
+
+ // Continuation package
+ let mut msg = cid.to_vec();
+ msg.extend(vec![0x00]); // SEQ
+ msg.extend(&RELYING_PARTY_HASH[18..]);
+ msg.extend(&[KEY_HANDLE.len() as u8]);
+ msg.extend(&KEY_HANDLE[..44]);
+ device.add_write(&msg, 0);
+
+ let mut msg = cid.to_vec();
+ msg.extend(vec![0x01]); // SEQ
+ msg.extend(&KEY_HANDLE[44..]);
+ device.add_write(&msg, 0);
+
+ // fido response
+ let mut msg = cid.to_vec();
+ msg.extend(&[HIDCmd::Msg.into(), 0x0, 0x4D]); // cmd + bcnt
+ msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP1[0..57]);
+ device.add_read(&msg, 0);
+
+ let mut msg = cid.to_vec();
+ msg.extend(&[0x0]); // SEQ
+ msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP1[57..]);
+ msg.extend(&answer_status);
+ device.add_read(&msg, 0);
+ }
+
+ #[test]
+ fn test_get_assertion_ctap1() {
+ let client_data = CollectedClientData {
+ webauthn_type: WebauthnType::Create,
+ challenge: Challenge::from(vec![0x00, 0x01, 0x02, 0x03]),
+ origin: String::from("example.com"),
+ cross_origin: false,
+ token_binding: Some(TokenBinding::Present(String::from("AAECAw"))),
+ };
+ let assertion = GetAssertion::new(
+ client_data.clone(),
+ RelyingPartyWrapper::Data(RelyingParty {
+ id: String::from("example.com"),
+ name: Some(String::from("Acme")),
+ icon: None,
+ }),
+ vec![PublicKeyCredentialDescriptor {
+ id: vec![
+ 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35,
+ 0xEF, 0xAA, 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D,
+ 0xA4, 0x85, 0x34, 0xC8, 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5,
+ 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD, 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5,
+ 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08, 0xFE, 0x42, 0x00, 0x38,
+ ],
+ transports: vec![Transport::USB],
+ }],
+ GetAssertionOptions {
+ user_presence: Some(true),
+ user_verification: None,
+ },
+ Default::default(),
+ None,
+ )
+ .expect("Failed to create GetAssertion");
+ let mut device = Device::new("commands/get_assertion").unwrap(); // not really used (all functions ignore it)
+ // channel id
+ let mut cid = [0u8; 4];
+ thread_rng().fill_bytes(&mut cid);
+
+ device.set_cid(cid);
+
+ // ctap1 request
+ fill_device_ctap1(
+ &mut device,
+ cid,
+ U2F_CHECK_IS_REGISTERED,
+ SW_CONDITIONS_NOT_SATISFIED,
+ );
+ let ctap1_request = assertion.ctap1_format(&mut device).unwrap();
+ // Check if the request is going to be correct
+ assert_eq!(ctap1_request, GET_ASSERTION_SAMPLE_REQUEST_CTAP1);
+
+ // Now do it again, but parse the actual response
+ fill_device_ctap1(
+ &mut device,
+ cid,
+ U2F_CHECK_IS_REGISTERED,
+ SW_CONDITIONS_NOT_SATISFIED,
+ );
+ fill_device_ctap1(&mut device, cid, U2F_REQUEST_USER_PRESENCE, SW_NO_ERROR);
+
+ let response = device.send_ctap1(&assertion).unwrap();
+
+ // Check if response is correct
+ let expected_auth_data = AuthenticatorData {
+ rp_id_hash: RpIdHash(RELYING_PARTY_HASH),
+ flags: AuthenticatorDataFlags::USER_PRESENT,
+ counter: 0x3B,
+ credential_data: None,
+ extensions: Default::default(),
+ };
+
+ let expected_assertion = Assertion {
+ credentials: None,
+ signature: vec![
+ 0x30, 0x44, 0x02, 0x20, 0x7B, 0xDE, 0x0A, 0x52, 0xAC, 0x1F, 0x4C, 0x8B, 0x27, 0xE0,
+ 0x03, 0xA3, 0x70, 0xCD, 0x66, 0xA4, 0xC7, 0x11, 0x8D, 0xD2, 0x2D, 0x54, 0x47, 0x83,
+ 0x5F, 0x45, 0xB9, 0x9C, 0x68, 0x42, 0x3F, 0xF7, 0x02, 0x20, 0x3C, 0x51, 0x7B, 0x47,
+ 0x87, 0x7F, 0x85, 0x78, 0x2D, 0xE1, 0x00, 0x86, 0xA7, 0x83, 0xD1, 0xE7, 0xDF, 0x4E,
+ 0x36, 0x39, 0xE7, 0x71, 0xF5, 0xF6, 0xAF, 0xA3, 0x5A, 0xAD, 0x53, 0x73, 0x85, 0x8E,
+ ],
+ user: None,
+ auth_data: expected_auth_data,
+ };
+
+ let expected = GetAssertionResult::CTAP2(
+ AssertionObject(vec![expected_assertion]),
+ CollectedClientDataWrapper {
+ client_data,
+ serialized_data: CLIENT_DATA_VEC.to_vec(),
+ },
+ );
+
+ assert_eq!(response, expected);
+ }
+
+ #[test]
+ fn test_get_assertion_ctap1_long_keys() {
+ let client_data = CollectedClientData {
+ webauthn_type: WebauthnType::Create,
+ challenge: Challenge::from(vec![0x00, 0x01, 0x02, 0x03]),
+ origin: String::from("example.com"),
+ cross_origin: false,
+ token_binding: Some(TokenBinding::Present(String::from("AAECAw"))),
+ };
+
+ let too_long_key_handle = PublicKeyCredentialDescriptor {
+ id: vec![0; 1000],
+ transports: vec![Transport::USB],
+ };
+ let mut assertion = GetAssertion::new(
+ client_data.clone(),
+ RelyingPartyWrapper::Data(RelyingParty {
+ id: String::from("example.com"),
+ name: Some(String::from("Acme")),
+ icon: None,
+ }),
+ vec![too_long_key_handle.clone()],
+ GetAssertionOptions {
+ user_presence: Some(true),
+ user_verification: None,
+ },
+ Default::default(),
+ None,
+ )
+ .expect("Failed to create GetAssertion");
+
+ let mut device = Device::new("commands/get_assertion").unwrap(); // not really used (all functions ignore it)
+ // channel id
+ let mut cid = [0u8; 4];
+ thread_rng().fill_bytes(&mut cid);
+
+ device.set_cid(cid);
+
+ assert_matches!(
+ assertion.ctap1_format(&mut device),
+ Err(HIDError::DeviceNotSupported)
+ );
+
+ assertion.allow_list = vec![too_long_key_handle.clone(); 5];
+
+ assert_matches!(
+ assertion.ctap1_format(&mut device),
+ Err(HIDError::DeviceNotSupported)
+ );
+ let ok_key_handle = PublicKeyCredentialDescriptor {
+ id: vec![
+ 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35, 0xEF,
+ 0xAA, 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D, 0xA4, 0x85,
+ 0x34, 0xC8, 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5, 0xCC, 0x4E, 0x78,
+ 0x05, 0x5B, 0xDD, 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC,
+ 0xD4, 0x15, 0xCD, 0x08, 0xFE, 0x42, 0x00, 0x38,
+ ],
+ transports: vec![Transport::USB],
+ };
+ assertion.allow_list = vec![
+ too_long_key_handle.clone(),
+ too_long_key_handle.clone(),
+ too_long_key_handle.clone(),
+ ok_key_handle,
+ too_long_key_handle.clone(),
+ ];
+
+ // ctap1 request
+ fill_device_ctap1(
+ &mut device,
+ cid,
+ U2F_CHECK_IS_REGISTERED,
+ SW_CONDITIONS_NOT_SATISFIED,
+ );
+ let ctap1_request = assertion.ctap1_format(&mut device).unwrap();
+ // Check if the request is going to be correct
+ assert_eq!(ctap1_request, GET_ASSERTION_SAMPLE_REQUEST_CTAP1);
+
+ // Now do it again, but parse the actual response
+ fill_device_ctap1(
+ &mut device,
+ cid,
+ U2F_CHECK_IS_REGISTERED,
+ SW_CONDITIONS_NOT_SATISFIED,
+ );
+ fill_device_ctap1(&mut device, cid, U2F_REQUEST_USER_PRESENCE, SW_NO_ERROR);
+
+ let response = device.send_ctap1(&assertion).unwrap();
+
+ // Check if response is correct
+ let expected_auth_data = AuthenticatorData {
+ rp_id_hash: RpIdHash(RELYING_PARTY_HASH),
+ flags: AuthenticatorDataFlags::USER_PRESENT,
+ counter: 0x3B,
+ credential_data: None,
+ extensions: Default::default(),
+ };
+
+ let expected_assertion = Assertion {
+ credentials: None,
+ signature: vec![
+ 0x30, 0x44, 0x02, 0x20, 0x7B, 0xDE, 0x0A, 0x52, 0xAC, 0x1F, 0x4C, 0x8B, 0x27, 0xE0,
+ 0x03, 0xA3, 0x70, 0xCD, 0x66, 0xA4, 0xC7, 0x11, 0x8D, 0xD2, 0x2D, 0x54, 0x47, 0x83,
+ 0x5F, 0x45, 0xB9, 0x9C, 0x68, 0x42, 0x3F, 0xF7, 0x02, 0x20, 0x3C, 0x51, 0x7B, 0x47,
+ 0x87, 0x7F, 0x85, 0x78, 0x2D, 0xE1, 0x00, 0x86, 0xA7, 0x83, 0xD1, 0xE7, 0xDF, 0x4E,
+ 0x36, 0x39, 0xE7, 0x71, 0xF5, 0xF6, 0xAF, 0xA3, 0x5A, 0xAD, 0x53, 0x73, 0x85, 0x8E,
+ ],
+ user: None,
+ auth_data: expected_auth_data,
+ };
+
+ let expected = GetAssertionResult::CTAP2(
+ AssertionObject(vec![expected_assertion]),
+ CollectedClientDataWrapper {
+ client_data,
+ serialized_data: CLIENT_DATA_VEC.to_vec(),
+ },
+ );
+
+ assert_eq!(response, expected);
+ }
+
+ // Manually assembled according to https://www.w3.org/TR/webauthn-2/#clientdatajson-serialization
+ const CLIENT_DATA_VEC: [u8; 140] = [
+ 0x7b, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, // {"type":
+ 0x22, 0x77, 0x65, 0x62, 0x61, 0x75, 0x74, 0x68, 0x6e, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74,
+ 0x65, 0x22, // "webauthn.create"
+ 0x2c, 0x22, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x22,
+ 0x3a, // (,"challenge":
+ 0x22, 0x41, 0x41, 0x45, 0x43, 0x41, 0x77, 0x22, // challenge in base64
+ 0x2c, 0x22, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x22, 0x3a, // ,"origin":
+ 0x22, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d,
+ 0x22, // "example.com"
+ 0x2c, 0x22, 0x63, 0x72, 0x6f, 0x73, 0x73, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x22,
+ 0x3a, // ,"crossOrigin":
+ 0x66, 0x61, 0x6c, 0x73, 0x65, // false
+ 0x2c, 0x22, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x22,
+ 0x3a, // ,"tokenBinding":
+ 0x7b, 0x22, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x3a, // {"status":
+ 0x22, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x22, // "present"
+ 0x2c, 0x22, 0x69, 0x64, 0x22, 0x3a, // ,"id":
+ 0x22, 0x41, 0x41, 0x45, 0x43, 0x41, 0x77, 0x22, // "AAECAw"
+ 0x7d, // }
+ 0x7d, // }
+ ];
+
+ const CLIENT_DATA_HASH: [u8; 32] = [
+ 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, // hash
+ 0x32, 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, // hash
+ 0x10, 0x87, 0x54, 0xc3, 0x2d, 0x80, // hash
+ ];
+
+ const RELYING_PARTY_HASH: [u8; 32] = [
+ 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, 0x34, 0xE2,
+ 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, 0x12, 0x55, 0x86, 0xCE,
+ 0x19, 0x47,
+ ];
+ const KEY_HANDLE: [u8; 64] = [
+ 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35, 0xEF, 0xAA,
+ 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D, 0xA4, 0x85, 0x34, 0xC8,
+ 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5, 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD,
+ 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08,
+ 0xFE, 0x42, 0x00, 0x38,
+ ];
+
+ const GET_ASSERTION_SAMPLE_REQUEST_CTAP1: [u8; 138] = [
+ // CBOR Header
+ 0x0, // CLA
+ 0x2, // INS U2F_Authenticate
+ 0x3, // P1 Flags (user presence)
+ 0x0, // P2
+ 0x0, 0x0, 0x81, // Lc
+ // NOTE: This has been taken from CTAP2.0 spec, but the clientDataHash has been replaced
+ // to be able to operate with known values for CollectedClientData (spec doesn't say
+ // what values led to the provided example hash)
+ // clientDataHash:
+ 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, // hash
+ 0x32, 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, // hash
+ 0x10, 0x87, 0x54, 0xc3, 0x2d, 0x80, // hash
+ // rpIdHash:
+ 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, 0x34, 0xE2,
+ 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, 0x12, 0x55, 0x86, 0xCE,
+ 0x19, 0x47, // ..
+ // Key Handle Length (1 Byte):
+ 0x40, // ..
+ // Key Handle (Key Handle Length Bytes):
+ 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35, 0xEF, 0xAA,
+ 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D, 0xA4, 0x85, 0x34, 0xC8,
+ 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5, 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD,
+ 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08,
+ 0xFE, 0x42, 0x00, 0x38, // ..
+ // Le (Ne=65536):
+ 0x0, 0x0,
+ ];
+
+ const GET_ASSERTION_SAMPLE_REQUEST_CTAP2: [u8; 138] = [
+ // CBOR Header
+ 0x0, // leading zero
+ 0x2, // CMD U2F_Authenticate
+ 0x3, // Flags (user presence)
+ 0x0, 0x0, // zero bits
+ 0x0, 0x81, // size
+ // NOTE: This has been taken from CTAP2.0 spec, but the clientDataHash has been replaced
+ // to be able to operate with known values for CollectedClientData (spec doesn't say
+ // what values led to the provided example hash)
+ // clientDataHash:
+ 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, 0x32, 0x64,
+ 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, 0x10, 0x87, 0x54, 0xc3,
+ 0x2d, 0x80, // hash
+ // rpIdHash:
+ 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, 0x34, 0xE2,
+ 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, 0x12, 0x55, 0x86, 0xCE,
+ 0x19, 0x47, // ..
+ // Key Handle Length (1 Byte):
+ 0x40, // ..
+ // Key Handle (Key Handle Length Bytes):
+ 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35, 0xEF, 0xAA,
+ 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D, 0xA4, 0x85, 0x34, 0xC8,
+ 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5, 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD,
+ 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08,
+ 0xFE, 0x42, 0x00, 0x38, 0x0, 0x0, // 2 trailing zeros from protocol
+ ];
+
+ const GET_ASSERTION_SAMPLE_RESPONSE_CTAP1: [u8; 75] = [
+ 0x01, // User Presence (1 Byte)
+ 0x00, 0x00, 0x00, 0x3B, // Sign Count (4 Bytes)
+ // Signature (variable Length)
+ 0x30, 0x44, 0x02, 0x20, 0x7B, 0xDE, 0x0A, 0x52, 0xAC, 0x1F, 0x4C, 0x8B, 0x27, 0xE0, 0x03,
+ 0xA3, 0x70, 0xCD, 0x66, 0xA4, 0xC7, 0x11, 0x8D, 0xD2, 0x2D, 0x54, 0x47, 0x83, 0x5F, 0x45,
+ 0xB9, 0x9C, 0x68, 0x42, 0x3F, 0xF7, 0x02, 0x20, 0x3C, 0x51, 0x7B, 0x47, 0x87, 0x7F, 0x85,
+ 0x78, 0x2D, 0xE1, 0x00, 0x86, 0xA7, 0x83, 0xD1, 0xE7, 0xDF, 0x4E, 0x36, 0x39, 0xE7, 0x71,
+ 0xF5, 0xF6, 0xAF, 0xA3, 0x5A, 0xAD, 0x53, 0x73, 0x85, 0x8E,
+ ];
+
+ const GET_ASSERTION_SAMPLE_RESPONSE_CTAP2: [u8; 348] = [
+ 0x00, // status == success
+ 0xA5, // map(5)
+ 0x01, // unsigned(1)
+ 0xA2, // map(2)
+ 0x62, // text(2)
+ 0x69, 0x64, // "id"
+ 0x58, 0x40, // bytes(0x64, )
+ 0xF2, 0x20, 0x06, 0xDE, 0x4F, 0x90, 0x5A, 0xF6, 0x8A, 0x43, 0x94, 0x2F, 0x02, 0x4F, 0x2A,
+ 0x5E, 0xCE, 0x60, 0x3D, 0x9C, 0x6D, 0x4B, 0x3D, 0xF8, 0xBE, 0x08, 0xED, 0x01, 0xFC, 0x44,
+ 0x26, 0x46, 0xD0, 0x34, 0x85, 0x8A, 0xC7, 0x5B, 0xED, 0x3F, 0xD5, 0x80, 0xBF, 0x98, 0x08,
+ 0xD9, 0x4F, 0xCB, 0xEE, 0x82, 0xB9, 0xB2, 0xEF, 0x66, 0x77, 0xAF, 0x0A, 0xDC, 0xC3, 0x58,
+ 0x52, 0xEA, 0x6B,
+ 0x9E, // "\x0xF2, \x0x06, \x0xDE, O\x0x90, Z\x0xF6, \x0x8A, C\x0x94, /\x0x02, O*^\x0xCE, `=\x0x9C, mK=\x0xF8, \x0xBE, \b\x0xED, \x0x01, \x0xFC, D&F\x0xD0, 4\x0x85, \x0x8A, \x0xC7, [\x0xED, ?\x0xD5, \x0x80, \x0xBF, \x0x98, \b\x0xD9, O\x0xCB, \x0xEE, \x0x82, \x0xB9, \x0xB2, \x0xEF, fw\x0xAF, \n\x0xDC, \x0xC3, 0xXR, \x0xEA, k\x0x9E, "
+ 0x64, // text(4)
+ 0x74, 0x79, 0x70, 0x65, // "type"
+ 0x6A, // text(0x10, )
+ 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // "public-key"
+ 0x02, // unsigned(2)
+ 0x58, 0x25, // bytes(0x37, )
+ 0x62, 0x5D, 0xDA, 0xDF, 0x74, 0x3F, 0x57, 0x27, 0xE6, 0x6B, 0xBA, 0x8C, 0x2E, 0x38, 0x79,
+ 0x22, 0xD1, 0xAF, 0x43, 0xC5, 0x03, 0xD9, 0x11, 0x4A, 0x8F, 0xBA, 0x10, 0x4D, 0x84, 0xD0,
+ 0x2B, 0xFA, 0x01, 0x00, 0x00, 0x00,
+ 0x11, // "b]\x0xDA, \x0xDF, t?W'\x0xE6, k\x0xBA, \x0x8C, .8y\"\x0xD1, \x0xAF, C\x0xC5, \x0x03, \x0xD9, \x0x11, J\x0x8F, \x0xBA, \x0x10, M\x0x84, \x0xD0, +\x0xFA, \x0x01, \x0x00, \x0x00, \x0x00, \x0x11, "
+ 0x03, // unsigned(3)
+ 0x58, 0x47, // bytes(0x71, )
+ 0x30, 0x45, 0x02, 0x20, 0x4A, 0x5A, 0x9D, 0xD3, 0x92, 0x98, 0x14, 0x9D, 0x90, 0x47, 0x69,
+ 0xB5, 0x1A, 0x45, 0x14, 0x33, 0x00, 0x6F, 0x18, 0x2A, 0x34, 0xFB, 0xDF, 0x66, 0xDE, 0x5F,
+ 0xC7, 0x17, 0xD7, 0x5F, 0xB3, 0x50, 0x02, 0x21, 0x00, 0xA4, 0x6B, 0x8E, 0xA3, 0xC3, 0xB9,
+ 0x33, 0x82, 0x1C, 0x6E, 0x7F, 0x5E, 0xF9, 0xDA, 0xAE, 0x94, 0xAB, 0x47, 0xF1, 0x8D, 0xB4,
+ 0x74, 0xC7, 0x47, 0x90, 0xEA, 0xAB, 0xB1, 0x44, 0x11, 0xE7,
+ 0xA0, // "0x0E, \x0x02, 0xJZ, \x0x9D, \x0xD3, \x0x92, \x0x98, \x0x14, \x0x9D, \x0x90, Gi\x0xB5, \x0x1A, E\x0x14, 3\x0x00, o\x0x18, *4\x0xFB, \x0xDF, f\x0xDE, _\x0xC7, \x0x17, \x0xD7, _\x0xB3, P\x0x02, !\x0x00, \x0xA4, k\x0x8E, \x0xA3, \x0xC3, \x0xB9, 3\x0x82, \x0x1C, n\x0x7F, ^\x0xF9, \x0xDA, \x0xAE, \x0x94, \x0xAB, G\x0xF1, \x0x8D, \x0xB4, t\x0xC7, G\x0x90, \x0xEA, \x0xAB, \x0xB1, D\x0x11, \x0xE7, \x0xA0, "
+ 0x04, // unsigned(4)
+ 0xA4, // map(4)
+ 0x62, // text(2)
+ 0x69, 0x64, // "id"
+ 0x58, 0x20, // bytes(0x32, )
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xA0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82,
+ 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xA0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82, 0x01, 0x93,
+ 0x30,
+ 0x82, // "0\x0x82, \x0x01, \x0x93, 0\x0x82, \x0x01, 8\x0xA0, \x0x03, \x0x02, \x0x01, \x0x02, 0\x0x82, \x0x01, \x0x93, 0\x0x82, \x0x01, 8\x0xA0, \x0x03, \x0x02, \x0x01, \x0x02, 0\x0x82, \x0x01, \x0x93, 0\x0x82, "
+ 0x64, // text(4)
+ 0x69, 0x63, 0x6F, 0x6E, // "icon"
+ 0x78, 0x2B, // text(0x43, )
+ 0x68, 0x74, 0x74, 0x70, 0x73, 0x3A, 0x2F, 0x2F, 0x70, 0x69, 0x63, 0x73, 0x2E, 0x65, 0x78,
+ 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x30, 0x30, 0x2F, 0x70, 0x2F,
+ 0x61, 0x42, 0x6A, 0x6A, 0x6A, 0x70, 0x71, 0x50, 0x62, 0x2E, 0x70, 0x6E,
+ 0x67, // "https://pics.example.com/0x00, /p/aBjjjpqPb.png"
+ 0x64, // text(4)
+ 0x6E, 0x61, 0x6D, 0x65, // "name"
+ 0x76, // text(0x22, )
+ 0x6A, 0x6F, 0x68, 0x6E, 0x70, 0x73, 0x6D, 0x69, 0x74, 0x68, 0x40, 0x65, 0x78, 0x61, 0x6D,
+ 0x70, 0x6C, 0x65, 0x2E, 0x63, 0x6F, 0x6D, // "johnpsmith@example.com"
+ 0x6B, // text(0x11, )
+ 0x64, 0x69, 0x73, 0x70, 0x6C, 0x61, 0x79, 0x4E, 0x61, 0x6D, 0x65, // "displayName"
+ 0x6D, // text(0x13, )
+ 0x4A, 0x6F, 0x68, 0x6E, 0x20, 0x50, 0x2E, 0x20, 0x53, 0x6D, 0x69, 0x74,
+ 0x68, // "John P. Smith"
+ 0x05, // unsigned(5)
+ 0x01, // unsigned(1)
+ ];
+}
diff --git a/third_party/rust/authenticator/src/ctap2/commands/get_info.rs b/third_party/rust/authenticator/src/ctap2/commands/get_info.rs
new file mode 100644
index 0000000000..ffad083682
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/get_info.rs
@@ -0,0 +1,733 @@
+use super::{Command, CommandError, RequestCtap2, StatusCode};
+use crate::ctap2::attestation::AAGuid;
+use crate::ctap2::server::PublicKeyCredentialParameters;
+use crate::transport::errors::HIDError;
+use crate::u2ftypes::U2FDevice;
+use serde::{
+ de::{Error as SError, IgnoredAny, MapAccess, Visitor},
+ Deserialize, Deserializer,
+};
+use serde_cbor::{de::from_slice, Value};
+use std::collections::BTreeMap;
+use std::fmt;
+
+#[derive(Debug)]
+pub struct GetInfo {}
+
+impl Default for GetInfo {
+ fn default() -> GetInfo {
+ GetInfo {}
+ }
+}
+
+impl RequestCtap2 for GetInfo {
+ type Output = AuthenticatorInfo;
+
+ fn command() -> Command {
+ Command::GetInfo
+ }
+
+ fn wire_format<Dev>(&self, _dev: &mut Dev) -> Result<Vec<u8>, HIDError>
+ where
+ Dev: U2FDevice,
+ {
+ Ok(Vec::new())
+ }
+
+ fn handle_response_ctap2<Dev>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError>
+ where
+ Dev: U2FDevice,
+ {
+ if input.is_empty() {
+ return Err(CommandError::InputTooSmall.into());
+ }
+
+ let status: StatusCode = input[0].into();
+
+ if input.len() > 1 {
+ if status.is_ok() {
+ trace!("parsing authenticator info data: {:#04X?}", &input);
+ let authenticator_info =
+ from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ Ok(authenticator_info)
+ } else {
+ let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ Err(CommandError::StatusCode(status, Some(data)).into())
+ }
+ } else {
+ Err(CommandError::InputTooSmall.into())
+ }
+ }
+}
+
+fn true_val() -> bool {
+ true
+}
+
+#[derive(Debug, Deserialize, Clone, Eq, PartialEq)]
+pub(crate) struct AuthenticatorOptions {
+ /// Indicates that the device is attached to the client and therefore can’t
+ /// be removed and used on another client.
+ #[serde(rename = "plat", default)]
+ pub(crate) platform_device: bool,
+ /// Indicates that the device is capable of storing keys on the device
+ /// itself and therefore can satisfy the authenticatorGetAssertion request
+ /// with allowList parameter not specified or empty.
+ #[serde(rename = "rk", default)]
+ pub(crate) resident_key: bool,
+
+ /// Client PIN:
+ /// If present and set to true, it indicates that the device is capable of
+ /// accepting a PIN from the client and PIN has been set.
+ /// If present and set to false, it indicates that the device is capable of
+ /// accepting a PIN from the client and PIN has not been set yet.
+ /// If absent, it indicates that the device is not capable of accepting a
+ /// PIN from the client.
+ /// Client PIN is one of the ways to do user verification.
+ #[serde(rename = "clientPin")]
+ pub(crate) client_pin: Option<bool>,
+
+ /// Indicates that the device is capable of testing user presence.
+ #[serde(rename = "up", default = "true_val")]
+ pub(crate) user_presence: bool,
+
+ /// Indicates that the device is capable of verifying the user within
+ /// itself. For example, devices with UI, biometrics fall into this
+ /// category.
+ /// If present and set to true, it indicates that the device is capable of
+ /// user verification within itself and has been configured.
+ /// If present and set to false, it indicates that the device is capable of
+ /// user verification within itself and has not been yet configured. For
+ /// example, a biometric device that has not yet been configured will
+ /// return this parameter set to false.
+ /// If absent, it indicates that the device is not capable of user
+ /// verification within itself.
+ /// A device that can only do Client PIN will not return the "uv" parameter.
+ /// If a device is capable of verifying the user within itself as well as
+ /// able to do Client PIN, it will return both "uv" and the Client PIN
+ /// option.
+ // TODO(MS): My Token (key-ID FIDO2) does return Some(false) here, even though
+ // it has no built-in verification method. Not to be trusted...
+ #[serde(rename = "uv")]
+ pub(crate) user_verification: Option<bool>,
+}
+
+impl Default for AuthenticatorOptions {
+ fn default() -> Self {
+ AuthenticatorOptions {
+ platform_device: false,
+ resident_key: false,
+ client_pin: None,
+ user_presence: true,
+ user_verification: None,
+ }
+ }
+}
+
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+pub struct AuthenticatorInfo {
+ pub(crate) versions: Vec<String>,
+ pub(crate) extensions: Vec<String>,
+ pub(crate) aaguid: AAGuid,
+ pub(crate) options: AuthenticatorOptions,
+ pub(crate) max_msg_size: Option<usize>,
+ pub(crate) pin_protocols: Vec<u64>,
+ // CTAP 2.1
+ pub(crate) max_credential_count_in_list: Option<usize>,
+ pub(crate) max_credential_id_length: Option<usize>,
+ pub(crate) transports: Option<Vec<String>>,
+ pub(crate) algorithms: Option<Vec<PublicKeyCredentialParameters>>,
+ pub(crate) max_ser_large_blob_array: Option<u64>,
+ pub(crate) force_pin_change: Option<bool>,
+ pub(crate) min_pin_length: Option<u64>,
+ pub(crate) firmware_version: Option<u64>,
+ pub(crate) max_cred_blob_length: Option<u64>,
+ pub(crate) max_rpids_for_set_min_pin_length: Option<u64>,
+ pub(crate) preferred_platform_uv_attempts: Option<u64>,
+ pub(crate) uv_modality: Option<u64>,
+ pub(crate) certifications: Option<BTreeMap<String, u64>>,
+ pub(crate) remaining_discoverable_credentials: Option<u64>,
+ pub(crate) vendor_prototype_config_commands: Option<Vec<u64>>,
+}
+
+impl AuthenticatorInfo {
+ pub fn supports_hmac_secret(&self) -> bool {
+ self.extensions.contains(&"hmac-secret".to_string())
+ }
+}
+
+macro_rules! parse_next_optional_value {
+ ($name:expr, $map:expr) => {
+ if $name.is_some() {
+ return Err(serde::de::Error::duplicate_field("$name"));
+ }
+ $name = Some($map.next_value()?);
+ };
+}
+
+impl<'de> Deserialize<'de> for AuthenticatorInfo {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct AuthenticatorInfoVisitor;
+
+ impl<'de> Visitor<'de> for AuthenticatorInfoVisitor {
+ type Value = AuthenticatorInfo;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a byte array")
+ }
+
+ fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ where
+ M: MapAccess<'de>,
+ {
+ let mut versions = Vec::new();
+ let mut extensions = Vec::new();
+ let mut aaguid = None;
+ let mut options = AuthenticatorOptions::default();
+ let mut max_msg_size = None;
+ let mut pin_protocols = Vec::new();
+ let mut max_credential_count_in_list = None;
+ let mut max_credential_id_length = None;
+ let mut transports = None;
+ let mut algorithms = None;
+ let mut max_ser_large_blob_array = None;
+ let mut force_pin_change = None;
+ let mut min_pin_length = None;
+ let mut firmware_version = None;
+ let mut max_cred_blob_length = None;
+ let mut max_rpids_for_set_min_pin_length = None;
+ let mut preferred_platform_uv_attempts = None;
+ let mut uv_modality = None;
+ let mut certifications = None;
+ let mut remaining_discoverable_credentials = None;
+ let mut vendor_prototype_config_commands = None;
+ while let Some(key) = map.next_key()? {
+ match key {
+ 0x01 => {
+ if !versions.is_empty() {
+ return Err(serde::de::Error::duplicate_field("versions"));
+ }
+ versions = map.next_value()?;
+ }
+ 0x02 => {
+ if !extensions.is_empty() {
+ return Err(serde::de::Error::duplicate_field("extensions"));
+ }
+ extensions = map.next_value()?;
+ }
+ 0x03 => {
+ parse_next_optional_value!(aaguid, map);
+ }
+ 0x04 => {
+ options = map.next_value()?;
+ }
+ 0x05 => {
+ parse_next_optional_value!(max_msg_size, map);
+ }
+ 0x06 => {
+ if !pin_protocols.is_empty() {
+ return Err(serde::de::Error::duplicate_field("pin_protocols"));
+ }
+ pin_protocols = map.next_value()?;
+ }
+ 0x07 => {
+ parse_next_optional_value!(max_credential_count_in_list, map);
+ }
+ 0x08 => {
+ parse_next_optional_value!(max_credential_id_length, map);
+ }
+ 0x09 => {
+ parse_next_optional_value!(transports, map);
+ }
+ 0x0a => {
+ parse_next_optional_value!(algorithms, map);
+ }
+ 0x0b => {
+ parse_next_optional_value!(max_ser_large_blob_array, map);
+ }
+ 0x0c => {
+ parse_next_optional_value!(force_pin_change, map);
+ }
+ 0x0d => {
+ parse_next_optional_value!(min_pin_length, map);
+ }
+ 0x0e => {
+ parse_next_optional_value!(firmware_version, map);
+ }
+ 0x0f => {
+ parse_next_optional_value!(max_cred_blob_length, map);
+ }
+ 0x10 => {
+ parse_next_optional_value!(max_rpids_for_set_min_pin_length, map);
+ }
+ 0x11 => {
+ parse_next_optional_value!(preferred_platform_uv_attempts, map);
+ }
+ 0x12 => {
+ parse_next_optional_value!(uv_modality, map);
+ }
+ 0x13 => {
+ parse_next_optional_value!(certifications, map);
+ }
+ 0x14 => {
+ parse_next_optional_value!(remaining_discoverable_credentials, map);
+ }
+ 0x15 => {
+ parse_next_optional_value!(vendor_prototype_config_commands, map);
+ }
+ k => {
+ warn!("GetInfo: unexpected key: {:?}", k);
+ let _ = map.next_value::<IgnoredAny>()?;
+ continue;
+ }
+ }
+ }
+
+ if versions.is_empty() {
+ return Err(M::Error::custom(
+ "expected at least one version, got none".to_string(),
+ ));
+ }
+
+ if let Some(aaguid) = aaguid {
+ Ok(AuthenticatorInfo {
+ versions,
+ extensions,
+ aaguid,
+ options,
+ max_msg_size,
+ pin_protocols,
+ max_credential_count_in_list,
+ max_credential_id_length,
+ transports,
+ algorithms,
+ max_ser_large_blob_array,
+ force_pin_change,
+ min_pin_length,
+ firmware_version,
+ max_cred_blob_length,
+ max_rpids_for_set_min_pin_length,
+ preferred_platform_uv_attempts,
+ uv_modality,
+ certifications,
+ remaining_discoverable_credentials,
+ vendor_prototype_config_commands,
+ })
+ } else {
+ Err(M::Error::custom("No AAGuid specified".to_string()))
+ }
+ }
+ }
+
+ deserializer.deserialize_bytes(AuthenticatorInfoVisitor)
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ use super::*;
+ use crate::consts::{Capability, HIDCmd, CID_BROADCAST};
+ use crate::crypto::COSEAlgorithm;
+ use crate::transport::device_selector::Device;
+ use crate::transport::platform::device::IN_HID_RPT_SIZE;
+ use crate::transport::{hid::HIDDevice, FidoDevice, Nonce};
+ use crate::u2ftypes::U2FDevice;
+ use rand::{thread_rng, RngCore};
+ use serde_cbor::de::from_slice;
+
+ // Raw data take from https://github.com/Yubico/python-fido2/blob/master/test/test_ctap2.py
+ pub const AAGUID_RAW: [u8; 16] = [
+ 0xF8, 0xA0, 0x11, 0xF3, 0x8C, 0x0A, 0x4D, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1F, 0x9E, 0xDC,
+ 0x7D,
+ ];
+
+ pub const AUTHENTICATOR_INFO_PAYLOAD: [u8; 89] = [
+ 0xa6, // map(6)
+ 0x01, // unsigned(1)
+ 0x82, // array(2)
+ 0x66, // text(6)
+ 0x55, 0x32, 0x46, 0x5f, 0x56, 0x32, // "U2F_V2"
+ 0x68, // text(8)
+ 0x46, 0x49, 0x44, 0x4f, 0x5f, 0x32, 0x5f, 0x30, // "FIDO_2_0"
+ 0x02, // unsigned(2)
+ 0x82, // array(2)
+ 0x63, // text(3)
+ 0x75, 0x76, 0x6d, // "uvm"
+ 0x6b, // text(11)
+ 0x68, 0x6d, 0x61, 0x63, 0x2d, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // "hmac-secret"
+ 0x03, // unsigned(3)
+ 0x50, // bytes(16)
+ 0xf8, 0xa0, 0x11, 0xf3, 0x8c, 0x0a, 0x4d, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1f, 0x9e, 0xdc,
+ 0x7d, // "\xF8\xA0\u0011\xF3\x8C\nM\u0015\x80\u0006\u0017\u0011\u001F\x9E\xDC}"
+ 0x04, // unsigned(4)
+ 0xa4, // map(4)
+ 0x62, // text(2)
+ 0x72, 0x6b, // "rk"
+ 0xf5, // primitive(21)
+ 0x62, // text(2)
+ 0x75, 0x70, // "up"
+ 0xf5, // primitive(21)
+ 0x64, // text(4)
+ 0x70, 0x6c, 0x61, 0x74, // "plat"
+ 0xf4, // primitive(20)
+ 0x69, // text(9)
+ 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x50, 0x69, 0x6e, // "clientPin"
+ 0xf4, // primitive(20)
+ 0x05, // unsigned(5)
+ 0x19, 0x04, 0xb0, // unsigned(1200)
+ 0x06, // unsigned(6)
+ 0x81, // array(1)
+ 0x01, // unsigned(1)
+ ];
+
+ // Real world example from Yubikey Bio
+ pub const AUTHENTICATOR_INFO_PAYLOAD_YK_BIO_5C: [u8; 409] = [
+ 0xB3, // map(19)
+ 0x01, // unsigned(1)
+ 0x84, // array(4)
+ 0x66, // text(6)
+ 0x55, 0x32, 0x46, 0x5F, 0x56, 0x32, // "U2F_V2"
+ 0x68, // text(8)
+ 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30, // "FIDO_2_0"
+ 0x6C, // text(12)
+ 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52,
+ 0x45, // "FIDO_2_1_PRE"
+ 0x68, // text(8)
+ 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x31, // "FIDO_2_1"
+ 0x02, // unsigned(2)
+ 0x85, // array(5)
+ 0x6B, // text(11)
+ 0x63, 0x72, 0x65, 0x64, 0x50, 0x72, 0x6F, 0x74, 0x65, 0x63, 0x74, // "credProtect"
+ 0x6B, // text(11)
+ 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // "hmac-secret"
+ 0x6C, // text(12)
+ 0x6C, 0x61, 0x72, 0x67, 0x65, 0x42, 0x6C, 0x6F, 0x62, 0x4B, 0x65,
+ 0x79, // "largeBlobKey"
+ 0x68, // text(8)
+ 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, // "credBlob"
+ 0x6C, // text(12)
+ 0x6D, 0x69, 0x6E, 0x50, 0x69, 0x6E, 0x4C, 0x65, 0x6E, 0x67, 0x74,
+ 0x68, // "minPinLength"
+ 0x03, // unsigned(3)
+ 0x50, // bytes(16)
+ 0xD8, 0x52, 0x2D, 0x9F, 0x57, 0x5B, 0x48, 0x66, 0x88, 0xA9, 0xBA, 0x99, 0xFA, 0x02, 0xF3,
+ 0x5B, // "\xD8R-\x9FW[Hf\x88\xA9\xBA\x99\xFA\u0002\xF3["
+ 0x04, // unsigned(4)
+ 0xB0, // map(16)
+ 0x62, // text(2)
+ 0x72, 0x6B, // "rk"
+ 0xF5, // primitive(21)
+ 0x62, // text(2)
+ 0x75, 0x70, // "up"
+ 0xF5, // primitive(21)
+ 0x62, // text(2)
+ 0x75, 0x76, // "uv"
+ 0xF5, // primitive(21)
+ 0x64, // text(4)
+ 0x70, 0x6C, 0x61, 0x74, // "plat"
+ 0xF4, // primitive(20)
+ 0x67, // text(7)
+ 0x75, 0x76, 0x54, 0x6F, 0x6B, 0x65, 0x6E, // "uvToken"
+ 0xF5, // primitive(21)
+ 0x68, // text(8)
+ 0x61, 0x6C, 0x77, 0x61, 0x79, 0x73, 0x55, 0x76, // "alwaysUv"
+ 0xF5, // primitive(21)
+ 0x68, // text(8)
+ 0x63, 0x72, 0x65, 0x64, 0x4D, 0x67, 0x6D, 0x74, // "credMgmt"
+ 0xF5, // primitive(21)
+ 0x69, // text(9)
+ 0x61, 0x75, 0x74, 0x68, 0x6E, 0x72, 0x43, 0x66, 0x67, // "authnrCfg"
+ 0xF5, // primitive(21)
+ 0x69, // text(9)
+ 0x62, 0x69, 0x6F, 0x45, 0x6E, 0x72, 0x6F, 0x6C, 0x6C, // "bioEnroll"
+ 0xF5, // primitive(21)
+ 0x69, // text(9)
+ 0x63, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x50, 0x69, 0x6E, // "clientPin"
+ 0xF5, // primitive(21)
+ 0x6A, // text(10)
+ 0x6C, 0x61, 0x72, 0x67, 0x65, 0x42, 0x6C, 0x6F, 0x62, 0x73, // "largeBlobs"
+ 0xF5, // primitive(21)
+ 0x6E, // text(14)
+ 0x70, 0x69, 0x6E, 0x55, 0x76, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6F, 0x6B, 0x65,
+ 0x6E, // "pinUvAuthToken"
+ 0xF5, // primitive(21)
+ 0x6F, // text(15)
+ 0x73, 0x65, 0x74, 0x4D, 0x69, 0x6E, 0x50, 0x49, 0x4E, 0x4C, 0x65, 0x6E, 0x67, 0x74,
+ 0x68, // "setMinPINLength"
+ 0xF5, // primitive(21)
+ 0x70, // text(16)
+ 0x6D, 0x61, 0x6B, 0x65, 0x43, 0x72, 0x65, 0x64, 0x55, 0x76, 0x4E, 0x6F, 0x74, 0x52, 0x71,
+ 0x64, // "makeCredUvNotRqd"
+ 0xF4, // primitive(20)
+ 0x75, // text(21)
+ 0x63, 0x72, 0x65, 0x64, 0x65, 0x6E, 0x74, 0x69, 0x61, 0x6C, 0x4D, 0x67, 0x6D, 0x74, 0x50,
+ 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, // "credentialMgmtPreview"
+ 0xF5, // primitive(21)
+ 0x78, 0x1B, // text(27)
+ 0x75, 0x73, 0x65, 0x72, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F,
+ 0x6E, 0x4D, 0x67, 0x6D, 0x74, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65,
+ 0x77, // "userVerificationMgmtPreview"
+ 0xF5, // primitive(21)
+ 0x05, // unsigned(5)
+ 0x19, 0x04, 0xB0, // unsigned(1200)
+ 0x06, // unsigned(6)
+ 0x82, // array(2)
+ 0x02, // unsigned(2)
+ 0x01, // unsigned(1)
+ 0x07, // unsigned(7)
+ 0x08, // unsigned(8)
+ 0x08, // unsigned(8)
+ 0x18, 0x80, // unsigned(128)
+ 0x09, // unsigned(9)
+ 0x81, // array(1)
+ 0x63, // text(3)
+ 0x75, 0x73, 0x62, // "usb"
+ 0x0A, // unsigned(10)
+ 0x82, // array(2)
+ 0xA2, // map(2)
+ 0x63, // text(3)
+ 0x61, 0x6C, 0x67, // "alg"
+ 0x26, // negative(6)
+ 0x64, // text(4)
+ 0x74, 0x79, 0x70, 0x65, // "type"
+ 0x6A, // text(10)
+ 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // "public-key"
+ 0xA2, // map(2)
+ 0x63, // text(3)
+ 0x61, 0x6C, 0x67, // "alg"
+ 0x27, // negative(7)
+ 0x64, // text(4)
+ 0x74, 0x79, 0x70, 0x65, // "type"
+ 0x6A, // text(10)
+ 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // "public-key"
+ 0x0B, // unsigned(11)
+ 0x19, 0x04, 0x00, // unsigned(1024)
+ 0x0C, // unsigned(12)
+ 0xF4, // primitive(20)
+ 0x0D, // unsigned(13)
+ 0x04, // unsigned(4)
+ 0x0E, // unsigned(14)
+ 0x1A, 0x00, 0x05, 0x05, 0x06, // unsigned(328966)
+ 0x0F, // unsigned(15)
+ 0x18, 0x20, // unsigned(32)
+ 0x10, // unsigned(16)
+ 0x01, // unsigned(1)
+ 0x11, // unsigned(17)
+ 0x03, // unsigned(3)
+ 0x12, // unsigned(18)
+ 0x02, // unsigned(2)
+ 0x14, // unsigned(20)
+ 0x18, 0x18, // unsigned(24)
+ ];
+
+ #[test]
+ fn parse_authenticator_info() {
+ let authenticator_info: AuthenticatorInfo =
+ from_slice(&AUTHENTICATOR_INFO_PAYLOAD).unwrap();
+
+ let expected = AuthenticatorInfo {
+ versions: vec!["U2F_V2".to_string(), "FIDO_2_0".to_string()],
+ extensions: vec!["uvm".to_string(), "hmac-secret".to_string()],
+ aaguid: AAGuid(AAGUID_RAW),
+ options: AuthenticatorOptions {
+ platform_device: false,
+ resident_key: true,
+ client_pin: Some(false),
+ user_presence: true,
+ user_verification: None,
+ },
+ max_msg_size: Some(1200),
+ pin_protocols: vec![1],
+ max_credential_count_in_list: None,
+ max_credential_id_length: None,
+ transports: None,
+ algorithms: None,
+ max_ser_large_blob_array: None,
+ force_pin_change: None,
+ min_pin_length: None,
+ firmware_version: None,
+ max_cred_blob_length: None,
+ max_rpids_for_set_min_pin_length: None,
+ preferred_platform_uv_attempts: None,
+ uv_modality: None,
+ certifications: None,
+ remaining_discoverable_credentials: None,
+ vendor_prototype_config_commands: None,
+ };
+
+ assert_eq!(authenticator_info, expected);
+
+ // Test broken auth info
+ let mut broken_payload = AUTHENTICATOR_INFO_PAYLOAD.to_vec();
+ // Have one more entry in the map
+ broken_payload[0] += 1;
+ // Add the additional entry at the back with an invalid key
+ broken_payload.extend_from_slice(&[
+ 0x17, // unsigned(23) -> invalid key-number. CTAP2.1 goes only to 0x15
+ 0x6B, // text(11)
+ 0x69, 0x6E, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x5F, 0x6B, 0x65, 0x79, // "invalid_key"
+ ]);
+
+ let authenticator_info: AuthenticatorInfo = from_slice(&broken_payload).unwrap();
+ assert_eq!(authenticator_info, expected);
+ }
+
+ #[test]
+ fn parse_authenticator_info_yk_bio_5c() {
+ let authenticator_info: AuthenticatorInfo =
+ from_slice(&AUTHENTICATOR_INFO_PAYLOAD_YK_BIO_5C).unwrap();
+
+ let expected = AuthenticatorInfo {
+ versions: vec![
+ "U2F_V2".to_string(),
+ "FIDO_2_0".to_string(),
+ "FIDO_2_1_PRE".to_string(),
+ "FIDO_2_1".to_string(),
+ ],
+ extensions: vec![
+ "credProtect".to_string(),
+ "hmac-secret".to_string(),
+ "largeBlobKey".to_string(),
+ "credBlob".to_string(),
+ "minPinLength".to_string(),
+ ],
+ aaguid: AAGuid([
+ 0xd8, 0x52, 0x2d, 0x9f, 0x57, 0x5b, 0x48, 0x66, 0x88, 0xa9, 0xba, 0x99, 0xfa, 0x02,
+ 0xf3, 0x5b,
+ ]),
+ options: AuthenticatorOptions {
+ platform_device: false,
+ resident_key: true,
+ client_pin: Some(true),
+ user_presence: true,
+ user_verification: Some(true),
+ },
+ max_msg_size: Some(1200),
+ pin_protocols: vec![2, 1],
+ max_credential_count_in_list: Some(8),
+ max_credential_id_length: Some(128),
+ transports: Some(vec!["usb".to_string()]),
+ algorithms: Some(vec![
+ PublicKeyCredentialParameters {
+ alg: COSEAlgorithm::ES256,
+ },
+ PublicKeyCredentialParameters {
+ alg: COSEAlgorithm::EDDSA,
+ },
+ ]),
+ max_ser_large_blob_array: Some(1024),
+ force_pin_change: Some(false),
+ min_pin_length: Some(4),
+ firmware_version: Some(328966),
+ max_cred_blob_length: Some(32),
+ max_rpids_for_set_min_pin_length: Some(1),
+ preferred_platform_uv_attempts: Some(3),
+ uv_modality: Some(2),
+ certifications: None,
+ remaining_discoverable_credentials: Some(24),
+ vendor_prototype_config_commands: None,
+ };
+
+ assert_eq!(authenticator_info, expected);
+ }
+
+ #[test]
+ fn test_get_info_ctap2_only() {
+ let mut device = Device::new("commands/get_info").unwrap();
+ let nonce = [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01];
+
+ // channel id
+ let mut cid = [0u8; 4];
+ thread_rng().fill_bytes(&mut cid);
+
+ // init packet
+ let mut msg = CID_BROADCAST.to_vec();
+ msg.extend(vec![HIDCmd::Init.into(), 0x00, 0x08]); // cmd + bcnt
+ msg.extend_from_slice(&nonce);
+ device.add_write(&msg, 0);
+
+ // init_resp packet
+ let mut msg = CID_BROADCAST.to_vec();
+ msg.extend(vec![
+ 0x06, /*HIDCmd::Init without TYPE_INIT*/
+ 0x00, 0x11,
+ ]); // cmd + bcnt
+ msg.extend_from_slice(&nonce);
+ msg.extend_from_slice(&cid); // new channel id
+
+ // We are setting NMSG, to signal that the device does not support CTAP1
+ msg.extend(vec![0x02, 0x04, 0x01, 0x08, 0x01 | 0x04 | 0x08]); // versions + flags (wink+cbor+nmsg)
+ device.add_read(&msg, 0);
+
+ // ctap2 request
+ let mut msg = cid.to_vec();
+ msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x1]); // cmd + bcnt
+ msg.extend(vec![0x04]); // authenticatorGetInfo
+ device.add_write(&msg, 0);
+
+ // ctap2 response
+ let mut msg = cid.to_vec();
+ msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x5A]); // cmd + bcnt
+ msg.extend(vec![0]); // Status code: Success
+ msg.extend(&AUTHENTICATOR_INFO_PAYLOAD[0..(IN_HID_RPT_SIZE - 8)]);
+ device.add_read(&msg, 0);
+ // Continuation package
+ let mut msg = cid.to_vec();
+ msg.extend(vec![0x00]); // SEQ
+ msg.extend(&AUTHENTICATOR_INFO_PAYLOAD[(IN_HID_RPT_SIZE - 8)..]);
+ device.add_read(&msg, 0);
+ device
+ .init(Nonce::Use(nonce))
+ .expect("Failed to init device");
+
+ assert_eq!(device.get_cid(), &cid);
+
+ let dev_info = device.get_device_info();
+ assert_eq!(
+ dev_info.cap_flags,
+ Capability::WINK | Capability::CBOR | Capability::NMSG
+ );
+
+ let result = device
+ .get_authenticator_info()
+ .expect("Didn't get any authenticator_info");
+ let expected = AuthenticatorInfo {
+ versions: vec!["U2F_V2".to_string(), "FIDO_2_0".to_string()],
+ extensions: vec!["uvm".to_string(), "hmac-secret".to_string()],
+ aaguid: AAGuid(AAGUID_RAW),
+ options: AuthenticatorOptions {
+ platform_device: false,
+ resident_key: true,
+ client_pin: Some(false),
+ user_presence: true,
+ user_verification: None,
+ },
+ max_msg_size: Some(1200),
+ pin_protocols: vec![1],
+ max_credential_count_in_list: None,
+ max_credential_id_length: None,
+ transports: None,
+ algorithms: None,
+ max_ser_large_blob_array: None,
+ force_pin_change: None,
+ min_pin_length: None,
+ firmware_version: None,
+ max_cred_blob_length: None,
+ max_rpids_for_set_min_pin_length: None,
+ preferred_platform_uv_attempts: None,
+ uv_modality: None,
+ certifications: None,
+ remaining_discoverable_credentials: None,
+ vendor_prototype_config_commands: None,
+ };
+
+ assert_eq!(result, &expected);
+ }
+}
diff --git a/third_party/rust/authenticator/src/ctap2/commands/get_next_assertion.rs b/third_party/rust/authenticator/src/ctap2/commands/get_next_assertion.rs
new file mode 100644
index 0000000000..dd5b23edcd
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/get_next_assertion.rs
@@ -0,0 +1,53 @@
+use super::{Command, CommandError, RequestCtap2, StatusCode};
+use crate::ctap2::commands::get_assertion::GetAssertionResponse;
+use crate::transport::errors::HIDError;
+use crate::u2ftypes::U2FDevice;
+use serde_cbor::{de::from_slice, Value};
+
+#[derive(Debug)]
+pub(crate) struct GetNextAssertion;
+
+impl RequestCtap2 for GetNextAssertion {
+ type Output = GetAssertionResponse;
+
+ fn command() -> Command {
+ Command::GetNextAssertion
+ }
+
+ fn wire_format<Dev>(&self, _dev: &mut Dev) -> Result<Vec<u8>, HIDError>
+ where
+ Dev: U2FDevice,
+ {
+ Ok(Vec::new())
+ }
+
+ fn handle_response_ctap2<Dev>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError>
+ where
+ Dev: U2FDevice,
+ {
+ if input.is_empty() {
+ return Err(CommandError::InputTooSmall.into());
+ }
+
+ let status: StatusCode = input[0].into();
+ debug!("response status code: {:?}", status);
+ if input.len() > 1 {
+ if status.is_ok() {
+ let assertion = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ // TODO(baloo): check assertion response does not have numberOfCredentials
+ Ok(assertion)
+ } else {
+ let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ Err(CommandError::StatusCode(status, Some(data)).into())
+ }
+ } else if status.is_ok() {
+ Err(CommandError::InputTooSmall.into())
+ } else {
+ Err(CommandError::StatusCode(status, None).into())
+ }
+ }
+}
diff --git a/third_party/rust/authenticator/src/ctap2/commands/get_version.rs b/third_party/rust/authenticator/src/ctap2/commands/get_version.rs
new file mode 100644
index 0000000000..6f088da1bf
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/get_version.rs
@@ -0,0 +1,118 @@
+use super::{CommandError, RequestCtap1, Retryable};
+use crate::consts::U2F_VERSION;
+use crate::transport::errors::{ApduErrorStatus, HIDError};
+use crate::u2ftypes::CTAP1RequestAPDU;
+use crate::u2ftypes::U2FDevice;
+
+#[allow(non_camel_case_types)]
+pub enum U2FInfo {
+ U2F_V2,
+}
+
+#[derive(Debug)]
+// TODO(baloo): if one does not issue U2F_VERSION before makecredentials or getassertion, token
+// will return error (ConditionsNotSatified), test this in unit tests
+pub struct GetVersion {}
+
+impl Default for GetVersion {
+ fn default() -> GetVersion {
+ GetVersion {}
+ }
+}
+
+impl RequestCtap1 for GetVersion {
+ type Output = U2FInfo;
+
+ fn handle_response_ctap1(
+ &self,
+ _status: Result<(), ApduErrorStatus>,
+ input: &[u8],
+ ) -> Result<Self::Output, Retryable<HIDError>> {
+ if input.is_empty() {
+ return Err(Retryable::Error(HIDError::Command(
+ CommandError::InputTooSmall,
+ )));
+ }
+
+ let expected = String::from("U2F_V2");
+ let result = String::from_utf8_lossy(input);
+ match result {
+ ref data if data == &expected => Ok(U2FInfo::U2F_V2),
+ _ => Err(Retryable::Error(HIDError::UnexpectedVersion)),
+ }
+ }
+
+ fn ctap1_format<Dev>(&self, _dev: &mut Dev) -> Result<Vec<u8>, HIDError>
+ where
+ Dev: U2FDevice,
+ {
+ let flags = 0;
+
+ let cmd = U2F_VERSION;
+ let data = CTAP1RequestAPDU::serialize(cmd, flags, &[])?;
+ Ok(data)
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ use crate::consts::{Capability, HIDCmd, CID_BROADCAST, SW_NO_ERROR};
+ use crate::transport::device_selector::Device;
+ use crate::transport::{hid::HIDDevice, FidoDevice, Nonce};
+ use crate::u2ftypes::U2FDevice;
+ use rand::{thread_rng, RngCore};
+
+ #[test]
+ fn test_get_version_ctap1_only() {
+ let mut device = Device::new("commands/get_version").unwrap();
+ let nonce = [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01];
+
+ // channel id
+ let mut cid = [0u8; 4];
+ thread_rng().fill_bytes(&mut cid);
+
+ // init packet
+ let mut msg = CID_BROADCAST.to_vec();
+ msg.extend(&[HIDCmd::Init.into(), 0x00, 0x08]); // cmd + bcnt
+ msg.extend_from_slice(&nonce);
+ device.add_write(&msg, 0);
+
+ // init_resp packet
+ let mut msg = CID_BROADCAST.to_vec();
+ msg.extend(vec![
+ 0x06, /*HIDCmd::Init without !TYPE_INIT*/
+ 0x00, 0x11,
+ ]); // cmd + bcnt
+ msg.extend_from_slice(&nonce);
+ msg.extend_from_slice(&cid); // new channel id
+
+ // We are not setting CBOR, to signal that the device does not support CTAP1
+ msg.extend(&[0x02, 0x04, 0x01, 0x08, 0x01]); // versions + flags (wink)
+ device.add_read(&msg, 0);
+
+ // ctap1 U2F_VERSION request
+ let mut msg = cid.to_vec();
+ msg.extend(&[HIDCmd::Msg.into(), 0x0, 0x7]); // cmd + bcnt
+ msg.extend(&[0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0]);
+ device.add_write(&msg, 0);
+
+ // fido response
+ let mut msg = cid.to_vec();
+ msg.extend(&[HIDCmd::Msg.into(), 0x0, 0x08]); // cmd + bcnt
+ msg.extend(&[0x55, 0x32, 0x46, 0x5f, 0x56, 0x32]); // 'U2F_V2'
+ msg.extend(&SW_NO_ERROR);
+ device.add_read(&msg, 0);
+
+ device
+ .init(Nonce::Use(nonce))
+ .expect("Failed to init device");
+
+ assert_eq!(device.get_cid(), &cid);
+
+ let dev_info = device.get_device_info();
+ assert_eq!(dev_info.cap_flags, Capability::WINK);
+
+ let result = device.get_authenticator_info();
+ assert!(result.is_none());
+ }
+}
diff --git a/third_party/rust/authenticator/src/ctap2/commands/make_credentials.rs b/third_party/rust/authenticator/src/ctap2/commands/make_credentials.rs
new file mode 100644
index 0000000000..7f3fa0bf79
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/make_credentials.rs
@@ -0,0 +1,1033 @@
+use super::{
+ Command, CommandError, PinAuthCommand, Request, RequestCtap1, RequestCtap2, Retryable,
+ StatusCode,
+};
+use crate::consts::{PARAMETER_SIZE, U2F_REGISTER, U2F_REQUEST_USER_PRESENCE};
+use crate::crypto::{
+ parse_u2f_der_certificate, serialize_key, COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType,
+ ECDSACurve,
+};
+use crate::ctap2::attestation::{
+ AAGuid, AttestationObject, AttestationStatement, AttestationStatementFidoU2F,
+ AttestedCredentialData, AuthenticatorData, AuthenticatorDataFlags,
+};
+use crate::ctap2::client_data::{
+ Challenge, ClientDataHash, CollectedClientData, CollectedClientDataWrapper, WebauthnType,
+};
+use crate::ctap2::commands::client_pin::{Pin, PinAuth};
+use crate::ctap2::commands::get_assertion::CheckKeyHandle;
+use crate::ctap2::server::{
+ PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty,
+ RelyingPartyWrapper, User,
+};
+use crate::transport::{
+ errors::{ApduErrorStatus, HIDError},
+ FidoDevice,
+};
+use crate::u2ftypes::{CTAP1RequestAPDU, U2FDevice};
+use nom::{
+ bytes::complete::{tag, take},
+ error::VerboseError,
+ number::complete::be_u8,
+};
+#[cfg(test)]
+use serde::Deserialize;
+use serde::{
+ de::Error as DesError,
+ ser::{Error as SerError, SerializeMap},
+ Serialize, Serializer,
+};
+use serde_cbor::{self, de::from_slice, ser, Value};
+use std::fmt;
+use std::io;
+
+#[derive(Debug)]
+pub enum MakeCredentialsResult {
+ CTAP1(Vec<u8>),
+ CTAP2(AttestationObject, CollectedClientDataWrapper),
+}
+
+#[derive(Copy, Clone, Debug, Serialize)]
+#[cfg_attr(test, derive(Deserialize))]
+pub struct MakeCredentialsOptions {
+ #[serde(rename = "rk", skip_serializing_if = "Option::is_none")]
+ pub resident_key: Option<bool>,
+ #[serde(rename = "uv", skip_serializing_if = "Option::is_none")]
+ pub user_verification: Option<bool>,
+ // TODO(MS): ctap2.1 supports user_presence, but ctap2.0 does not and tokens will error out
+ // Commands need a version-flag to know what to de/serialize and what to ignore.
+}
+
+impl Default for MakeCredentialsOptions {
+ fn default() -> Self {
+ Self {
+ resident_key: None,
+ user_verification: None,
+ }
+ }
+}
+
+impl MakeCredentialsOptions {
+ pub(crate) fn has_some(&self) -> bool {
+ self.resident_key.is_some() || self.user_verification.is_some()
+ }
+}
+
+pub(crate) trait UserVerification {
+ fn ask_user_verification(&self) -> bool;
+}
+
+impl UserVerification for MakeCredentialsOptions {
+ fn ask_user_verification(&self) -> bool {
+ if let Some(e) = self.user_verification {
+ e
+ } else {
+ false
+ }
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Default)]
+pub struct MakeCredentialsExtensions {
+ #[serde(rename = "pinMinLength", skip_serializing_if = "Option::is_none")]
+ pub pin_min_length: Option<bool>,
+ #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")]
+ pub hmac_secret: Option<bool>,
+}
+
+impl MakeCredentialsExtensions {
+ fn has_extensions(&self) -> bool {
+ self.pin_min_length.or(self.hmac_secret).is_some()
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct MakeCredentials {
+ pub(crate) client_data_wrapper: CollectedClientDataWrapper,
+ pub(crate) rp: RelyingPartyWrapper,
+ // Note(baloo): If none -> ctap1
+ pub(crate) user: Option<User>,
+ pub(crate) pub_cred_params: Vec<PublicKeyCredentialParameters>,
+ pub(crate) exclude_list: Vec<PublicKeyCredentialDescriptor>,
+
+ // https://www.w3.org/TR/webauthn/#client-extension-input
+ // The client extension input, which is a value that can be encoded in JSON,
+ // is passed from the WebAuthn Relying Party to the client in the get() or
+ // create() call, while the CBOR authenticator extension input is passed
+ // from the client to the authenticator for authenticator extensions during
+ // the processing of these calls.
+ pub(crate) extensions: MakeCredentialsExtensions,
+ pub(crate) options: MakeCredentialsOptions,
+ pub(crate) pin: Option<Pin>,
+ pub(crate) pin_auth: Option<PinAuth>,
+ pub(crate) pin_auth_protocol: Option<u64>,
+ pub(crate) enterprise_attestation: Option<u64>,
+}
+
+impl MakeCredentials {
+ pub fn new(
+ client_data: CollectedClientData,
+ rp: RelyingPartyWrapper,
+ user: Option<User>,
+ pub_cred_params: Vec<PublicKeyCredentialParameters>,
+ exclude_list: Vec<PublicKeyCredentialDescriptor>,
+ options: MakeCredentialsOptions,
+ extensions: MakeCredentialsExtensions,
+ pin: Option<Pin>,
+ ) -> Result<Self, HIDError> {
+ let client_data_wrapper = CollectedClientDataWrapper::new(client_data)?;
+ Ok(Self {
+ client_data_wrapper,
+ rp,
+ user,
+ pub_cred_params,
+ exclude_list,
+ extensions,
+ options,
+ pin,
+ pin_auth: None,
+ pin_auth_protocol: None,
+ enterprise_attestation: None,
+ })
+ }
+}
+
+impl PinAuthCommand for MakeCredentials {
+ fn pin(&self) -> &Option<Pin> {
+ &self.pin
+ }
+
+ fn set_pin(&mut self, pin: Option<Pin>) {
+ self.pin = pin;
+ }
+
+ fn pin_auth(&self) -> &Option<PinAuth> {
+ &self.pin_auth
+ }
+
+ fn set_pin_auth(&mut self, pin_auth: Option<PinAuth>, pin_auth_protocol: Option<u64>) {
+ self.pin_auth = pin_auth;
+ self.pin_auth_protocol = pin_auth_protocol;
+ }
+
+ fn client_data_hash(&self) -> ClientDataHash {
+ self.client_data_wrapper.hash()
+ }
+
+ fn unset_uv_option(&mut self) {
+ self.options.user_verification = None;
+ }
+}
+
+impl Serialize for MakeCredentials {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ debug!("Serialize MakeCredentials");
+ // Need to define how many elements are going to be in the map
+ // beforehand
+ let mut map_len = 4;
+ if !self.exclude_list.is_empty() {
+ map_len += 1;
+ }
+ if self.extensions.has_extensions() {
+ map_len += 1;
+ }
+ if self.options.has_some() {
+ map_len += 1;
+ }
+ if self.pin_auth.is_some() {
+ map_len += 1;
+ }
+ if self.pin_auth_protocol.is_some() {
+ map_len += 1;
+ }
+ if self.enterprise_attestation.is_some() {
+ map_len += 1;
+ }
+
+ let mut map = serializer.serialize_map(Some(map_len))?;
+ let client_data_hash = self.client_data_hash();
+ map.serialize_entry(&0x01, &client_data_hash)?;
+ match self.rp {
+ RelyingPartyWrapper::Data(ref d) => {
+ map.serialize_entry(&0x02, &d)?;
+ }
+ _ => {
+ return Err(S::Error::custom(
+ "Can't serialize a RelyingParty::Hash for CTAP2",
+ ));
+ }
+ }
+ map.serialize_entry(&0x03, &self.user)?;
+ map.serialize_entry(&0x04, &self.pub_cred_params)?;
+ if !self.exclude_list.is_empty() {
+ map.serialize_entry(&0x05, &self.exclude_list)?;
+ }
+ if self.extensions.has_extensions() {
+ map.serialize_entry(&0x06, &self.extensions)?;
+ }
+ if self.options.has_some() {
+ map.serialize_entry(&0x07, &self.options)?;
+ }
+ if let Some(pin_auth) = &self.pin_auth {
+ map.serialize_entry(&0x08, &pin_auth)?;
+ }
+ if let Some(pin_auth_protocol) = &self.pin_auth_protocol {
+ map.serialize_entry(&0x09, &pin_auth_protocol)?;
+ }
+ if let Some(enterprise_attestation) = self.enterprise_attestation {
+ map.serialize_entry(&0x0a, &enterprise_attestation)?;
+ }
+ map.end()
+ }
+}
+
+impl Request<MakeCredentialsResult> for MakeCredentials {
+ fn is_ctap2_request(&self) -> bool {
+ self.user.is_some()
+ }
+}
+
+impl RequestCtap1 for MakeCredentials {
+ type Output = MakeCredentialsResult;
+
+ fn ctap1_format<Dev>(&self, dev: &mut Dev) -> Result<Vec<u8>, HIDError>
+ where
+ Dev: io::Read + io::Write + fmt::Debug + FidoDevice,
+ {
+ // TODO(MS): Mandatory sanity checks are missing:
+ // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#u2f-authenticatorMakeCredential-interoperability
+ // If any of the below conditions is not true, platform errors out with CTAP2_ERR_UNSUPPORTED_OPTION.
+ // * pubKeyCredParams must use the ES256 algorithm (-7).
+ // * Options must not include "rk" set to true.
+ // * Options must not include "uv" set to true.
+
+ let is_already_registered = self
+ .exclude_list
+ .iter()
+ // key-handles in CTAP1 are limited to 255 bytes, but are not limited in CTAP2.
+ // Filter out key-handles that are too long (can happen if this is a CTAP2-request,
+ // but the token only speaks CTAP1). If none is found, return an error.
+ .filter(|exclude_handle| exclude_handle.id.len() < 256)
+ .map(|exclude_handle| {
+ let check_command = CheckKeyHandle {
+ key_handle: exclude_handle.id.as_ref(),
+ client_data_wrapper: &self.client_data_wrapper,
+ rp: &self.rp,
+ };
+ let res = dev.send_ctap1(&check_command);
+ res.is_ok()
+ })
+ .any(|x| x == true);
+
+ if is_already_registered {
+ // Now we need to send a dummy registration request, to make the token blink
+ // Spec says "dummy appid and invalid challenge". We use the same, as we do for
+ // making the token blink upon device selection.
+ let msg = dummy_make_credentials_cmd()?;
+ let _ = dev.send_ctap1(&msg); // Ignore answer, return "CrednetialExcluded"
+ return Err(HIDError::Command(CommandError::StatusCode(
+ StatusCode::CredentialExcluded,
+ None,
+ )));
+ }
+
+ let flags = if self.options.ask_user_verification() {
+ U2F_REQUEST_USER_PRESENCE
+ } else {
+ 0
+ };
+
+ let mut register_data = Vec::with_capacity(2 * PARAMETER_SIZE);
+ if self.is_ctap2_request() {
+ register_data.extend_from_slice(self.client_data_hash().as_ref());
+ } else {
+ let decoded = base64::decode_config(
+ &self.client_data_wrapper.client_data.challenge.0,
+ base64::URL_SAFE_NO_PAD,
+ )
+ .map_err(|_| HIDError::DeviceError)?; // We encoded it, so this should never fail
+ register_data.extend_from_slice(&decoded);
+ }
+ register_data.extend_from_slice(self.rp.hash().as_ref());
+ let cmd = U2F_REGISTER;
+ let apdu = CTAP1RequestAPDU::serialize(cmd, flags, &register_data)?;
+
+ Ok(apdu)
+ }
+
+ fn handle_response_ctap1(
+ &self,
+ status: Result<(), ApduErrorStatus>,
+ input: &[u8],
+ ) -> Result<Self::Output, Retryable<HIDError>> {
+ if Err(ApduErrorStatus::ConditionsNotSatisfied) == status {
+ return Err(Retryable::Retry);
+ }
+ if let Err(err) = status {
+ return Err(Retryable::Error(HIDError::ApduStatus(err)));
+ }
+
+ if self.is_ctap2_request() {
+ let parse_register = |input| {
+ let (rest, _) = tag(&[0x05])(input)?;
+ let (rest, public_key) = take(65u8)(rest)?;
+ let (rest, key_handle_len) = be_u8(rest)?;
+ let (rest, key_handle) = take(key_handle_len)(rest)?;
+ Ok((rest, public_key, key_handle))
+ };
+ let (rest, public_key, key_handle) = parse_register(input)
+ .map_err(|e: nom::Err<VerboseError<_>>| {
+ error!("error while parsing registration: {:?}", e);
+ CommandError::Deserializing(DesError::custom("unable to parse registration"))
+ })
+ .map_err(HIDError::Command)
+ .map_err(Retryable::Error)?;
+
+ let cert_and_sig = parse_u2f_der_certificate(rest)
+ .map_err(|e| HIDError::Command(CommandError::Crypto(e)))
+ .map_err(Retryable::Error)?;
+
+ let (x, y) = serialize_key(ECDSACurve::SECP256R1, public_key)
+ .map_err(|e| HIDError::Command(CommandError::Crypto(e.into())))
+ .map_err(Retryable::Error)?;
+ let credential_public_key = COSEKey {
+ alg: COSEAlgorithm::ES256,
+ key: COSEKeyType::EC2(COSEEC2Key {
+ curve: ECDSACurve::SECP256R1,
+ x: x.to_vec(),
+ y: y.to_vec(),
+ }),
+ };
+ let auth_data = AuthenticatorData {
+ rp_id_hash: self.rp.hash(),
+ // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#u2f-authenticatorMakeCredential-interoperability
+ // "Let flags be a byte whose zeroth bit (bit 0, UP) is set, and whose sixth bit
+ // (bit 6, AT) is set, and all other bits are zero (bit zero is the least significant bit)"
+ flags: AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::ATTESTED,
+ counter: 0,
+ credential_data: Some(AttestedCredentialData {
+ aaguid: AAGuid::default(),
+ credential_id: Vec::from(key_handle),
+ credential_public_key,
+ }),
+ extensions: Default::default(),
+ };
+
+ let att_statement = AttestationStatement::FidoU2F(AttestationStatementFidoU2F::new(
+ cert_and_sig.certificate,
+ cert_and_sig.signature,
+ ));
+
+ let attestation_object = AttestationObject {
+ auth_data,
+ att_statement,
+ };
+ let client_data = self.client_data_wrapper.clone();
+
+ Ok(MakeCredentialsResult::CTAP2(
+ attestation_object,
+ client_data,
+ ))
+ } else {
+ Ok(MakeCredentialsResult::CTAP1(input.to_vec()))
+ }
+ }
+}
+
+impl RequestCtap2 for MakeCredentials {
+ type Output = MakeCredentialsResult;
+
+ fn command() -> Command {
+ Command::MakeCredentials
+ }
+
+ fn wire_format<Dev>(&self, _dev: &mut Dev) -> Result<Vec<u8>, HIDError>
+ where
+ Dev: U2FDevice + io::Read + io::Write + fmt::Debug,
+ {
+ Ok(ser::to_vec(&self).map_err(CommandError::Serializing)?)
+ }
+
+ fn handle_response_ctap2<Dev>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError>
+ where
+ Dev: U2FDevice + io::Read + io::Write + fmt::Debug,
+ {
+ if input.is_empty() {
+ return Err(HIDError::Command(CommandError::InputTooSmall));
+ }
+
+ let status: StatusCode = input[0].into();
+ debug!("response status code: {:?}", status);
+ if input.len() > 1 {
+ if status.is_ok() {
+ let attestation = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ let client_data = self.client_data_wrapper.clone();
+ Ok(MakeCredentialsResult::CTAP2(attestation, client_data))
+ } else {
+ let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ Err(HIDError::Command(CommandError::StatusCode(
+ status,
+ Some(data),
+ )))
+ }
+ } else if status.is_ok() {
+ Err(HIDError::Command(CommandError::InputTooSmall))
+ } else {
+ Err(HIDError::Command(CommandError::StatusCode(status, None)))
+ }
+ }
+}
+
+pub(crate) fn dummy_make_credentials_cmd() -> Result<MakeCredentials, HIDError> {
+ MakeCredentials::new(
+ CollectedClientData {
+ webauthn_type: WebauthnType::Create,
+ challenge: Challenge::new(vec![0, 1, 2, 3, 4]),
+ origin: String::new(),
+ cross_origin: false,
+ token_binding: None,
+ },
+ RelyingPartyWrapper::Data(RelyingParty {
+ id: String::from("make.me.blink"),
+ ..Default::default()
+ }),
+ Some(User {
+ id: vec![0],
+ name: Some(String::from("make.me.blink")),
+ ..Default::default()
+ }),
+ vec![PublicKeyCredentialParameters {
+ alg: crate::COSEAlgorithm::ES256,
+ }],
+ vec![],
+ MakeCredentialsOptions::default(),
+ MakeCredentialsExtensions::default(),
+ None,
+ )
+}
+
+#[cfg(test)]
+pub mod test {
+ use super::{MakeCredentials, MakeCredentialsOptions, MakeCredentialsResult};
+ use crate::crypto::{COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, ECDSACurve};
+ use crate::ctap2::attestation::{
+ AAGuid, AttestationCertificate, AttestationObject, AttestationStatement,
+ AttestationStatementFidoU2F, AttestationStatementPacked, AttestedCredentialData,
+ AuthenticatorData, AuthenticatorDataFlags, Signature,
+ };
+ use crate::ctap2::client_data::{Challenge, CollectedClientData, TokenBinding, WebauthnType};
+ use crate::ctap2::commands::{RequestCtap1, RequestCtap2};
+ use crate::ctap2::server::RpIdHash;
+ use crate::ctap2::server::{
+ PublicKeyCredentialParameters, RelyingParty, RelyingPartyWrapper, User,
+ };
+ use crate::transport::device_selector::Device;
+ use crate::transport::hid::HIDDevice;
+ use serde_bytes::ByteBuf;
+
+ #[test]
+ fn test_make_credentials_ctap2() {
+ let req = MakeCredentials::new(
+ CollectedClientData {
+ webauthn_type: WebauthnType::Create,
+ challenge: Challenge::from(vec![0x00, 0x01, 0x02, 0x03]),
+ origin: String::from("example.com"),
+ cross_origin: false,
+ token_binding: Some(TokenBinding::Present(String::from("AAECAw"))),
+ },
+ RelyingPartyWrapper::Data(RelyingParty {
+ id: String::from("example.com"),
+ name: Some(String::from("Acme")),
+ icon: None,
+ }),
+ Some(User {
+ id: base64::decode_config(
+ "MIIBkzCCATigAwIBAjCCAZMwggE4oAMCAQIwggGTMII=",
+ base64::URL_SAFE_NO_PAD,
+ )
+ .unwrap(),
+ icon: Some("https://pics.example.com/00/p/aBjjjpqPb.png".to_string()),
+ name: Some(String::from("johnpsmith@example.com")),
+ display_name: Some(String::from("John P. Smith")),
+ }),
+ vec![
+ PublicKeyCredentialParameters {
+ alg: COSEAlgorithm::ES256,
+ },
+ PublicKeyCredentialParameters {
+ alg: COSEAlgorithm::RS256,
+ },
+ ],
+ Vec::new(),
+ MakeCredentialsOptions {
+ resident_key: Some(true),
+ user_verification: None,
+ },
+ Default::default(),
+ None,
+ )
+ .expect("Failed to create MakeCredentials");
+
+ let mut device = Device::new("commands/make_credentials").unwrap(); // not really used (all functions ignore it)
+ let req_serialized = req
+ .wire_format(&mut device)
+ .expect("Failed to serialize MakeCredentials request");
+ assert_eq!(req_serialized, MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP2);
+ let (attestation_object, _collected_client_data) = match req
+ .handle_response_ctap2(&mut device, &MAKE_CREDENTIALS_SAMPLE_RESPONSE_CTAP2)
+ .expect("Failed to handle CTAP2 response")
+ {
+ MakeCredentialsResult::CTAP2(attestation_object, _collected_client_data) => {
+ (attestation_object, _collected_client_data)
+ }
+ _ => panic!("Got CTAP1 Result, but CTAP2 expected"),
+ };
+
+ let expected = AttestationObject {
+ auth_data: AuthenticatorData {
+ rp_id_hash: RpIdHash::from(&[
+ 0xc2, 0x89, 0xc5, 0xca, 0x9b, 0x04, 0x60, 0xf9, 0x34, 0x6a, 0xb4, 0xe4, 0x2d,
+ 0x84, 0x27, 0x43, 0x40, 0x4d, 0x31, 0xf4, 0x84, 0x68, 0x25, 0xa6, 0xd0, 0x65,
+ 0xbe, 0x59, 0x7a, 0x87, 0x5, 0x1d,
+ ])
+ .unwrap(),
+ flags: AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::ATTESTED,
+ counter: 11,
+ credential_data: Some(AttestedCredentialData {
+ aaguid: AAGuid::from(&[
+ 0xf8, 0xa0, 0x11, 0xf3, 0x8c, 0x0a, 0x4d, 0x15, 0x80, 0x06, 0x17, 0x11,
+ 0x1f, 0x9e, 0xdc, 0x7d,
+ ])
+ .unwrap(),
+ credential_id: vec![
+ 0x89, 0x59, 0xce, 0xad, 0x5b, 0x5c, 0x48, 0x16, 0x4e, 0x8a, 0xbc, 0xd6,
+ 0xd9, 0x43, 0x5c, 0x6f,
+ ],
+ credential_public_key: COSEKey {
+ alg: COSEAlgorithm::ES256,
+ key: COSEKeyType::EC2(COSEEC2Key {
+ curve: ECDSACurve::SECP256R1,
+ x: vec![
+ 0xA5, 0xFD, 0x5C, 0xE1, 0xB1, 0xC4, 0x58, 0xC5, 0x30, 0xA5, 0x4F,
+ 0xA6, 0x1B, 0x31, 0xBF, 0x6B, 0x04, 0xBE, 0x8B, 0x97, 0xAF, 0xDE,
+ 0x54, 0xDD, 0x8C, 0xBB, 0x69, 0x27, 0x5A, 0x8A, 0x1B, 0xE1,
+ ],
+ y: vec![
+ 0xFA, 0x3A, 0x32, 0x31, 0xDD, 0x9D, 0xEE, 0xD9, 0xD1, 0x89, 0x7B,
+ 0xE5, 0xA6, 0x22, 0x8C, 0x59, 0x50, 0x1E, 0x4B, 0xCD, 0x12, 0x97,
+ 0x5D, 0x3D, 0xFF, 0x73, 0x0F, 0x01, 0x27, 0x8E, 0xA6, 0x1C,
+ ],
+ }),
+ },
+ }),
+ extensions: Default::default(),
+ },
+ att_statement: AttestationStatement::Packed(AttestationStatementPacked {
+ alg: COSEAlgorithm::ES256,
+ sig: Signature(ByteBuf::from([
+ 0x30, 0x45, 0x02, 0x20, 0x13, 0xf7, 0x3c, 0x5d, 0x9d, 0x53, 0x0e, 0x8c, 0xc1,
+ 0x5c, 0xc9, 0xbd, 0x96, 0xad, 0x58, 0x6d, 0x39, 0x36, 0x64, 0xe4, 0x62, 0xd5,
+ 0xf0, 0x56, 0x12, 0x35, 0xe6, 0x35, 0x0f, 0x2b, 0x72, 0x89, 0x02, 0x21, 0x00,
+ 0x90, 0x35, 0x7f, 0xf9, 0x10, 0xcc, 0xb5, 0x6a, 0xc5, 0xb5, 0x96, 0x51, 0x19,
+ 0x48, 0x58, 0x1c, 0x8f, 0xdd, 0xb4, 0xa2, 0xb7, 0x99, 0x59, 0x94, 0x80, 0x78,
+ 0xb0, 0x9f, 0x4b, 0xdc, 0x62, 0x29,
+ ])),
+ attestation_cert: vec![AttestationCertificate(vec![
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02,
+ 0x02, 0x09, 0x00, 0x85, 0x9b, 0x72, 0x6c, 0xb2, 0x4b, 0x4c, 0x29, 0x30, 0x0a,
+ 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30, 0x47, 0x31,
+ 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31,
+ 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75, 0x62,
+ 0x69, 0x63, 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, 0x06,
+ 0x03, 0x55, 0x04, 0x0b, 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74,
+ 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74,
+ 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x1e, 0x17, 0x0d, 0x31, 0x36, 0x31, 0x32,
+ 0x30, 0x34, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x17, 0x0d, 0x32, 0x36,
+ 0x31, 0x32, 0x30, 0x32, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x30, 0x47,
+ 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53,
+ 0x31, 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75,
+ 0x62, 0x69, 0x63, 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20,
+ 0x06, 0x03, 0x55, 0x04, 0x0b, 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e,
+ 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x74, 0x74, 0x65, 0x73,
+ 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a,
+ 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d,
+ 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xad, 0x11, 0xeb, 0x0e, 0x88, 0x52,
+ 0xe5, 0x3a, 0xd5, 0xdf, 0xed, 0x86, 0xb4, 0x1e, 0x61, 0x34, 0xa1, 0x8e, 0xc4,
+ 0xe1, 0xaf, 0x8f, 0x22, 0x1a, 0x3c, 0x7d, 0x6e, 0x63, 0x6c, 0x80, 0xea, 0x13,
+ 0xc3, 0xd5, 0x04, 0xff, 0x2e, 0x76, 0x21, 0x1b, 0xb4, 0x45, 0x25, 0xb1, 0x96,
+ 0xc4, 0x4c, 0xb4, 0x84, 0x99, 0x79, 0xcf, 0x6f, 0x89, 0x6e, 0xcd, 0x2b, 0xb8,
+ 0x60, 0xde, 0x1b, 0xf4, 0x37, 0x6b, 0xa3, 0x0d, 0x30, 0x0b, 0x30, 0x09, 0x06,
+ 0x03, 0x55, 0x1d, 0x13, 0x04, 0x02, 0x30, 0x00, 0x30, 0x0a, 0x06, 0x08, 0x2a,
+ 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x49, 0x00, 0x30, 0x46, 0x02,
+ 0x21, 0x00, 0xe9, 0xa3, 0x9f, 0x1b, 0x03, 0x19, 0x75, 0x25, 0xf7, 0x37, 0x3e,
+ 0x10, 0xce, 0x77, 0xe7, 0x80, 0x21, 0x73, 0x1b, 0x94, 0xd0, 0xc0, 0x3f, 0x3f,
+ 0xda, 0x1f, 0xd2, 0x2d, 0xb3, 0xd0, 0x30, 0xe7, 0x02, 0x21, 0x00, 0xc4, 0xfa,
+ 0xec, 0x34, 0x45, 0xa8, 0x20, 0xcf, 0x43, 0x12, 0x9c, 0xdb, 0x00, 0xaa, 0xbe,
+ 0xfd, 0x9a, 0xe2, 0xd8, 0x74, 0xf9, 0xc5, 0xd3, 0x43, 0xcb, 0x2f, 0x11, 0x3d,
+ 0xa2, 0x37, 0x23, 0xf3,
+ ])],
+ }),
+ };
+
+ assert_eq!(attestation_object, expected);
+ }
+
+ #[test]
+ fn test_make_credentials_ctap1() {
+ let req = MakeCredentials::new(
+ CollectedClientData {
+ webauthn_type: WebauthnType::Create,
+ challenge: Challenge::new(vec![0x00, 0x01, 0x02, 0x03]),
+ origin: String::from("example.com"),
+ cross_origin: false,
+ token_binding: Some(TokenBinding::Present(String::from("AAECAw"))),
+ },
+ RelyingPartyWrapper::Data(RelyingParty {
+ id: String::from("example.com"),
+ name: Some(String::from("Acme")),
+ icon: None,
+ }),
+ Some(User {
+ id: base64::decode_config(
+ "MIIBkzCCATigAwIBAjCCAZMwggE4oAMCAQIwggGTMII=",
+ base64::URL_SAFE_NO_PAD,
+ )
+ .unwrap(),
+ icon: Some("https://pics.example.com/00/p/aBjjjpqPb.png".to_string()),
+ name: Some(String::from("johnpsmith@example.com")),
+ display_name: Some(String::from("John P. Smith")),
+ }),
+ vec![
+ PublicKeyCredentialParameters {
+ alg: COSEAlgorithm::ES256,
+ },
+ PublicKeyCredentialParameters {
+ alg: COSEAlgorithm::RS256,
+ },
+ ],
+ Vec::new(),
+ MakeCredentialsOptions {
+ resident_key: Some(true),
+ user_verification: None,
+ },
+ Default::default(),
+ None,
+ )
+ .expect("Failed to create MakeCredentials");
+
+ let mut device = Device::new("commands/make_credentials").unwrap(); // not really used (all functions ignore it)
+ let req_serialized = req
+ .ctap1_format(&mut device)
+ .expect("Failed to serialize MakeCredentials request");
+ assert_eq!(
+ req_serialized, MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP1,
+ "\nGot: {:X?}\nExpected: {:X?}",
+ req_serialized, MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP1
+ );
+ let (attestation_object, _collected_client_data) = match req
+ .handle_response_ctap1(Ok(()), &MAKE_CREDENTIALS_SAMPLE_RESPONSE_CTAP1)
+ .expect("Failed to handle CTAP1 response")
+ {
+ MakeCredentialsResult::CTAP2(attestation_object, _collected_client_data) => {
+ (attestation_object, _collected_client_data)
+ }
+ _ => panic!("Got CTAP1 Result, but CTAP2 expected"),
+ };
+
+ let expected = AttestationObject {
+ auth_data: AuthenticatorData {
+ rp_id_hash: RpIdHash::from(&[
+ 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80,
+ 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2,
+ 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47,
+ ])
+ .unwrap(),
+ flags: AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::ATTESTED,
+ counter: 0,
+ credential_data: Some(AttestedCredentialData {
+ aaguid: AAGuid::default(),
+ credential_id: vec![
+ 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26,
+ 0x35, 0xEF, 0xAA, 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3,
+ 0x71, 0x7D, 0xA4, 0x85, 0x34, 0xC8, 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94,
+ 0x5F, 0x50, 0xB5, 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD, 0x39, 0x6B, 0x64,
+ 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08,
+ 0xFE, 0x42, 0x00, 0x38,
+ ],
+ credential_public_key: COSEKey {
+ alg: COSEAlgorithm::ES256,
+ key: COSEKeyType::EC2(COSEEC2Key {
+ curve: ECDSACurve::SECP256R1,
+ x: vec![
+ 0xE8, 0x76, 0x25, 0x89, 0x6E, 0xE4, 0xE4, 0x6D, 0xC0, 0x32, 0x76,
+ 0x6E, 0x80, 0x87, 0x96, 0x2F, 0x36, 0xDF, 0x9D, 0xFE, 0x8B, 0x56,
+ 0x7F, 0x37, 0x63, 0x01, 0x5B, 0x19, 0x90, 0xA6, 0x0E, 0x14,
+ ],
+ y: vec![
+ 0x27, 0xDE, 0x61, 0x2D, 0x66, 0x41, 0x8B, 0xDA, 0x19, 0x50, 0x58,
+ 0x1E, 0xBC, 0x5C, 0x8C, 0x1D, 0xAD, 0x71, 0x0C, 0xB1, 0x4C, 0x22,
+ 0xF8, 0xC9, 0x70, 0x45, 0xF4, 0x61, 0x2F, 0xB2, 0x0C, 0x91,
+ ],
+ }),
+ },
+ }),
+ extensions: Default::default(),
+ },
+ att_statement: AttestationStatement::FidoU2F(AttestationStatementFidoU2F {
+ sig: Signature(ByteBuf::from([
+ 0x30, 0x45, 0x02, 0x20, 0x32, 0x47, 0x79, 0xC6, 0x8F, 0x33, 0x80, 0x28, 0x8A,
+ 0x11, 0x97, 0xB6, 0x09, 0x5F, 0x7A, 0x6E, 0xB9, 0xB1, 0xB1, 0xC1, 0x27, 0xF6,
+ 0x6A, 0xE1, 0x2A, 0x99, 0xFE, 0x85, 0x32, 0xEC, 0x23, 0xB9, 0x02, 0x21, 0x00,
+ 0xE3, 0x95, 0x16, 0xAC, 0x4D, 0x61, 0xEE, 0x64, 0x04, 0x4D, 0x50, 0xB4, 0x15,
+ 0xA6, 0xA4, 0xD4, 0xD8, 0x4B, 0xA6, 0xD8, 0x95, 0xCB, 0x5A, 0xB7, 0xA1, 0xAA,
+ 0x7D, 0x08, 0x1D, 0xE3, 0x41, 0xFA,
+ ])),
+ attestation_cert: vec![AttestationCertificate(vec![
+ 0x30, 0x82, 0x02, 0x4A, 0x30, 0x82, 0x01, 0x32, 0xA0, 0x03, 0x02, 0x01, 0x02,
+ 0x02, 0x04, 0x04, 0x6C, 0x88, 0x22, 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48,
+ 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B, 0x05, 0x00, 0x30, 0x2E, 0x31, 0x2C, 0x30,
+ 0x2A, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x23, 0x59, 0x75, 0x62, 0x69, 0x63,
+ 0x6F, 0x20, 0x55, 0x32, 0x46, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41,
+ 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6C, 0x20, 0x34, 0x35, 0x37, 0x32, 0x30,
+ 0x30, 0x36, 0x33, 0x31, 0x30, 0x20, 0x17, 0x0D, 0x31, 0x34, 0x30, 0x38, 0x30,
+ 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5A, 0x18, 0x0F, 0x32, 0x30, 0x35,
+ 0x30, 0x30, 0x39, 0x30, 0x34, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5A, 0x30,
+ 0x2C, 0x31, 0x2A, 0x30, 0x28, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C, 0x21, 0x59,
+ 0x75, 0x62, 0x69, 0x63, 0x6F, 0x20, 0x55, 0x32, 0x46, 0x20, 0x45, 0x45, 0x20,
+ 0x53, 0x65, 0x72, 0x69, 0x61, 0x6C, 0x20, 0x32, 0x34, 0x39, 0x31, 0x38, 0x32,
+ 0x33, 0x32, 0x34, 0x37, 0x37, 0x30, 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2A,
+ 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01, 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D,
+ 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0x3C, 0xCA, 0xB9, 0x2C, 0xCB, 0x97,
+ 0x28, 0x7E, 0xE8, 0xE6, 0x39, 0x43, 0x7E, 0x21, 0xFC, 0xD6, 0xB6, 0xF1, 0x65,
+ 0xB2, 0xD5, 0xA3, 0xF3, 0xDB, 0x13, 0x1D, 0x31, 0xC1, 0x6B, 0x74, 0x2B, 0xB4,
+ 0x76, 0xD8, 0xD1, 0xE9, 0x90, 0x80, 0xEB, 0x54, 0x6C, 0x9B, 0xBD, 0xF5, 0x56,
+ 0xE6, 0x21, 0x0F, 0xD4, 0x27, 0x85, 0x89, 0x9E, 0x78, 0xCC, 0x58, 0x9E, 0xBE,
+ 0x31, 0x0F, 0x6C, 0xDB, 0x9F, 0xF4, 0xA3, 0x3B, 0x30, 0x39, 0x30, 0x22, 0x06,
+ 0x09, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0xC4, 0x0A, 0x02, 0x04, 0x15, 0x31,
+ 0x2E, 0x33, 0x2E, 0x36, 0x2E, 0x31, 0x2E, 0x34, 0x2E, 0x31, 0x2E, 0x34, 0x31,
+ 0x34, 0x38, 0x32, 0x2E, 0x31, 0x2E, 0x32, 0x30, 0x13, 0x06, 0x0B, 0x2B, 0x06,
+ 0x01, 0x04, 0x01, 0x82, 0xE5, 0x1C, 0x02, 0x01, 0x01, 0x04, 0x04, 0x03, 0x02,
+ 0x04, 0x30, 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01,
+ 0x01, 0x0B, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, 0x00, 0x9F, 0x9B, 0x05, 0x22,
+ 0x48, 0xBC, 0x4C, 0xF4, 0x2C, 0xC5, 0x99, 0x1F, 0xCA, 0xAB, 0xAC, 0x9B, 0x65,
+ 0x1B, 0xBE, 0x5B, 0xDC, 0xDC, 0x8E, 0xF0, 0xAD, 0x2C, 0x1C, 0x1F, 0xFB, 0x36,
+ 0xD1, 0x87, 0x15, 0xD4, 0x2E, 0x78, 0xB2, 0x49, 0x22, 0x4F, 0x92, 0xC7, 0xE6,
+ 0xE7, 0xA0, 0x5C, 0x49, 0xF0, 0xE7, 0xE4, 0xC8, 0x81, 0xBF, 0x2E, 0x94, 0xF4,
+ 0x5E, 0x4A, 0x21, 0x83, 0x3D, 0x74, 0x56, 0x85, 0x1D, 0x0F, 0x6C, 0x14, 0x5A,
+ 0x29, 0x54, 0x0C, 0x87, 0x4F, 0x30, 0x92, 0xC9, 0x34, 0xB4, 0x3D, 0x22, 0x2B,
+ 0x89, 0x62, 0xC0, 0xF4, 0x10, 0xCE, 0xF1, 0xDB, 0x75, 0x89, 0x2A, 0xF1, 0x16,
+ 0xB4, 0x4A, 0x96, 0xF5, 0xD3, 0x5A, 0xDE, 0xA3, 0x82, 0x2F, 0xC7, 0x14, 0x6F,
+ 0x60, 0x04, 0x38, 0x5B, 0xCB, 0x69, 0xB6, 0x5C, 0x99, 0xE7, 0xEB, 0x69, 0x19,
+ 0x78, 0x67, 0x03, 0xC0, 0xD8, 0xCD, 0x41, 0xE8, 0xF7, 0x5C, 0xCA, 0x44, 0xAA,
+ 0x8A, 0xB7, 0x25, 0xAD, 0x8E, 0x79, 0x9F, 0xF3, 0xA8, 0x69, 0x6A, 0x6F, 0x1B,
+ 0x26, 0x56, 0xE6, 0x31, 0xB1, 0xE4, 0x01, 0x83, 0xC0, 0x8F, 0xDA, 0x53, 0xFA,
+ 0x4A, 0x8F, 0x85, 0xA0, 0x56, 0x93, 0x94, 0x4A, 0xE1, 0x79, 0xA1, 0x33, 0x9D,
+ 0x00, 0x2D, 0x15, 0xCA, 0xBD, 0x81, 0x00, 0x90, 0xEC, 0x72, 0x2E, 0xF5, 0xDE,
+ 0xF9, 0x96, 0x5A, 0x37, 0x1D, 0x41, 0x5D, 0x62, 0x4B, 0x68, 0xA2, 0x70, 0x7C,
+ 0xAD, 0x97, 0xBC, 0xDD, 0x17, 0x85, 0xAF, 0x97, 0xE2, 0x58, 0xF3, 0x3D, 0xF5,
+ 0x6A, 0x03, 0x1A, 0xA0, 0x35, 0x6D, 0x8E, 0x8D, 0x5E, 0xBC, 0xAD, 0xC7, 0x4E,
+ 0x07, 0x16, 0x36, 0xC6, 0xB1, 0x10, 0xAC, 0xE5, 0xCC, 0x9B, 0x90, 0xDF, 0xEA,
+ 0xCA, 0xE6, 0x40, 0xFF, 0x1B, 0xB0, 0xF1, 0xFE, 0x5D, 0xB4, 0xEF, 0xF7, 0xA9,
+ 0x5F, 0x06, 0x07, 0x33, 0xF5,
+ ])],
+ }),
+ };
+
+ assert_eq!(attestation_object, expected);
+ }
+
+ #[rustfmt::skip]
+ pub const MAKE_CREDENTIALS_SAMPLE_RESPONSE_CTAP2: [u8; 660] = [
+ 0x00, // status = success
+ 0xa3, // map(3)
+ 0x01, // unsigned(1)
+ 0x66, // text(6)
+ 0x70, 0x61, 0x63, 0x6b, 0x65, 0x64, // "packed"
+ 0x02, // unsigned(2)
+ 0x58, 0x94, // bytes(148)
+ // authData
+ 0xc2, 0x89, 0xc5, 0xca, 0x9b, 0x04, 0x60, 0xf9, 0x34, 0x6a, 0xb4, 0xe4, 0x2d, 0x84, 0x27, // rp_id_hash
+ 0x43, 0x40, 0x4d, 0x31, 0xf4, 0x84, 0x68, 0x25, 0xa6, 0xd0, 0x65, 0xbe, 0x59, 0x7a, 0x87, // rp_id_hash
+ 0x05, 0x1d, // rp_id_hash
+ 0x41, // authData Flags
+ 0x00, 0x00, 0x00, 0x0b, // authData counter
+ 0xf8, 0xa0, 0x11, 0xf3, 0x8c, 0x0a, 0x4d, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1f, 0x9e, 0xdc, 0x7d, // AAGUID
+ 0x00, 0x10, // credential id length
+ 0x89, 0x59, 0xce, 0xad, 0x5b, 0x5c, 0x48, 0x16, 0x4e, 0x8a, 0xbc, 0xd6, 0xd9, 0x43, 0x5c, 0x6f, // credential id
+ // credential public key
+ 0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20, 0xa5, 0xfd, 0x5c, 0xe1, 0xb1, 0xc4,
+ 0x58, 0xc5, 0x30, 0xa5, 0x4f, 0xa6, 0x1b, 0x31, 0xbf, 0x6b, 0x04, 0xbe, 0x8b, 0x97, 0xaf, 0xde,
+ 0x54, 0xdd, 0x8c, 0xbb, 0x69, 0x27, 0x5a, 0x8a, 0x1b, 0xe1, 0x22, 0x58, 0x20, 0xfa, 0x3a, 0x32,
+ 0x31, 0xdd, 0x9d, 0xee, 0xd9, 0xd1, 0x89, 0x7b, 0xe5, 0xa6, 0x22, 0x8c, 0x59, 0x50, 0x1e, 0x4b,
+ 0xcd, 0x12, 0x97, 0x5d, 0x3d, 0xff, 0x73, 0x0f, 0x01, 0x27, 0x8e, 0xa6, 0x1c,
+ 0x03, // unsigned(3)
+ 0xa3, // map(3)
+ 0x63, // text(3)
+ 0x61, 0x6c, 0x67, // "alg"
+ 0x26, // -7 (ES256)
+ 0x63, // text(3)
+ 0x73, 0x69, 0x67, // "sig"
+ 0x58, 0x47, // bytes(71)
+ 0x30, 0x45, 0x02, 0x20, 0x13, 0xf7, 0x3c, 0x5d, 0x9d, 0x53, 0x0e, 0x8c, 0xc1, 0x5c, 0xc9, // signature
+ 0xbd, 0x96, 0xad, 0x58, 0x6d, 0x39, 0x36, 0x64, 0xe4, 0x62, 0xd5, 0xf0, 0x56, 0x12, 0x35, // ..
+ 0xe6, 0x35, 0x0f, 0x2b, 0x72, 0x89, 0x02, 0x21, 0x00, 0x90, 0x35, 0x7f, 0xf9, 0x10, 0xcc, // ..
+ 0xb5, 0x6a, 0xc5, 0xb5, 0x96, 0x51, 0x19, 0x48, 0x58, 0x1c, 0x8f, 0xdd, 0xb4, 0xa2, 0xb7, // ..
+ 0x99, 0x59, 0x94, 0x80, 0x78, 0xb0, 0x9f, 0x4b, 0xdc, 0x62, 0x29, // ..
+ 0x63, // text(3)
+ 0x78, 0x35, 0x63, // "x5c"
+ 0x81, // array(1)
+ 0x59, 0x01, 0x97, // bytes(407)
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, //certificate...
+ 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x09, 0x00, 0x85, 0x9b, 0x72, 0x6c, 0xb2, 0x4b,
+ 0x4c, 0x29, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30,
+ 0x47, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31,
+ 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75, 0x62, 0x69, 0x63,
+ 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, 0x06, 0x03, 0x55, 0x04, 0x0b,
+ 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72,
+ 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x1e, 0x17,
+ 0x0d, 0x31, 0x36, 0x31, 0x32, 0x30, 0x34, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x17,
+ 0x0d, 0x32, 0x36, 0x31, 0x32, 0x30, 0x32, 0x31, 0x31, 0x35, 0x35, 0x30, 0x30, 0x5a, 0x30,
+ 0x47, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31,
+ 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0b, 0x59, 0x75, 0x62, 0x69, 0x63,
+ 0x6f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, 0x06, 0x03, 0x55, 0x04, 0x0b,
+ 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72,
+ 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x59, 0x30,
+ 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48,
+ 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xad, 0x11, 0xeb, 0x0e, 0x88, 0x52,
+ 0xe5, 0x3a, 0xd5, 0xdf, 0xed, 0x86, 0xb4, 0x1e, 0x61, 0x34, 0xa1, 0x8e, 0xc4, 0xe1, 0xaf,
+ 0x8f, 0x22, 0x1a, 0x3c, 0x7d, 0x6e, 0x63, 0x6c, 0x80, 0xea, 0x13, 0xc3, 0xd5, 0x04, 0xff,
+ 0x2e, 0x76, 0x21, 0x1b, 0xb4, 0x45, 0x25, 0xb1, 0x96, 0xc4, 0x4c, 0xb4, 0x84, 0x99, 0x79,
+ 0xcf, 0x6f, 0x89, 0x6e, 0xcd, 0x2b, 0xb8, 0x60, 0xde, 0x1b, 0xf4, 0x37, 0x6b, 0xa3, 0x0d,
+ 0x30, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x04, 0x02, 0x30, 0x00, 0x30, 0x0a,
+ 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x49, 0x00, 0x30, 0x46,
+ 0x02, 0x21, 0x00, 0xe9, 0xa3, 0x9f, 0x1b, 0x03, 0x19, 0x75, 0x25, 0xf7, 0x37, 0x3e, 0x10,
+ 0xce, 0x77, 0xe7, 0x80, 0x21, 0x73, 0x1b, 0x94, 0xd0, 0xc0, 0x3f, 0x3f, 0xda, 0x1f, 0xd2,
+ 0x2d, 0xb3, 0xd0, 0x30, 0xe7, 0x02, 0x21, 0x00, 0xc4, 0xfa, 0xec, 0x34, 0x45, 0xa8, 0x20,
+ 0xcf, 0x43, 0x12, 0x9c, 0xdb, 0x00, 0xaa, 0xbe, 0xfd, 0x9a, 0xe2, 0xd8, 0x74, 0xf9, 0xc5,
+ 0xd3, 0x43, 0xcb, 0x2f, 0x11, 0x3d, 0xa2, 0x37, 0x23, 0xf3,
+ ];
+
+ #[rustfmt::skip]
+ pub const MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP2: [u8; 260] = [
+ // NOTE: This has been taken from CTAP2.0 spec, but the clientDataHash has been replaced
+ // to be able to operate with known values for CollectedClientData (spec doesn't say
+ // what values led to the provided example hash (see client_data.rs))
+ 0xa5, // map(5)
+ 0x01, // unsigned(1) - clientDataHash
+ 0x58, 0x20, // bytes(32)
+ 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, // hash
+ 0x32, 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, // hash
+ 0x10, 0x87, 0x54, 0xc3, 0x2d, 0x80, // hash
+ 0x02, // unsigned(2) - rp
+ 0xa2, // map(2) Replace line below with this one, once RelyingParty supports "name"
+ 0x62, // text(2)
+ 0x69, 0x64, // "id"
+ 0x6b, // text(11)
+ 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, // "example.com"
+ 0x64, // text(4)
+ 0x6e, 0x61, 0x6d, 0x65, // "name"
+ 0x64, // text(4)
+ 0x41, 0x63, 0x6d, 0x65, // "Acme"
+ 0x03, // unsigned(3) - user
+ 0xa4, // map(4)
+ 0x62, // text(2)
+ 0x69, 0x64, // "id"
+ 0x58, 0x20, // bytes(32)
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, // userid
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, // ...
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, // ...
+ 0x64, // text(4)
+ 0x69, 0x63, 0x6f, 0x6e, // "icon"
+ 0x78, 0x2b, // text(43)
+ 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, // "https://pics.example.com/00/p/aBjjjpqPb.png"
+ 0x2f, 0x70, 0x69, 0x63, 0x73, 0x2e, 0x65, 0x78, // ..
+ 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x30, 0x30, 0x2f, 0x70, // ..
+ 0x2f, 0x61, 0x42, 0x6a, 0x6a, 0x6a, 0x70, 0x71, 0x50, 0x62, 0x2e, 0x70, 0x6e, 0x67, // ..
+ 0x64, // text(4)
+ 0x6e, 0x61, 0x6d, 0x65, // "name"
+ 0x76, // text(22)
+ 0x6a, 0x6f, 0x68, 0x6e, 0x70, 0x73, 0x6d, 0x69, 0x74, // "johnpsmith@example.com"
+ 0x68, 0x40, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, // ...
+ 0x6b, // text(11)
+ 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, // "displayName"
+ 0x6d, // text(13)
+ 0x4a, 0x6f, 0x68, 0x6e, 0x20, 0x50, 0x2e, 0x20, 0x53, 0x6d, 0x69, 0x74, 0x68, // "John P. Smith"
+ 0x04, // unsigned(4) - pubKeyCredParams
+ 0x82, // array(2)
+ 0xa2, // map(2)
+ 0x63, // text(3)
+ 0x61, 0x6c, 0x67, // "alg"
+ 0x26, // -7 (ES256)
+ 0x64, // text(4)
+ 0x74, 0x79, 0x70, 0x65, // "type"
+ 0x6a, // text(10)
+ 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // "public-key"
+ 0xa2, // map(2)
+ 0x63, // text(3)
+ 0x61, 0x6c, 0x67, // "alg"
+ 0x39, 0x01, 0x00, // -257 (RS256)
+ 0x64, // text(4)
+ 0x74, 0x79, 0x70, 0x65, // "type"
+ 0x6a, // text(10)
+ 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // "public-key"
+ // TODO(MS): Options seem to be parsed differently than in the example here.
+ 0x07, // unsigned(7) - options
+ 0xa1, // map(1)
+ 0x62, // text(2)
+ 0x72, 0x6b, // "rk"
+ 0xf5, // primitive(21)
+ ];
+
+ pub const MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP1: [u8; 73] = [
+ // CBOR Header
+ 0x0, // CLA
+ 0x1, // INS U2F_Register
+ 0x0, // P1 Flags
+ 0x0, // P2
+ 0x0, 0x0, 0x40, // Lc
+ // NOTE: This has been taken from CTAP2.0 spec, but the clientDataHash has been replaced
+ // to be able to operate with known values for CollectedClientData (spec doesn't say
+ // what values led to the provided example hash)
+ // clientDataHash:
+ 0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11, // hash
+ 0x32, 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f, // hash
+ 0x10, 0x87, 0x54, 0xc3, 0x2d, 0x80, // hash
+ // rpIdHash:
+ 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, 0x34, 0xE2,
+ 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, 0x12, 0x55, 0x86, 0xCE,
+ 0x19, 0x47, // ..
+ // Le (Ne=65536):
+ 0x0, 0x0,
+ ];
+
+ pub const MAKE_CREDENTIALS_SAMPLE_RESPONSE_CTAP1: [u8; 792] = [
+ 0x05, // Reserved Byte (1 Byte)
+ // User Public Key (65 Bytes)
+ 0x04, 0xE8, 0x76, 0x25, 0x89, 0x6E, 0xE4, 0xE4, 0x6D, 0xC0, 0x32, 0x76, 0x6E, 0x80, 0x87,
+ 0x96, 0x2F, 0x36, 0xDF, 0x9D, 0xFE, 0x8B, 0x56, 0x7F, 0x37, 0x63, 0x01, 0x5B, 0x19, 0x90,
+ 0xA6, 0x0E, 0x14, 0x27, 0xDE, 0x61, 0x2D, 0x66, 0x41, 0x8B, 0xDA, 0x19, 0x50, 0x58, 0x1E,
+ 0xBC, 0x5C, 0x8C, 0x1D, 0xAD, 0x71, 0x0C, 0xB1, 0x4C, 0x22, 0xF8, 0xC9, 0x70, 0x45, 0xF4,
+ 0x61, 0x2F, 0xB2, 0x0C, 0x91, // ...
+ 0x40, // Key Handle Length (1 Byte)
+ // Key Handle (Key Handle Length Bytes)
+ 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35, 0xEF, 0xAA,
+ 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D, 0xA4, 0x85, 0x34, 0xC8,
+ 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5, 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD,
+ 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08,
+ 0xFE, 0x42, 0x00, 0x38, // ...
+ // X.509 Cert (Variable length Cert)
+ 0x30, 0x82, 0x02, 0x4A, 0x30, 0x82, 0x01, 0x32, 0xA0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x04,
+ 0x04, 0x6C, 0x88, 0x22, 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01,
+ 0x01, 0x0B, 0x05, 0x00, 0x30, 0x2E, 0x31, 0x2C, 0x30, 0x2A, 0x06, 0x03, 0x55, 0x04, 0x03,
+ 0x13, 0x23, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6F, 0x20, 0x55, 0x32, 0x46, 0x20, 0x52, 0x6F,
+ 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6C, 0x20, 0x34, 0x35,
+ 0x37, 0x32, 0x30, 0x30, 0x36, 0x33, 0x31, 0x30, 0x20, 0x17, 0x0D, 0x31, 0x34, 0x30, 0x38,
+ 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5A, 0x18, 0x0F, 0x32, 0x30, 0x35, 0x30,
+ 0x30, 0x39, 0x30, 0x34, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5A, 0x30, 0x2C, 0x31, 0x2A,
+ 0x30, 0x28, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C, 0x21, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6F,
+ 0x20, 0x55, 0x32, 0x46, 0x20, 0x45, 0x45, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6C, 0x20,
+ 0x32, 0x34, 0x39, 0x31, 0x38, 0x32, 0x33, 0x32, 0x34, 0x37, 0x37, 0x30, 0x30, 0x59, 0x30,
+ 0x13, 0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01, 0x06, 0x08, 0x2A, 0x86, 0x48,
+ 0xCE, 0x3D, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0x3C, 0xCA, 0xB9, 0x2C, 0xCB, 0x97,
+ 0x28, 0x7E, 0xE8, 0xE6, 0x39, 0x43, 0x7E, 0x21, 0xFC, 0xD6, 0xB6, 0xF1, 0x65, 0xB2, 0xD5,
+ 0xA3, 0xF3, 0xDB, 0x13, 0x1D, 0x31, 0xC1, 0x6B, 0x74, 0x2B, 0xB4, 0x76, 0xD8, 0xD1, 0xE9,
+ 0x90, 0x80, 0xEB, 0x54, 0x6C, 0x9B, 0xBD, 0xF5, 0x56, 0xE6, 0x21, 0x0F, 0xD4, 0x27, 0x85,
+ 0x89, 0x9E, 0x78, 0xCC, 0x58, 0x9E, 0xBE, 0x31, 0x0F, 0x6C, 0xDB, 0x9F, 0xF4, 0xA3, 0x3B,
+ 0x30, 0x39, 0x30, 0x22, 0x06, 0x09, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0xC4, 0x0A, 0x02,
+ 0x04, 0x15, 0x31, 0x2E, 0x33, 0x2E, 0x36, 0x2E, 0x31, 0x2E, 0x34, 0x2E, 0x31, 0x2E, 0x34,
+ 0x31, 0x34, 0x38, 0x32, 0x2E, 0x31, 0x2E, 0x32, 0x30, 0x13, 0x06, 0x0B, 0x2B, 0x06, 0x01,
+ 0x04, 0x01, 0x82, 0xE5, 0x1C, 0x02, 0x01, 0x01, 0x04, 0x04, 0x03, 0x02, 0x04, 0x30, 0x30,
+ 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B, 0x05, 0x00, 0x03,
+ 0x82, 0x01, 0x01, 0x00, 0x9F, 0x9B, 0x05, 0x22, 0x48, 0xBC, 0x4C, 0xF4, 0x2C, 0xC5, 0x99,
+ 0x1F, 0xCA, 0xAB, 0xAC, 0x9B, 0x65, 0x1B, 0xBE, 0x5B, 0xDC, 0xDC, 0x8E, 0xF0, 0xAD, 0x2C,
+ 0x1C, 0x1F, 0xFB, 0x36, 0xD1, 0x87, 0x15, 0xD4, 0x2E, 0x78, 0xB2, 0x49, 0x22, 0x4F, 0x92,
+ 0xC7, 0xE6, 0xE7, 0xA0, 0x5C, 0x49, 0xF0, 0xE7, 0xE4, 0xC8, 0x81, 0xBF, 0x2E, 0x94, 0xF4,
+ 0x5E, 0x4A, 0x21, 0x83, 0x3D, 0x74, 0x56, 0x85, 0x1D, 0x0F, 0x6C, 0x14, 0x5A, 0x29, 0x54,
+ 0x0C, 0x87, 0x4F, 0x30, 0x92, 0xC9, 0x34, 0xB4, 0x3D, 0x22, 0x2B, 0x89, 0x62, 0xC0, 0xF4,
+ 0x10, 0xCE, 0xF1, 0xDB, 0x75, 0x89, 0x2A, 0xF1, 0x16, 0xB4, 0x4A, 0x96, 0xF5, 0xD3, 0x5A,
+ 0xDE, 0xA3, 0x82, 0x2F, 0xC7, 0x14, 0x6F, 0x60, 0x04, 0x38, 0x5B, 0xCB, 0x69, 0xB6, 0x5C,
+ 0x99, 0xE7, 0xEB, 0x69, 0x19, 0x78, 0x67, 0x03, 0xC0, 0xD8, 0xCD, 0x41, 0xE8, 0xF7, 0x5C,
+ 0xCA, 0x44, 0xAA, 0x8A, 0xB7, 0x25, 0xAD, 0x8E, 0x79, 0x9F, 0xF3, 0xA8, 0x69, 0x6A, 0x6F,
+ 0x1B, 0x26, 0x56, 0xE6, 0x31, 0xB1, 0xE4, 0x01, 0x83, 0xC0, 0x8F, 0xDA, 0x53, 0xFA, 0x4A,
+ 0x8F, 0x85, 0xA0, 0x56, 0x93, 0x94, 0x4A, 0xE1, 0x79, 0xA1, 0x33, 0x9D, 0x00, 0x2D, 0x15,
+ 0xCA, 0xBD, 0x81, 0x00, 0x90, 0xEC, 0x72, 0x2E, 0xF5, 0xDE, 0xF9, 0x96, 0x5A, 0x37, 0x1D,
+ 0x41, 0x5D, 0x62, 0x4B, 0x68, 0xA2, 0x70, 0x7C, 0xAD, 0x97, 0xBC, 0xDD, 0x17, 0x85, 0xAF,
+ 0x97, 0xE2, 0x58, 0xF3, 0x3D, 0xF5, 0x6A, 0x03, 0x1A, 0xA0, 0x35, 0x6D, 0x8E, 0x8D, 0x5E,
+ 0xBC, 0xAD, 0xC7, 0x4E, 0x07, 0x16, 0x36, 0xC6, 0xB1, 0x10, 0xAC, 0xE5, 0xCC, 0x9B, 0x90,
+ 0xDF, 0xEA, 0xCA, 0xE6, 0x40, 0xFF, 0x1B, 0xB0, 0xF1, 0xFE, 0x5D, 0xB4, 0xEF, 0xF7, 0xA9,
+ 0x5F, 0x06, 0x07, 0x33, 0xF5, // ...
+ // Signature (variable Length)
+ 0x30, 0x45, 0x02, 0x20, 0x32, 0x47, 0x79, 0xC6, 0x8F, 0x33, 0x80, 0x28, 0x8A, 0x11, 0x97,
+ 0xB6, 0x09, 0x5F, 0x7A, 0x6E, 0xB9, 0xB1, 0xB1, 0xC1, 0x27, 0xF6, 0x6A, 0xE1, 0x2A, 0x99,
+ 0xFE, 0x85, 0x32, 0xEC, 0x23, 0xB9, 0x02, 0x21, 0x00, 0xE3, 0x95, 0x16, 0xAC, 0x4D, 0x61,
+ 0xEE, 0x64, 0x04, 0x4D, 0x50, 0xB4, 0x15, 0xA6, 0xA4, 0xD4, 0xD8, 0x4B, 0xA6, 0xD8, 0x95,
+ 0xCB, 0x5A, 0xB7, 0xA1, 0xAA, 0x7D, 0x08, 0x1D, 0xE3, 0x41, 0xFA, // ...
+ ];
+}
diff --git a/third_party/rust/authenticator/src/ctap2/commands/mod.rs b/third_party/rust/authenticator/src/ctap2/commands/mod.rs
new file mode 100644
index 0000000000..788de842ba
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/mod.rs
@@ -0,0 +1,470 @@
+use crate::crypto;
+use crate::ctap2::client_data::ClientDataHash;
+use crate::ctap2::commands::client_pin::{GetPinToken, GetRetries, Pin, PinAuth, PinError};
+use crate::errors::AuthenticatorError;
+use crate::transport::errors::{ApduErrorStatus, HIDError};
+use crate::transport::FidoDevice;
+use serde_cbor::{error::Error as CborError, Value};
+use serde_json as json;
+use std::error::Error as StdErrorT;
+use std::fmt;
+use std::io::{Read, Write};
+
+pub(crate) mod client_pin;
+pub(crate) mod get_assertion;
+pub(crate) mod get_info;
+pub(crate) mod get_next_assertion;
+pub(crate) mod get_version;
+pub(crate) mod make_credentials;
+pub(crate) mod reset;
+pub(crate) mod selection;
+
+pub trait Request<T>
+where
+ Self: fmt::Debug,
+ Self: RequestCtap1<Output = T>,
+ Self: RequestCtap2<Output = T>,
+{
+ fn is_ctap2_request(&self) -> bool;
+}
+
+/// Retryable wraps an error type and may ask manager to retry sending a
+/// command, this is useful for ctap1 where token will reply with "condition not
+/// sufficient" because user needs to press the button.
+#[derive(Debug)]
+pub enum Retryable<T> {
+ Retry,
+ Error(T),
+}
+
+impl<T> Retryable<T> {
+ pub fn is_retry(&self) -> bool {
+ matches!(*self, Retryable::Retry)
+ }
+
+ pub fn is_error(&self) -> bool {
+ !self.is_retry()
+ }
+}
+
+impl<T> From<T> for Retryable<T> {
+ fn from(e: T) -> Self {
+ Retryable::Error(e)
+ }
+}
+
+pub trait RequestCtap1: fmt::Debug {
+ type Output;
+
+ /// Serializes a request into FIDO v1.x / CTAP1 / U2F format.
+ ///
+ /// See [`crate::u2ftypes::CTAP1RequestAPDU::serialize()`]
+ fn ctap1_format<Dev>(&self, dev: &mut Dev) -> Result<Vec<u8>, HIDError>
+ where
+ Dev: FidoDevice + Read + Write + fmt::Debug;
+
+ /// Deserializes a response from FIDO v1.x / CTAP1 / U2Fv2 format.
+ fn handle_response_ctap1(
+ &self,
+ status: Result<(), ApduErrorStatus>,
+ input: &[u8],
+ ) -> Result<Self::Output, Retryable<HIDError>>;
+}
+
+pub trait RequestCtap2: fmt::Debug {
+ type Output;
+
+ fn command() -> Command;
+
+ fn wire_format<Dev>(&self, dev: &mut Dev) -> Result<Vec<u8>, HIDError>
+ where
+ Dev: FidoDevice + Read + Write + fmt::Debug;
+
+ fn handle_response_ctap2<Dev>(
+ &self,
+ dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError>
+ where
+ Dev: FidoDevice + Read + Write + fmt::Debug;
+}
+
+pub(crate) trait PinAuthCommand {
+ fn pin(&self) -> &Option<Pin>;
+ fn set_pin(&mut self, pin: Option<Pin>);
+ fn pin_auth(&self) -> &Option<PinAuth>;
+ fn set_pin_auth(&mut self, pin_auth: Option<PinAuth>, pin_auth_protocol: Option<u64>);
+ fn client_data_hash(&self) -> ClientDataHash;
+ fn unset_uv_option(&mut self);
+ fn determine_pin_auth<D: FidoDevice>(&mut self, dev: &mut D) -> Result<(), AuthenticatorError> {
+ if !dev.supports_ctap2() {
+ self.set_pin_auth(None, None);
+ return Ok(());
+ }
+
+ let client_data_hash = self.client_data_hash();
+ let (pin_auth, pin_auth_protocol) =
+ match calculate_pin_auth(dev, &client_data_hash, &self.pin()) {
+ Ok((pin_auth, pin_auth_protocol)) => (pin_auth, pin_auth_protocol),
+ Err(e) => {
+ return Err(repackage_pin_errors(dev, e));
+ }
+ };
+ self.set_pin_auth(pin_auth, pin_auth_protocol);
+ Ok(())
+ }
+}
+
+pub(crate) fn repackage_pin_errors<D: FidoDevice>(
+ dev: &mut D,
+ error: AuthenticatorError,
+) -> AuthenticatorError {
+ match error {
+ AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
+ StatusCode::PinInvalid,
+ _,
+ ))) => {
+ // If the given PIN was wrong, determine no. of left retries
+ let cmd = GetRetries::new();
+ let retries = dev.send_cbor(&cmd).ok(); // If we got retries, wrap it in Some, otherwise ignore err
+ return AuthenticatorError::PinError(PinError::InvalidPin(retries));
+ }
+ AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
+ StatusCode::PinAuthBlocked,
+ _,
+ ))) => {
+ return AuthenticatorError::PinError(PinError::PinAuthBlocked);
+ }
+ AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
+ StatusCode::PinBlocked,
+ _,
+ ))) => {
+ return AuthenticatorError::PinError(PinError::PinBlocked);
+ }
+ AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
+ StatusCode::PinRequired,
+ _,
+ ))) => {
+ return AuthenticatorError::PinError(PinError::PinRequired);
+ }
+ AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
+ StatusCode::PinNotSet,
+ _,
+ ))) => {
+ return AuthenticatorError::PinError(PinError::PinNotSet);
+ }
+ // TODO(MS): Add "PinPolicyViolated"
+ err => {
+ return err;
+ }
+ }
+}
+
+// Spec: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticator-api
+// and: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#authenticator-api
+#[repr(u8)]
+#[derive(Debug)]
+pub enum Command {
+ MakeCredentials = 0x01,
+ GetAssertion = 0x02,
+ GetInfo = 0x04,
+ ClientPin = 0x06,
+ Reset = 0x07,
+ GetNextAssertion = 0x08,
+ Selection = 0x0B,
+}
+
+impl Command {
+ #[cfg(test)]
+ pub fn from_u8(v: u8) -> Option<Command> {
+ match v {
+ 0x01 => Some(Command::MakeCredentials),
+ 0x02 => Some(Command::GetAssertion),
+ 0x04 => Some(Command::GetInfo),
+ 0x06 => Some(Command::ClientPin),
+ 0x07 => Some(Command::Reset),
+ 0x08 => Some(Command::GetNextAssertion),
+ _ => None,
+ }
+ }
+}
+
+#[derive(Debug)]
+pub enum StatusCode {
+ /// Indicates successful response.
+ OK,
+ /// The command is not a valid CTAP command.
+ InvalidCommand,
+ /// The command included an invalid parameter.
+ InvalidParameter,
+ /// Invalid message or item length.
+ InvalidLength,
+ /// Invalid message sequencing.
+ InvalidSeq,
+ /// Message timed out.
+ Timeout,
+ /// Channel busy.
+ ChannelBusy,
+ /// Command requires channel lock.
+ LockRequired,
+ /// Command not allowed on this cid.
+ InvalidChannel,
+ /// Invalid/unexpected CBOR error.
+ CBORUnexpectedType,
+ /// Error when parsing CBOR.
+ InvalidCBOR,
+ /// Missing non-optional parameter.
+ MissingParameter,
+ /// Limit for number of items exceeded.
+ LimitExceeded,
+ /// Unsupported extension.
+ UnsupportedExtension,
+ /// Valid credential found in the exclude list.
+ CredentialExcluded,
+ /// Processing (Lengthy operation is in progress).
+ Processing,
+ /// Credential not valid for the authenticator.
+ InvalidCredential,
+ /// Authentication is waiting for user interaction.
+ UserActionPending,
+ /// Processing, lengthy operation is in progress.
+ OperationPending,
+ /// No request is pending.
+ NoOperations,
+ /// Authenticator does not support requested algorithm.
+ UnsupportedAlgorithm,
+ /// Not authorized for requested operation.
+ OperationDenied,
+ /// Internal key storage is full.
+ KeyStoreFull,
+ /// No outstanding operations.
+ NoOperationPending,
+ /// Unsupported option.
+ UnsupportedOption,
+ /// Not a valid option for current operation.
+ InvalidOption,
+ /// Pending keep alive was cancelled.
+ KeepaliveCancel,
+ /// No valid credentials provided.
+ NoCredentials,
+ /// Timeout waiting for user interaction.
+ UserActionTimeout,
+ /// Continuation command, such as, authenticatorGetNextAssertion not
+ /// allowed.
+ NotAllowed,
+ /// PIN Invalid.
+ PinInvalid,
+ /// PIN Blocked.
+ PinBlocked,
+ /// PIN authentication,pinAuth, verification failed.
+ PinAuthInvalid,
+ /// PIN authentication,pinAuth, blocked. Requires power recycle to reset.
+ PinAuthBlocked,
+ /// No PIN has been set.
+ PinNotSet,
+ /// PIN is required for the selected operation.
+ PinRequired,
+ /// PIN policy violation. Currently only enforces minimum length.
+ PinPolicyViolation,
+ /// pinToken expired on authenticator.
+ PinTokenExpired,
+ /// Authenticator cannot handle this request due to memory constraints.
+ RequestTooLarge,
+ /// The current operation has timed out.
+ ActionTimeout,
+ /// User presence is required for the requested operation.
+ UpRequired,
+
+ /// Unknown status.
+ Unknown(u8),
+}
+
+impl StatusCode {
+ fn is_ok(&self) -> bool {
+ matches!(*self, StatusCode::OK)
+ }
+
+ fn device_busy(&self) -> bool {
+ matches!(*self, StatusCode::ChannelBusy)
+ }
+}
+
+impl From<u8> for StatusCode {
+ fn from(value: u8) -> StatusCode {
+ match value {
+ 0x00 => StatusCode::OK,
+ 0x01 => StatusCode::InvalidCommand,
+ 0x02 => StatusCode::InvalidParameter,
+ 0x03 => StatusCode::InvalidLength,
+ 0x04 => StatusCode::InvalidSeq,
+ 0x05 => StatusCode::Timeout,
+ 0x06 => StatusCode::ChannelBusy,
+ 0x0A => StatusCode::LockRequired,
+ 0x0B => StatusCode::InvalidChannel,
+ 0x11 => StatusCode::CBORUnexpectedType,
+ 0x12 => StatusCode::InvalidCBOR,
+ 0x14 => StatusCode::MissingParameter,
+ 0x15 => StatusCode::LimitExceeded,
+ 0x16 => StatusCode::UnsupportedExtension,
+ 0x19 => StatusCode::CredentialExcluded,
+ 0x21 => StatusCode::Processing,
+ 0x22 => StatusCode::InvalidCredential,
+ 0x23 => StatusCode::UserActionPending,
+ 0x24 => StatusCode::OperationPending,
+ 0x25 => StatusCode::NoOperations,
+ 0x26 => StatusCode::UnsupportedAlgorithm,
+ 0x27 => StatusCode::OperationDenied,
+ 0x28 => StatusCode::KeyStoreFull,
+ 0x2A => StatusCode::NoOperationPending,
+ 0x2B => StatusCode::UnsupportedOption,
+ 0x2C => StatusCode::InvalidOption,
+ 0x2D => StatusCode::KeepaliveCancel,
+ 0x2E => StatusCode::NoCredentials,
+ 0x2f => StatusCode::UserActionTimeout,
+ 0x30 => StatusCode::NotAllowed,
+ 0x31 => StatusCode::PinInvalid,
+ 0x32 => StatusCode::PinBlocked,
+ 0x33 => StatusCode::PinAuthInvalid,
+ 0x34 => StatusCode::PinAuthBlocked,
+ 0x35 => StatusCode::PinNotSet,
+ 0x36 => StatusCode::PinRequired,
+ 0x37 => StatusCode::PinPolicyViolation,
+ 0x38 => StatusCode::PinTokenExpired,
+ 0x39 => StatusCode::RequestTooLarge,
+ 0x3A => StatusCode::ActionTimeout,
+ 0x3B => StatusCode::UpRequired,
+
+ othr => StatusCode::Unknown(othr),
+ }
+ }
+}
+
+#[cfg(test)]
+impl Into<u8> for StatusCode {
+ fn into(self) -> u8 {
+ match self {
+ StatusCode::OK => 0x00,
+ StatusCode::InvalidCommand => 0x01,
+ StatusCode::InvalidParameter => 0x02,
+ StatusCode::InvalidLength => 0x03,
+ StatusCode::InvalidSeq => 0x04,
+ StatusCode::Timeout => 0x05,
+ StatusCode::ChannelBusy => 0x06,
+ StatusCode::LockRequired => 0x0A,
+ StatusCode::InvalidChannel => 0x0B,
+ StatusCode::CBORUnexpectedType => 0x11,
+ StatusCode::InvalidCBOR => 0x12,
+ StatusCode::MissingParameter => 0x14,
+ StatusCode::LimitExceeded => 0x15,
+ StatusCode::UnsupportedExtension => 0x16,
+ StatusCode::CredentialExcluded => 0x19,
+ StatusCode::Processing => 0x21,
+ StatusCode::InvalidCredential => 0x22,
+ StatusCode::UserActionPending => 0x23,
+ StatusCode::OperationPending => 0x24,
+ StatusCode::NoOperations => 0x25,
+ StatusCode::UnsupportedAlgorithm => 0x26,
+ StatusCode::OperationDenied => 0x27,
+ StatusCode::KeyStoreFull => 0x28,
+ StatusCode::NoOperationPending => 0x2A,
+ StatusCode::UnsupportedOption => 0x2B,
+ StatusCode::InvalidOption => 0x2C,
+ StatusCode::KeepaliveCancel => 0x2D,
+ StatusCode::NoCredentials => 0x2E,
+ StatusCode::UserActionTimeout => 0x2f,
+ StatusCode::NotAllowed => 0x30,
+ StatusCode::PinInvalid => 0x31,
+ StatusCode::PinBlocked => 0x32,
+ StatusCode::PinAuthInvalid => 0x33,
+ StatusCode::PinAuthBlocked => 0x34,
+ StatusCode::PinNotSet => 0x35,
+ StatusCode::PinRequired => 0x36,
+ StatusCode::PinPolicyViolation => 0x37,
+ StatusCode::PinTokenExpired => 0x38,
+ StatusCode::RequestTooLarge => 0x39,
+ StatusCode::ActionTimeout => 0x3A,
+ StatusCode::UpRequired => 0x3B,
+
+ StatusCode::Unknown(othr) => othr,
+ }
+ }
+}
+
+#[derive(Debug)]
+pub enum CommandError {
+ InputTooSmall,
+ MissingRequiredField(&'static str),
+ Deserializing(CborError),
+ Serializing(CborError),
+ StatusCode(StatusCode, Option<Value>),
+ Json(json::Error),
+ Crypto(crypto::CryptoError),
+ UnsupportedPinProtocol,
+}
+
+impl fmt::Display for CommandError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ CommandError::InputTooSmall => write!(f, "CommandError: Input is too small"),
+ CommandError::MissingRequiredField(field) => {
+ write!(f, "CommandError: Missing required field {}", field)
+ }
+ CommandError::Deserializing(ref e) => {
+ write!(f, "CommandError: Error while parsing: {}", e)
+ }
+ CommandError::Serializing(ref e) => {
+ write!(f, "CommandError: Error while serializing: {}", e)
+ }
+ CommandError::StatusCode(ref code, ref value) => {
+ write!(f, "CommandError: Unexpected code: {:?} ({:?})", code, value)
+ }
+ CommandError::Json(ref e) => write!(f, "CommandError: Json serializing error: {}", e),
+ CommandError::Crypto(ref e) => write!(f, "CommandError: Crypto error: {:?}", e),
+ CommandError::UnsupportedPinProtocol => {
+ write!(f, "CommandError: Pin protocol is not supported")
+ }
+ }
+ }
+}
+
+impl StdErrorT for CommandError {}
+
+pub(crate) fn calculate_pin_auth<Dev>(
+ dev: &mut Dev,
+ client_data_hash: &ClientDataHash,
+ pin: &Option<Pin>,
+) -> Result<(Option<PinAuth>, Option<u64>), AuthenticatorError>
+where
+ Dev: FidoDevice,
+{
+ // Not reusing the shared secret here, if it exists, since we might start again
+ // with a different PIN (e.g. if the last one was wrong)
+ let (shared_secret, info) = dev.establish_shared_secret()?;
+
+ // TODO(MS): What to do if token supports client_pin, but none has been set: Some(false)
+ // AND a Pin is not None?
+ let pin_auth = if info.options.client_pin == Some(true) {
+ let pin = pin
+ .as_ref()
+ .ok_or(HIDError::Command(CommandError::StatusCode(
+ StatusCode::PinRequired,
+ None,
+ )))?;
+
+ let pin_command = GetPinToken::new(&info, &shared_secret, &pin)?;
+ let pin_token = dev.send_cbor(&pin_command)?;
+
+ (
+ Some(
+ pin_token
+ .auth(client_data_hash.as_ref())
+ .map_err(CommandError::Crypto)?,
+ ),
+ Some(1), // Currently only pin_auth_protocol 1 supported
+ )
+ } else {
+ (None, None)
+ };
+
+ Ok(pin_auth)
+}
diff --git a/third_party/rust/authenticator/src/ctap2/commands/reset.rs b/third_party/rust/authenticator/src/ctap2/commands/reset.rs
new file mode 100644
index 0000000000..a40ae34ad0
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/reset.rs
@@ -0,0 +1,129 @@
+use super::{Command, CommandError, RequestCtap2, StatusCode};
+use crate::transport::errors::HIDError;
+use crate::u2ftypes::U2FDevice;
+use serde_cbor::{de::from_slice, Value};
+
+#[derive(Debug)]
+pub struct Reset {}
+
+impl Default for Reset {
+ fn default() -> Reset {
+ Reset {}
+ }
+}
+
+impl RequestCtap2 for Reset {
+ type Output = ();
+
+ fn command() -> Command {
+ Command::Reset
+ }
+
+ fn wire_format<Dev>(&self, _dev: &mut Dev) -> Result<Vec<u8>, HIDError>
+ where
+ Dev: U2FDevice,
+ {
+ Ok(Vec::new())
+ }
+
+ fn handle_response_ctap2<Dev>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError>
+ where
+ Dev: U2FDevice,
+ {
+ if input.is_empty() {
+ return Err(CommandError::InputTooSmall.into());
+ }
+
+ let status: StatusCode = input[0].into();
+
+ if status.is_ok() {
+ Ok(())
+ } else {
+ let msg = if input.len() > 1 {
+ let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ Some(data)
+ } else {
+ None
+ };
+ Err(CommandError::StatusCode(status, msg).into())
+ }
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ use super::*;
+ use crate::consts::HIDCmd;
+ use crate::transport::device_selector::Device;
+ use crate::transport::{hid::HIDDevice, FidoDevice};
+ use crate::u2ftypes::U2FDevice;
+ use rand::{thread_rng, RngCore};
+ use serde_cbor::{de::from_slice, Value};
+
+ fn issue_command_and_get_response(cmd: u8, add: &[u8]) -> Result<(), HIDError> {
+ let mut device = Device::new("commands/Reset").unwrap();
+ // ctap2 request
+ let mut cid = [0u8; 4];
+ thread_rng().fill_bytes(&mut cid);
+ device.set_cid(cid);
+
+ let mut msg = cid.to_vec();
+ msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x1]); // cmd + bcnt
+ msg.extend(vec![0x07]); // authenticatorReset
+ device.add_write(&msg, 0);
+
+ // ctap2 response
+ let len = 0x1 + add.len() as u8;
+ let mut msg = cid.to_vec();
+ msg.extend(vec![HIDCmd::Cbor.into(), 0x00, len]); // cmd + bcnt
+ msg.push(cmd); // Status code
+ msg.extend(add); // + maybe additional data
+ device.add_read(&msg, 0);
+
+ device.send_cbor(&Reset {})
+ }
+
+ #[test]
+ fn test_select_ctap2_only() {
+ // Test, if we can parse the status codes specified by the spec
+
+ // Ok()
+ let response = issue_command_and_get_response(0, &vec![]).expect("Unexpected error");
+ assert_eq!(response, ());
+
+ // Denied by the user
+ let response = issue_command_and_get_response(0x27, &vec![]).expect_err("Not an error!");
+ assert!(matches!(
+ response,
+ HIDError::Command(CommandError::StatusCode(StatusCode::OperationDenied, None))
+ ));
+
+ // Timeout
+ let response = issue_command_and_get_response(0x2F, &vec![]).expect_err("Not an error!");
+ assert!(matches!(
+ response,
+ HIDError::Command(CommandError::StatusCode(
+ StatusCode::UserActionTimeout,
+ None
+ ))
+ ));
+
+ // Unexpected error with more random CBOR-data
+ let add_data = vec![
+ 0x63, // text(3)
+ 0x61, 0x6c, 0x67, // "alg"
+ ];
+ let response = issue_command_and_get_response(0x02, &add_data).expect_err("Not an error!");
+ match response {
+ HIDError::Command(CommandError::StatusCode(StatusCode::InvalidParameter, Some(d))) => {
+ let expected: Value = from_slice(&add_data).unwrap();
+ assert_eq!(d, expected)
+ }
+ e => panic!("Not the expected response: {:?}", e),
+ }
+ }
+}
diff --git a/third_party/rust/authenticator/src/ctap2/commands/selection.rs b/third_party/rust/authenticator/src/ctap2/commands/selection.rs
new file mode 100644
index 0000000000..9f4491a734
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/commands/selection.rs
@@ -0,0 +1,129 @@
+use super::{Command, CommandError, RequestCtap2, StatusCode};
+use crate::transport::errors::HIDError;
+use crate::u2ftypes::U2FDevice;
+use serde_cbor::{de::from_slice, Value};
+
+#[derive(Debug)]
+pub struct Selection {}
+
+impl Default for Selection {
+ fn default() -> Selection {
+ Selection {}
+ }
+}
+
+impl RequestCtap2 for Selection {
+ type Output = ();
+
+ fn command() -> Command {
+ Command::Selection
+ }
+
+ fn wire_format<Dev>(&self, _dev: &mut Dev) -> Result<Vec<u8>, HIDError>
+ where
+ Dev: U2FDevice,
+ {
+ Ok(Vec::new())
+ }
+
+ fn handle_response_ctap2<Dev>(
+ &self,
+ _dev: &mut Dev,
+ input: &[u8],
+ ) -> Result<Self::Output, HIDError>
+ where
+ Dev: U2FDevice,
+ {
+ if input.is_empty() {
+ return Err(CommandError::InputTooSmall.into());
+ }
+
+ let status: StatusCode = input[0].into();
+
+ if status.is_ok() {
+ Ok(())
+ } else {
+ let msg = if input.len() > 1 {
+ let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
+ Some(data)
+ } else {
+ None
+ };
+ Err(CommandError::StatusCode(status, msg).into())
+ }
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ use super::*;
+ use crate::consts::HIDCmd;
+ use crate::transport::device_selector::Device;
+ use crate::transport::{hid::HIDDevice, FidoDevice};
+ use crate::u2ftypes::U2FDevice;
+ use rand::{thread_rng, RngCore};
+ use serde_cbor::{de::from_slice, Value};
+
+ fn issue_command_and_get_response(cmd: u8, add: &[u8]) -> Result<(), HIDError> {
+ let mut device = Device::new("commands/selection").unwrap();
+ // ctap2 request
+ let mut cid = [0u8; 4];
+ thread_rng().fill_bytes(&mut cid);
+ device.set_cid(cid);
+
+ let mut msg = cid.to_vec();
+ msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x1]); // cmd + bcnt
+ msg.extend(vec![0x0B]); // authenticatorSelection
+ device.add_write(&msg, 0);
+
+ // ctap2 response
+ let len = 0x1 + add.len() as u8;
+ let mut msg = cid.to_vec();
+ msg.extend(vec![HIDCmd::Cbor.into(), 0x00, len]); // cmd + bcnt
+ msg.push(cmd); // Status code
+ msg.extend(add); // + maybe additional data
+ device.add_read(&msg, 0);
+
+ device.send_cbor(&Selection {})
+ }
+
+ #[test]
+ fn test_select_ctap2_only() {
+ // Test, if we can parse the status codes specified by the spec
+
+ // Ok()
+ let response = issue_command_and_get_response(0, &vec![]).expect("Unexpected error");
+ assert_eq!(response, ());
+
+ // Denied by the user
+ let response = issue_command_and_get_response(0x27, &vec![]).expect_err("Not an error!");
+ assert!(matches!(
+ response,
+ HIDError::Command(CommandError::StatusCode(StatusCode::OperationDenied, None))
+ ));
+
+ // Timeout
+ let response = issue_command_and_get_response(0x2F, &vec![]).expect_err("Not an error!");
+ assert!(matches!(
+ response,
+ HIDError::Command(CommandError::StatusCode(
+ StatusCode::UserActionTimeout,
+ None
+ ))
+ ));
+
+ // Unexpected error with more random CBOR-data
+ let add_data = vec![
+ 0x63, // text(3)
+ 0x61, 0x6c, 0x67, // "alg"
+ ];
+ let response = issue_command_and_get_response(0x02, &add_data).expect_err("Not an error!");
+ match response {
+ HIDError::Command(CommandError::StatusCode(StatusCode::InvalidParameter, Some(d))) => {
+ let expected: Value = from_slice(&add_data).unwrap();
+ assert_eq!(d, expected)
+ }
+ e => panic!("Not the expected response: {:?}", e),
+ }
+ }
+}
diff --git a/third_party/rust/authenticator/src/ctap2/mod.rs b/third_party/rust/authenticator/src/ctap2/mod.rs
new file mode 100644
index 0000000000..aa5dd73170
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/mod.rs
@@ -0,0 +1,9 @@
+#[allow(dead_code)] // TODO(MS): Remove me asap
+pub mod commands;
+pub use commands::get_assertion::AssertionObject;
+
+pub(crate) mod attestation;
+
+pub mod client_data;
+pub mod server;
+pub(crate) mod utils;
diff --git a/third_party/rust/authenticator/src/ctap2/server.rs b/third_party/rust/authenticator/src/ctap2/server.rs
new file mode 100644
index 0000000000..3b46584cf5
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/server.rs
@@ -0,0 +1,510 @@
+use crate::crypto::COSEAlgorithm;
+use crate::{errors::AuthenticatorError, AuthenticatorTransports, KeyHandle};
+use serde::de::MapAccess;
+use serde::{
+ de::{Error as SerdeError, Visitor},
+ ser::SerializeMap,
+ Deserialize, Deserializer, Serialize, Serializer,
+};
+use serde_bytes::ByteBuf;
+use sha2::{Digest, Sha256};
+use std::convert::{Into, TryFrom};
+use std::fmt;
+
+#[derive(Serialize, Deserialize, PartialEq, Eq, Clone)]
+pub struct RpIdHash(pub [u8; 32]);
+
+impl fmt::Debug for RpIdHash {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let value = base64::encode_config(&self.0, base64::URL_SAFE_NO_PAD);
+ write!(f, "RpIdHash({})", value)
+ }
+}
+
+impl AsRef<[u8]> for RpIdHash {
+ fn as_ref(&self) -> &[u8] {
+ self.0.as_ref()
+ }
+}
+
+impl RpIdHash {
+ pub fn from(src: &[u8]) -> Result<RpIdHash, AuthenticatorError> {
+ let mut payload = [0u8; 32];
+ if src.len() != payload.len() {
+ Err(AuthenticatorError::InvalidRelyingPartyInput)
+ } else {
+ payload.copy_from_slice(src);
+ Ok(RpIdHash(payload))
+ }
+ }
+}
+
+#[derive(Debug, Serialize, Clone, Default)]
+#[cfg_attr(test, derive(Deserialize))]
+pub struct RelyingParty {
+ // TODO(baloo): spec is wrong !!!!111
+ // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#commands
+ // in the example "A PublicKeyCredentialRpEntity DOM object defined as follows:"
+ // inconsistent with https://w3c.github.io/webauthn/#sctn-rp-credential-params
+ pub id: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub name: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub icon: Option<String>,
+}
+
+// Note: This enum is provided to make old CTAP1/U2F API work. This should be deprecated at some point
+#[derive(Debug, Clone)]
+pub enum RelyingPartyWrapper {
+ Data(RelyingParty),
+ // CTAP1 hash can be derived from full object, see RelyingParty::hash below,
+ // but very old backends might still provide application IDs.
+ Hash(RpIdHash),
+}
+
+impl RelyingPartyWrapper {
+ pub fn hash(&self) -> RpIdHash {
+ match *self {
+ RelyingPartyWrapper::Data(ref d) => {
+ let mut hasher = Sha256::new();
+ hasher.update(&d.id);
+
+ let mut output = [0u8; 32];
+ output.copy_from_slice(&hasher.finalize().as_slice());
+
+ RpIdHash(output)
+ }
+ RelyingPartyWrapper::Hash(ref d) => d.clone(),
+ }
+ }
+}
+
+// TODO(baloo): should we rename this PublicKeyCredentialUserEntity ?
+#[derive(Debug, Serialize, Clone, Eq, PartialEq, Deserialize, Default)]
+pub struct User {
+ #[serde(with = "serde_bytes")]
+ pub id: Vec<u8>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub icon: Option<String>, // This has been removed from Webauthn-2
+ pub name: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none", rename = "displayName")]
+ pub display_name: Option<String>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct PublicKeyCredentialParameters {
+ pub alg: COSEAlgorithm,
+}
+
+impl TryFrom<i32> for PublicKeyCredentialParameters {
+ type Error = AuthenticatorError;
+ fn try_from(arg: i32) -> Result<Self, Self::Error> {
+ let alg = COSEAlgorithm::try_from(arg as i64)?;
+ Ok(PublicKeyCredentialParameters { alg })
+ }
+}
+
+impl Serialize for PublicKeyCredentialParameters {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let mut map = serializer.serialize_map(Some(2))?;
+ map.serialize_entry("alg", &self.alg)?;
+ map.serialize_entry("type", "public-key")?;
+ map.end()
+ }
+}
+
+impl<'de> Deserialize<'de> for PublicKeyCredentialParameters {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct PublicKeyCredentialParametersVisitor;
+
+ impl<'de> Visitor<'de> for PublicKeyCredentialParametersVisitor {
+ type Value = PublicKeyCredentialParameters;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a map")
+ }
+
+ fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ where
+ M: MapAccess<'de>,
+ {
+ let mut found_type = false;
+ let mut alg = None;
+ while let Some(key) = map.next_key()? {
+ match key {
+ "alg" => {
+ if alg.is_some() {
+ return Err(SerdeError::duplicate_field("alg"));
+ }
+ alg = Some(map.next_value()?);
+ }
+ "type" => {
+ if found_type {
+ return Err(SerdeError::duplicate_field("type"));
+ }
+
+ let v: &str = map.next_value()?;
+ if v != "public-key" {
+ return Err(SerdeError::custom(format!("invalid value: {}", v)));
+ }
+ found_type = true;
+ }
+ v => {
+ return Err(SerdeError::unknown_field(v, &[]));
+ }
+ }
+ }
+
+ if !found_type {
+ return Err(SerdeError::missing_field("type"));
+ }
+
+ let alg = alg.ok_or(SerdeError::missing_field("alg"))?;
+
+ Ok(PublicKeyCredentialParameters { alg })
+ }
+ }
+
+ deserializer.deserialize_bytes(PublicKeyCredentialParametersVisitor)
+ }
+}
+
+#[derive(Debug, PartialEq, Serialize, Deserialize, Eq, Clone)]
+#[serde(rename_all = "lowercase")]
+pub enum Transport {
+ USB,
+ NFC,
+ BLE,
+ Internal,
+}
+
+impl From<AuthenticatorTransports> for Vec<Transport> {
+ fn from(t: AuthenticatorTransports) -> Self {
+ let mut transports = Vec::new();
+ if t.contains(AuthenticatorTransports::USB) {
+ transports.push(Transport::USB);
+ }
+ if t.contains(AuthenticatorTransports::NFC) {
+ transports.push(Transport::NFC);
+ }
+ if t.contains(AuthenticatorTransports::BLE) {
+ transports.push(Transport::BLE);
+ }
+
+ transports
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct PublicKeyCredentialDescriptor {
+ pub id: Vec<u8>,
+ pub transports: Vec<Transport>,
+}
+
+impl Serialize for PublicKeyCredentialDescriptor {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ // TODO(MS): Transports is OPTIONAL, but some older tokens don't understand it
+ // and return a CBOR-Parsing error. It is only a hint for the token,
+ // so we'll leave it out for the moment
+ let mut map = serializer.serialize_map(Some(2))?;
+ // let mut map = serializer.serialize_map(Some(3))?;
+ map.serialize_entry("type", "public-key")?;
+ map.serialize_entry("id", &ByteBuf::from(self.id.clone()))?;
+ // map.serialize_entry("transports", &self.transports)?;
+ map.end()
+ }
+}
+
+impl<'de> Deserialize<'de> for PublicKeyCredentialDescriptor {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct PublicKeyCredentialDescriptorVisitor;
+
+ impl<'de> Visitor<'de> for PublicKeyCredentialDescriptorVisitor {
+ type Value = PublicKeyCredentialDescriptor;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a map")
+ }
+
+ fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+ where
+ M: MapAccess<'de>,
+ {
+ let mut found_type = false;
+ let mut id = None;
+ let mut transports = None;
+ while let Some(key) = map.next_key()? {
+ match key {
+ "id" => {
+ if id.is_some() {
+ return Err(SerdeError::duplicate_field("id"));
+ }
+ let id_bytes: ByteBuf = map.next_value()?;
+ id = Some(id_bytes.into_vec());
+ }
+ "transports" => {
+ if transports.is_some() {
+ return Err(SerdeError::duplicate_field("transports"));
+ }
+ transports = Some(map.next_value()?);
+ }
+ "type" => {
+ if found_type {
+ return Err(SerdeError::duplicate_field("type"));
+ }
+ let v: &str = map.next_value()?;
+ if v != "public-key" {
+ return Err(SerdeError::custom(format!("invalid value: {}", v)));
+ }
+ found_type = true;
+ }
+ v => {
+ return Err(SerdeError::unknown_field(v, &[]));
+ }
+ }
+ }
+
+ if !found_type {
+ return Err(SerdeError::missing_field("type"));
+ }
+
+ let id = id.ok_or(SerdeError::missing_field("id"))?;
+ let transports = transports.unwrap_or(Vec::new());
+
+ Ok(PublicKeyCredentialDescriptor { id, transports })
+ }
+ }
+
+ deserializer.deserialize_bytes(PublicKeyCredentialDescriptorVisitor)
+ }
+}
+
+impl From<&KeyHandle> for PublicKeyCredentialDescriptor {
+ fn from(kh: &KeyHandle) -> Self {
+ Self {
+ id: kh.credential.clone(),
+ transports: kh.transports.into(),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::{
+ COSEAlgorithm, PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty,
+ Transport, User,
+ };
+
+ #[test]
+ fn serialize_rp() {
+ let rp = RelyingParty {
+ id: String::from("Acme"),
+ name: None,
+ icon: None,
+ };
+
+ let payload = ser::to_vec(&rp).unwrap();
+ assert_eq!(
+ &payload,
+ &[
+ 0xa1, // map(1)
+ 0x62, // text(2)
+ 0x69, 0x64, // "id"
+ 0x64, // text(4)
+ 0x41, 0x63, 0x6d, 0x65
+ ]
+ );
+ }
+
+ #[test]
+ fn serialize_user() {
+ let user = User {
+ id: vec![
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30,
+ 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82,
+ 0x01, 0x93, 0x30, 0x82,
+ ],
+ icon: Some(String::from("https://pics.example.com/00/p/aBjjjpqPb.png")),
+ name: Some(String::from("johnpsmith@example.com")),
+ display_name: Some(String::from("John P. Smith")),
+ };
+
+ let payload = ser::to_vec(&user).unwrap();
+ println!("payload = {:?}", payload);
+ assert_eq!(
+ payload,
+ vec![
+ 0xa4, // map(4)
+ 0x62, // text(2)
+ 0x69, 0x64, // "id"
+ 0x58, 0x20, // bytes(32)
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, // userid
+ 0x02, 0x01, 0x02, 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, // ...
+ 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82, 0x01, 0x93, // ...
+ 0x30, 0x82, // ...
+ 0x64, // text(4)
+ 0x69, 0x63, 0x6f, 0x6e, // "icon"
+ 0x78, 0x2b, // text(43)
+ 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x70,
+ 0x69, // "https://pics.example.com/00/p/aBjjjpqPb.png"
+ 0x63, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, // ...
+ 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x30, 0x30, 0x2f, 0x70, 0x2f, // ...
+ 0x61, 0x42, 0x6a, 0x6a, 0x6a, 0x70, 0x71, 0x50, 0x62, 0x2e, // ...
+ 0x70, 0x6e, 0x67, // ...
+ 0x64, // text(4)
+ 0x6e, 0x61, 0x6d, 0x65, // "name"
+ 0x76, // text(22)
+ 0x6a, 0x6f, 0x68, 0x6e, 0x70, 0x73, 0x6d, 0x69, 0x74,
+ 0x68, // "johnpsmith@example.com"
+ 0x40, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, // ...
+ 0x6f, 0x6d, // ...
+ 0x6b, // text(11)
+ 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, // "displayName"
+ 0x65, // ...
+ 0x6d, // text(13)
+ 0x4a, 0x6f, 0x68, 0x6e, 0x20, 0x50, 0x2e, 0x20, 0x53, 0x6d, // "John P. Smith"
+ 0x69, 0x74, 0x68, // ...
+ ]
+ );
+ }
+
+ #[test]
+ fn serialize_user_noicon_nodisplayname() {
+ let user = User {
+ id: vec![
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30,
+ 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82,
+ 0x01, 0x93, 0x30, 0x82,
+ ],
+ icon: None,
+ name: Some(String::from("johnpsmith@example.com")),
+ display_name: None,
+ };
+
+ let payload = ser::to_vec(&user).unwrap();
+ println!("payload = {:?}", payload);
+ assert_eq!(
+ payload,
+ vec![
+ 0xa2, // map(2)
+ 0x62, // text(2)
+ 0x69, 0x64, // "id"
+ 0x58, 0x20, // bytes(32)
+ 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, // userid
+ 0x02, 0x01, 0x02, 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, // ...
+ 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82, 0x01, 0x93, // ...
+ 0x30, 0x82, // ...
+ 0x64, // text(4)
+ 0x6e, 0x61, 0x6d, 0x65, // "name"
+ 0x76, // text(22)
+ 0x6a, 0x6f, 0x68, 0x6e, 0x70, 0x73, 0x6d, 0x69, 0x74,
+ 0x68, // "johnpsmith@example.com"
+ 0x40, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, // ...
+ 0x6f, 0x6d, // ...
+ ]
+ );
+ }
+
+ use serde_cbor::ser;
+
+ #[test]
+ fn public_key() {
+ let keys = vec![
+ PublicKeyCredentialParameters {
+ alg: COSEAlgorithm::ES256,
+ },
+ PublicKeyCredentialParameters {
+ alg: COSEAlgorithm::RS256,
+ },
+ ];
+
+ let payload = ser::to_vec(&keys);
+ println!("payload = {:?}", payload);
+ let payload = payload.unwrap();
+ assert_eq!(
+ payload,
+ vec![
+ 0x82, // array(2)
+ 0xa2, // map(2)
+ 0x63, // text(3)
+ 0x61, 0x6c, 0x67, // "alg"
+ 0x26, // -7 (ES256)
+ 0x64, // text(4)
+ 0x74, 0x79, 0x70, 0x65, // "type"
+ 0x6a, // text(10)
+ 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, // "public-key"
+ 0x2D, 0x6B, 0x65, 0x79, // ...
+ 0xa2, // map(2)
+ 0x63, // text(3)
+ 0x61, 0x6c, 0x67, // "alg"
+ 0x39, 0x01, 0x00, // -257 (RS256)
+ 0x64, // text(4)
+ 0x74, 0x79, 0x70, 0x65, // "type"
+ 0x6a, // text(10)
+ 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, // "public-key"
+ 0x2D, 0x6B, 0x65, 0x79 // ...
+ ]
+ );
+ }
+
+ #[test]
+ fn public_key_desc() {
+ let key = PublicKeyCredentialDescriptor {
+ id: vec![
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
+ 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
+ 0x1c, 0x1d, 0x1e, 0x1f,
+ ],
+ transports: vec![Transport::BLE, Transport::USB],
+ };
+
+ let payload = ser::to_vec(&key);
+ println!("payload = {:?}", payload);
+ let payload = payload.unwrap();
+
+ assert_eq!(
+ payload,
+ vec![
+ // 0xa3, // map(3)
+ 0xa2, // map(2)
+ 0x64, // text(4)
+ 0x74, 0x79, 0x70, 0x65, // "type"
+ 0x6a, // text(10)
+ 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, // "public-key"
+ 0x2D, 0x6B, 0x65, 0x79, // ...
+ 0x62, // text(2)
+ 0x69, 0x64, // "id"
+ 0x58, 0x20, // bytes(32)
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, // key id
+ 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, // ...
+ 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, // ...
+ 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, // ...
+ 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, // ...
+ 0x1e,
+ 0x1f, // ...
+
+ // Deactivated for now
+ //0x6a, // text(10)
+ //0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, // "transports"
+ //0x6f, 0x72, 0x74, 0x73, // ...
+ //0x82, // array(2)
+ //0x63, // text(3)
+ //0x62, 0x6c, 0x65, // "ble"
+ //0x63, // text(3)
+ //0x75, 0x73, 0x62 // "usb"
+ ]
+ );
+ }
+}
diff --git a/third_party/rust/authenticator/src/ctap2/utils.rs b/third_party/rust/authenticator/src/ctap2/utils.rs
new file mode 100644
index 0000000000..ba9c7db3b4
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2/utils.rs
@@ -0,0 +1,14 @@
+use serde::de;
+use serde_cbor::error::Result;
+use serde_cbor::Deserializer;
+
+pub fn from_slice_stream<'a, T>(slice: &'a [u8]) -> Result<(&'a [u8], T)>
+where
+ T: de::Deserialize<'a>,
+{
+ let mut deserializer = Deserializer::from_slice(slice);
+ let value = de::Deserialize::deserialize(&mut deserializer)?;
+ let rest = &slice[deserializer.byte_offset()..];
+
+ Ok((rest, value))
+}
diff --git a/third_party/rust/authenticator/src/ctap2_capi.rs b/third_party/rust/authenticator/src/ctap2_capi.rs
new file mode 100644
index 0000000000..f1dc4bb8f9
--- /dev/null
+++ b/third_party/rust/authenticator/src/ctap2_capi.rs
@@ -0,0 +1,945 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::authenticatorservice::{
+ AuthenticatorService, CtapVersion, RegisterArgsCtap2, SignArgsCtap2,
+};
+use crate::ctap2::attestation::AttestationStatement;
+use crate::ctap2::commands::get_assertion::{Assertion, AssertionObject, GetAssertionOptions};
+use crate::ctap2::commands::make_credentials::MakeCredentialsOptions;
+use crate::ctap2::server::{
+ PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, User,
+};
+use crate::errors::{AuthenticatorError, U2FTokenError};
+use crate::statecallback::StateCallback;
+use crate::{AttestationObject, CollectedClientDataWrapper, Pin, StatusUpdate};
+use crate::{RegisterResult, SignResult};
+use libc::size_t;
+use rand::{thread_rng, Rng};
+use serde_cbor;
+use std::convert::TryFrom;
+use std::ffi::{CStr, CString};
+use std::os::raw::c_char;
+use std::sync::mpsc::channel;
+use std::thread;
+use std::{ptr, slice};
+
+type Ctap2RegisterResult = Result<(AttestationObject, CString), AuthenticatorError>;
+type Ctap2PubKeyCredDescriptors = Vec<PublicKeyCredentialDescriptor>;
+type Ctap2RegisterCallback = extern "C" fn(u64, *mut Ctap2RegisterResult);
+type Ctap2SignResult = Result<(AssertionObject, CString), AuthenticatorError>;
+type Ctap2SignCallback = extern "C" fn(u64, *mut Ctap2SignResult);
+type Ctap2StatusUpdateCallback = extern "C" fn(*mut StatusUpdate);
+
+const SIGN_RESULT_PUBKEY_CRED_ID: u8 = 1;
+const SIGN_RESULT_AUTH_DATA: u8 = 2;
+const SIGN_RESULT_SIGNATURE: u8 = 3;
+const SIGN_RESULT_USER_ID: u8 = 4;
+const SIGN_RESULT_USER_NAME: u8 = 5;
+
+#[repr(C)]
+pub struct AuthenticatorArgsUser {
+ id_ptr: *const u8,
+ id_len: usize,
+ name: *const c_char,
+}
+
+#[repr(C)]
+pub struct AuthenticatorArgsChallenge {
+ ptr: *const u8,
+ len: usize,
+}
+
+#[repr(C)]
+pub struct AuthenticatorArgsPubCred {
+ ptr: *const i32,
+ len: usize,
+}
+
+#[repr(C)]
+pub struct AuthenticatorArgsOptions {
+ resident_key: bool,
+ user_verification: bool,
+ user_presence: bool,
+ force_none_attestation: bool,
+}
+
+// Generates a new 64-bit transaction id with collision probability 2^-32.
+fn new_tid() -> u64 {
+ thread_rng().gen::<u64>()
+}
+
+unsafe fn from_raw(ptr: *const u8, len: usize) -> Vec<u8> {
+ slice::from_raw_parts(ptr, len).to_vec()
+}
+
+/// # Safety
+///
+/// This method must not be called on a handle twice, and the handle is unusable
+/// after.
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_mgr_free(mgr: *mut AuthenticatorService) {
+ if !mgr.is_null() {
+ drop(Box::from_raw(mgr));
+ }
+}
+
+/// # Safety
+///
+/// The handle returned by this method must be freed by the caller.
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_pkcd_new() -> *mut Ctap2PubKeyCredDescriptors {
+ Box::into_raw(Box::new(vec![]))
+}
+
+/// # Safety
+///
+/// This method must be used on an actual Ctap2PubKeyCredDescriptors CredDescriptorse
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_pkcd_add(
+ pkcd: *mut Ctap2PubKeyCredDescriptors,
+ id_ptr: *const u8,
+ id_len: usize,
+ transports: u8,
+) {
+ (*pkcd).push(PublicKeyCredentialDescriptor {
+ id: from_raw(id_ptr, id_len),
+ transports: crate::AuthenticatorTransports::from_bits_truncate(transports).into(),
+ });
+}
+
+/// # Safety
+///
+/// This method must not be called on a handle twice, and the handle is unusable
+/// after.
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_pkcd_free(khs: *mut Ctap2PubKeyCredDescriptors) {
+ if !khs.is_null() {
+ drop(Box::from_raw(khs));
+ }
+}
+
+/// # Safety
+///
+/// The handle returned by this method must be freed by the caller.
+/// The returned handle can be used with all rust_u2f_mgr_*-functions as well
+/// but uses CTAP2 as the underlying protocol. CTAP1 requests will be repackaged
+/// into CTAP2 (if the device supports it)
+#[no_mangle]
+pub extern "C" fn rust_ctap2_mgr_new() -> *mut AuthenticatorService {
+ if let Ok(mut mgr) = AuthenticatorService::new(CtapVersion::CTAP2) {
+ mgr.add_detected_transports();
+ Box::into_raw(Box::new(mgr))
+ } else {
+ ptr::null_mut()
+ }
+}
+
+fn rewrap_client_data(
+ client_data: CollectedClientDataWrapper,
+) -> Result<CString, AuthenticatorError> {
+ let s = CString::new(client_data.serialized_data.clone()).map_err(|_| {
+ AuthenticatorError::Custom("Failed to transform client_data to C String".to_string())
+ })?;
+ Ok(s)
+}
+
+fn rewrap_register_result(
+ attestation_object: AttestationObject,
+ client_data: CollectedClientDataWrapper,
+) -> Ctap2RegisterResult {
+ let s = rewrap_client_data(client_data)?;
+ Ok((attestation_object, s))
+}
+
+fn rewrap_sign_result(
+ assertion_object: AssertionObject,
+ client_data: CollectedClientDataWrapper,
+) -> Ctap2SignResult {
+ let s = rewrap_client_data(client_data)?;
+ Ok((assertion_object, s))
+}
+
+/// # Safety
+///
+/// This method should not be called on AuthenticatorService handles after
+/// they've been freed
+/// All input is copied and it is the callers responsibility to free appropriately.
+/// Note: `KeyHandles` are used as `PublicKeyCredentialDescriptor`s for the exclude_list
+/// to keep the API smaller, as they are essentially the same thing.
+/// `PublicKeyCredentialParameters` in pub_cred_params are represented as i32 with
+/// their COSE value (see: https://www.iana.org/assignments/cose/cose.xhtml#table-algorithms)
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_mgr_register(
+ mgr: *mut AuthenticatorService,
+ timeout: u64,
+ callback: Ctap2RegisterCallback,
+ status_callback: Ctap2StatusUpdateCallback,
+ challenge: AuthenticatorArgsChallenge,
+ relying_party_id: *const c_char,
+ origin_ptr: *const c_char,
+ user: AuthenticatorArgsUser,
+ pub_cred_params: AuthenticatorArgsPubCred,
+ exclude_list: *const Ctap2PubKeyCredDescriptors,
+ options: AuthenticatorArgsOptions,
+ pin_ptr: *const c_char,
+) -> u64 {
+ if mgr.is_null() {
+ return 0;
+ }
+
+ // Check buffers.
+ if challenge.ptr.is_null()
+ || origin_ptr.is_null()
+ || relying_party_id.is_null()
+ || user.id_ptr.is_null()
+ || user.name.is_null()
+ || exclude_list.is_null()
+ {
+ return 0;
+ }
+
+ let pub_cred_params = match slice::from_raw_parts(pub_cred_params.ptr, pub_cred_params.len)
+ .iter()
+ .map(|x| PublicKeyCredentialParameters::try_from(*x))
+ .collect()
+ {
+ Ok(x) => x,
+ Err(_) => {
+ return 0;
+ }
+ };
+ let pin = if pin_ptr.is_null() {
+ None
+ } else {
+ Some(Pin::new(&CStr::from_ptr(pin_ptr).to_string_lossy()))
+ };
+ let user = User {
+ id: from_raw(user.id_ptr, user.id_len),
+ name: Some(CStr::from_ptr(user.name).to_string_lossy().to_string()), // TODO(MS): Use to_str() and error out on failure?
+ display_name: None,
+ icon: None,
+ };
+ let rp = RelyingParty {
+ id: CStr::from_ptr(relying_party_id)
+ .to_string_lossy()
+ .to_string(),
+ name: None,
+ icon: None,
+ };
+ let origin = CStr::from_ptr(origin_ptr).to_string_lossy().to_string();
+ let challenge = from_raw(challenge.ptr, challenge.len);
+ let exclude_list = (*exclude_list).clone();
+ let force_none_attestation = options.force_none_attestation;
+
+ let (status_tx, status_rx) = channel::<crate::StatusUpdate>();
+ thread::spawn(move || loop {
+ match status_rx.recv() {
+ Ok(r) => {
+ let rb = Box::new(r);
+ status_callback(Box::into_raw(rb));
+ }
+ Err(e) => {
+ status_callback(ptr::null_mut());
+ error!("Error when receiving status update: {:?}", e);
+ return;
+ }
+ }
+ });
+
+ let tid = new_tid();
+
+ let state_callback = StateCallback::<crate::Result<RegisterResult>>::new(Box::new(move |rv| {
+ let res = match rv {
+ Ok(RegisterResult::CTAP1(..)) => Err(AuthenticatorError::VersionMismatch(
+ "rust_ctap2_mgr_register",
+ 2,
+ )),
+ Ok(RegisterResult::CTAP2(mut attestation_object, client_data)) => {
+ if force_none_attestation {
+ attestation_object.att_statement = AttestationStatement::None;
+ }
+ rewrap_register_result(attestation_object, client_data)
+ }
+ Err(e) => Err(e),
+ };
+
+ callback(tid, Box::into_raw(Box::new(res)));
+ }));
+
+ let ctap_args = RegisterArgsCtap2 {
+ challenge,
+ relying_party: rp,
+ origin,
+ user,
+ pub_cred_params,
+ exclude_list,
+ options: MakeCredentialsOptions {
+ resident_key: options.resident_key.then(|| true),
+ user_verification: options.user_verification.then(|| true),
+ },
+ extensions: Default::default(),
+ pin,
+ };
+
+ let res = (*mgr).register(timeout, ctap_args.into(), status_tx, state_callback);
+
+ if res.is_ok() {
+ tid
+ } else {
+ 0
+ }
+}
+
+/// # Safety
+///
+/// This method should not be called on AuthenticatorService handles after
+/// they've been freed
+/// Note: `KeyHandles` are used as `PublicKeyCredentialDescriptor`s for the exclude_list
+/// to keep the API smaller, as they are essentially the same thing.
+/// `PublicKeyCredentialParameters` in pub_cred_params are represented as i32 with
+/// their COSE value (see: https://www.iana.org/assignments/cose/cose.xhtml#table-algorithms)
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_mgr_sign(
+ mgr: *mut AuthenticatorService,
+ timeout: u64,
+ callback: Ctap2SignCallback,
+ status_callback: Ctap2StatusUpdateCallback,
+ challenge: AuthenticatorArgsChallenge,
+ relying_party_id: *const c_char,
+ origin_ptr: *const c_char,
+ allow_list: *const Ctap2PubKeyCredDescriptors,
+ options: AuthenticatorArgsOptions,
+ pin_ptr: *const c_char,
+) -> u64 {
+ if mgr.is_null() {
+ return 0;
+ }
+
+ // Check buffers.
+ if challenge.ptr.is_null()
+ || origin_ptr.is_null()
+ || relying_party_id.is_null()
+ || allow_list.is_null()
+ {
+ return 0;
+ }
+
+ let pin = if pin_ptr.is_null() {
+ None
+ } else {
+ Some(Pin::new(&CStr::from_ptr(pin_ptr).to_string_lossy()))
+ };
+ let rpid = CStr::from_ptr(relying_party_id)
+ .to_string_lossy()
+ .to_string();
+ let origin = CStr::from_ptr(origin_ptr).to_string_lossy().to_string();
+ let challenge = from_raw(challenge.ptr, challenge.len);
+ let allow_list: Vec<_> = (*allow_list).clone();
+
+ let (status_tx, status_rx) = channel::<crate::StatusUpdate>();
+ thread::spawn(move || loop {
+ match status_rx.recv() {
+ Ok(r) => {
+ let rb = Box::new(r);
+ status_callback(Box::into_raw(rb));
+ }
+ Err(e) => {
+ status_callback(ptr::null_mut());
+ error!("Error when receiving status update: {:?}", e);
+ return;
+ }
+ }
+ });
+
+ let single_key_handle = if allow_list.len() == 1 {
+ Some(allow_list.first().unwrap().clone())
+ } else {
+ None
+ };
+
+ let tid = new_tid();
+ let state_callback = StateCallback::<crate::Result<SignResult>>::new(Box::new(move |rv| {
+ let res = match rv {
+ Ok(SignResult::CTAP1(..)) => Err(AuthenticatorError::VersionMismatch(
+ "rust_ctap2_mgr_register",
+ 2,
+ )),
+ Ok(SignResult::CTAP2(mut assertion_object, client_data)) => {
+ // The token can omit sending back credentials, if the allow_list had only one
+ // entry. Thus we re-add that here now for all found assertions before handing it out.
+ assertion_object.0.iter_mut().for_each(|x| {
+ x.credentials = x.credentials.clone().or(single_key_handle.clone());
+ });
+ rewrap_sign_result(assertion_object, client_data)
+ }
+ Err(e) => Err(e),
+ };
+
+ callback(tid, Box::into_raw(Box::new(res)));
+ }));
+ let ctap_args = SignArgsCtap2 {
+ challenge,
+ origin,
+ relying_party_id: rpid,
+ allow_list,
+ options: GetAssertionOptions {
+ user_presence: options.user_presence.then(|| true),
+ user_verification: options.user_verification.then(|| true),
+ },
+ extensions: Default::default(),
+ pin,
+ };
+
+ let res = (*mgr).sign(timeout, ctap_args.into(), status_tx, state_callback);
+
+ if res.is_ok() {
+ tid
+ } else {
+ 0
+ }
+}
+
+// /// # Safety
+// ///
+// /// This method must be used on an actual U2FResult handle
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_register_result_error(res: *const Ctap2RegisterResult) -> u8 {
+ if res.is_null() {
+ return U2FTokenError::Unknown as u8;
+ }
+
+ match &*res {
+ Ok(..) => 0, // No error, the request succeeded.
+ Err(e) => e.as_u2f_errorcode(),
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_sign_result_error(res: *const Ctap2SignResult) -> u8 {
+ if res.is_null() {
+ return U2FTokenError::Unknown as u8;
+ }
+
+ match &*res {
+ Ok(..) => 0, // No error, the request succeeded.
+ Err(e) => e.as_u2f_errorcode(),
+ }
+}
+
+/// # Safety
+///
+/// This method should not be called on RegisterResult handles after they've been
+/// freed or a double-free will occur
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_register_res_free(res: *mut Ctap2RegisterResult) {
+ if !res.is_null() {
+ drop(Box::from_raw(res));
+ }
+}
+
+/// # Safety
+///
+/// This method should not be called on SignResult handles after they've been
+/// freed or a double-free will occur
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_sign_res_free(res: *mut Ctap2SignResult) {
+ if !res.is_null() {
+ drop(Box::from_raw(res));
+ }
+}
+
+/// # Safety
+///
+/// This method should not be called AuthenticatorService handles after they've
+/// been freed
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_mgr_cancel(mgr: *mut AuthenticatorService) {
+ if !mgr.is_null() {
+ // Ignore return value.
+ let _ = (*mgr).cancel();
+ }
+}
+
+unsafe fn client_data_len<T>(
+ res: *const Result<(T, CString), AuthenticatorError>,
+ len: *mut size_t,
+) -> bool {
+ if res.is_null() || len.is_null() {
+ return false;
+ }
+
+ match &*res {
+ Ok((_, client_data)) => {
+ *len = client_data.as_bytes().len();
+ true
+ }
+ Err(_) => false,
+ }
+}
+
+/// This function is used to get the length, prior to calling
+/// rust_ctap2_register_result_client_data_copy()
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_register_result_client_data_len(
+ res: *const Ctap2RegisterResult,
+ len: *mut size_t,
+) -> bool {
+ client_data_len(res, len)
+}
+
+/// This function is used to get the length, prior to calling
+/// rust_ctap2_sign_result_client_data_copy()
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_sign_result_client_data_len(
+ res: *const Ctap2SignResult,
+ len: *mut size_t,
+) -> bool {
+ client_data_len(res, len)
+}
+
+unsafe fn client_data_copy<T>(
+ res: *const Result<(T, CString), AuthenticatorError>,
+ dst: *mut c_char,
+) -> bool {
+ if dst.is_null() || res.is_null() {
+ return false;
+ }
+
+ match &*res {
+ Ok((_, client_data)) => {
+ ptr::copy_nonoverlapping(client_data.as_ptr(), dst, client_data.as_bytes().len());
+ return true;
+ }
+ Err(_) => false,
+ }
+}
+
+/// # Safety
+///
+/// This method does not ensure anything about dst before copying, so
+/// ensure it is long enough (using rust_ctap2_register_result_client_data_len)
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_register_result_client_data_copy(
+ res: *const Ctap2RegisterResult,
+ dst: *mut c_char,
+) -> bool {
+ client_data_copy(res, dst)
+}
+
+/// # Safety
+///
+/// This method does not ensure anything about dst before copying, so
+/// ensure it is long enough (using rust_ctap2_register_result_client_data_len)
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_sign_result_client_data_copy(
+ res: *const Ctap2SignResult,
+ dst: *mut c_char,
+) -> bool {
+ client_data_copy(res, dst)
+}
+
+/// # Safety
+///
+/// This function is used to get how long the specific item is.
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_register_result_attestation_len(
+ res: *const Ctap2RegisterResult,
+ len: *mut size_t,
+) -> bool {
+ if res.is_null() || len.is_null() {
+ return false;
+ }
+
+ if let Ok((attestation, _)) = &*res {
+ if let Some(item_len) = serde_cbor::to_vec(&attestation).ok().map(|x| x.len()) {
+ *len = item_len;
+ return true;
+ }
+ }
+ false
+}
+
+/// # Safety
+///
+/// This method does not ensure anything about dst before copying, so
+/// ensure it is long enough (using rust_ctap2_register_result_item_len)
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_register_result_attestation_copy(
+ res: *const Ctap2RegisterResult,
+ dst: *mut u8,
+) -> bool {
+ if res.is_null() || dst.is_null() {
+ return false;
+ }
+
+ if let Ok((attestation, _)) = &*res {
+ if let Ok(item) = serde_cbor::to_vec(&attestation) {
+ ptr::copy_nonoverlapping(item.as_ptr(), dst, item.len());
+ return true;
+ }
+ }
+ false
+}
+
+/// This function is used to get how many assertions there are in total
+/// The returned number can be used as index-maximum to access individual
+/// fields
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_sign_result_assertions_len(
+ res: *const Ctap2SignResult,
+ len: *mut size_t,
+) -> bool {
+ if res.is_null() || len.is_null() {
+ return false;
+ }
+
+ match &*res {
+ Ok((assertions, _)) => {
+ *len = assertions.0.len();
+ return true;
+ }
+ Err(_) => false,
+ }
+}
+
+fn sign_result_item_len(assertion: &Assertion, item_idx: u8) -> Option<usize> {
+ match item_idx {
+ SIGN_RESULT_PUBKEY_CRED_ID => assertion.credentials.as_ref().map(|x| x.id.len()),
+ // This is inefficent! Converting twice here. Once for len, once for copy
+ SIGN_RESULT_AUTH_DATA => assertion.auth_data.to_vec().ok().map(|x| x.len()),
+ SIGN_RESULT_SIGNATURE => Some(assertion.signature.len()),
+ SIGN_RESULT_USER_ID => assertion.user.as_ref().map(|u| u.id.len()),
+ SIGN_RESULT_USER_NAME => assertion
+ .user
+ .as_ref()
+ .map(|u| {
+ u.display_name
+ .as_ref()
+ .or(u.name.as_ref())
+ .map(|n| n.as_bytes().len())
+ })
+ .flatten(),
+ _ => None,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_sign_result_item_contains(
+ res: *const Ctap2SignResult,
+ assertion_idx: usize,
+ item_idx: u8,
+) -> bool {
+ if res.is_null() {
+ return false;
+ }
+
+ match &*res {
+ Ok((assertions, _)) => {
+ if assertion_idx >= assertions.0.len() {
+ return false;
+ }
+ if item_idx == SIGN_RESULT_AUTH_DATA {
+ // Short-cut to avoid serializing auth_data
+ return true;
+ }
+ sign_result_item_len(&assertions.0[assertion_idx], item_idx).is_some()
+ }
+ Err(_) => false,
+ }
+}
+
+/// # Safety
+///
+/// This function is used to get how long the specific item is.
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_sign_result_item_len(
+ res: *const Ctap2SignResult,
+ assertion_idx: usize,
+ item_idx: u8,
+ len: *mut size_t,
+) -> bool {
+ if res.is_null() || len.is_null() {
+ return false;
+ }
+
+ match &*res {
+ Ok((assertions, _)) => {
+ if assertion_idx >= assertions.0.len() {
+ return false;
+ }
+
+ if let Some(item_len) = sign_result_item_len(&assertions.0[assertion_idx], item_idx) {
+ *len = item_len;
+ true
+ } else {
+ false
+ }
+ }
+ Err(_) => false,
+ }
+}
+
+unsafe fn sign_result_item_copy(assertion: &Assertion, item_idx: u8, dst: *mut u8) -> bool {
+ if dst.is_null() {
+ return false;
+ }
+
+ let tmp_val;
+ let item = match item_idx {
+ SIGN_RESULT_PUBKEY_CRED_ID => assertion.credentials.as_ref().map(|x| x.id.as_ref()),
+ // This is inefficent! Converting twice here. Once for len, once for copy
+ SIGN_RESULT_AUTH_DATA => {
+ tmp_val = assertion.auth_data.to_vec().ok();
+ tmp_val.as_ref().map(|x| x.as_ref())
+ }
+ SIGN_RESULT_SIGNATURE => Some(assertion.signature.as_ref()),
+ SIGN_RESULT_USER_ID => assertion.user.as_ref().map(|u| u.id.as_ref()),
+ SIGN_RESULT_USER_NAME => assertion
+ .user
+ .as_ref()
+ .map(|u| {
+ u.display_name
+ .as_ref()
+ .or(u.name.as_ref())
+ .map(|n| n.as_bytes().as_ref())
+ })
+ .flatten(),
+ _ => None,
+ };
+
+ if let Some(item) = item {
+ ptr::copy_nonoverlapping(item.as_ptr(), dst, item.len());
+ true
+ } else {
+ false
+ }
+}
+
+/// # Safety
+///
+/// This method does not ensure anything about dst before copying, so
+/// ensure it is long enough (using rust_ctap2_sign_result_item_len)
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_sign_result_item_copy(
+ res: *const Ctap2SignResult,
+ assertion_idx: usize,
+ item_idx: u8,
+ dst: *mut u8,
+) -> bool {
+ if res.is_null() || dst.is_null() {
+ return false;
+ }
+
+ match &*res {
+ Ok((assertions, _)) => {
+ if assertion_idx >= assertions.0.len() {
+ return false;
+ }
+
+ sign_result_item_copy(&assertions.0[assertion_idx], item_idx, dst)
+ }
+ Err(_) => false,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_sign_result_contains_username(
+ res: *const Ctap2SignResult,
+ assertion_idx: usize,
+) -> bool {
+ if res.is_null() {
+ return false;
+ }
+
+ match &*res {
+ Ok((assertions, _)) => {
+ if assertion_idx >= assertions.0.len() {
+ return false;
+ }
+ assertions.0[assertion_idx]
+ .user
+ .as_ref()
+ .map(|u| u.display_name.as_ref().or(u.name.as_ref()))
+ .is_some()
+ }
+ Err(_) => false,
+ }
+}
+
+/// # Safety
+///
+/// This function is used to get how long the specific username is.
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_sign_result_username_len(
+ res: *const Ctap2SignResult,
+ assertion_idx: usize,
+ len: *mut size_t,
+) -> bool {
+ if res.is_null() || len.is_null() {
+ return false;
+ }
+
+ match &*res {
+ Ok((assertions, _)) => {
+ if assertion_idx >= assertions.0.len() {
+ return false;
+ }
+
+ if let Some(name_len) = assertions.0[assertion_idx]
+ .user
+ .as_ref()
+ .map(|u| u.display_name.as_ref().or(u.name.as_ref()))
+ .flatten()
+ .map(|x| x.as_bytes().len())
+ {
+ *len = name_len;
+ true
+ } else {
+ false
+ }
+ }
+ Err(_) => false,
+ }
+}
+
+/// # Safety
+///
+/// This method does not ensure anything about dst before copying, so
+/// ensure it is long enough (using rust_ctap2_sign_result_username_len)
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_sign_result_username_copy(
+ res: *const Ctap2SignResult,
+ assertion_idx: usize,
+ dst: *mut c_char,
+) -> bool {
+ if res.is_null() || dst.is_null() {
+ return false;
+ }
+
+ match &*res {
+ Ok((assertions, _)) => {
+ if assertion_idx >= assertions.0.len() {
+ return false;
+ }
+
+ if let Some(name) = assertions.0[assertion_idx]
+ .user
+ .as_ref()
+ .map(|u| u.display_name.as_ref().or(u.name.as_ref()))
+ .flatten()
+ .map(|u| CString::new(u.clone()).ok())
+ .flatten()
+ {
+ ptr::copy_nonoverlapping(name.as_ptr(), dst, name.as_bytes().len());
+ true
+ } else {
+ false
+ }
+ }
+
+ Err(_) => false,
+ }
+}
+
+/// # Safety
+///
+/// This function is used to get how long the JSON-representation of a status update is.
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_status_update_len(
+ res: *const StatusUpdate,
+ len: *mut size_t,
+) -> bool {
+ if res.is_null() || len.is_null() {
+ return false;
+ }
+
+ match serde_json::to_string(&*res) {
+ Ok(s) => {
+ *len = s.len();
+ true
+ }
+ Err(e) => {
+ error!("Failed to parse {:?} into json: {:?}", &*res, e);
+ false
+ }
+ }
+}
+
+/// # Safety
+///
+/// This method does not ensure anything about dst before copying, so
+/// ensure it is long enough (using rust_ctap2_status_update_len)
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_status_update_copy_json(
+ res: *const StatusUpdate,
+ dst: *mut c_char,
+) -> bool {
+ if res.is_null() || dst.is_null() {
+ return false;
+ }
+
+ match serde_json::to_string(&*res) {
+ Ok(s) => {
+ if let Ok(cs) = CString::new(s) {
+ ptr::copy_nonoverlapping(cs.as_ptr(), dst, cs.as_bytes().len());
+ true
+ } else {
+ error!("Failed to convert String to CString");
+ false
+ }
+ }
+ Err(e) => {
+ error!("Failed to parse {:?} into json: {:?}", &*res, e);
+ false
+ }
+ }
+}
+
+/// # Safety
+///
+/// We copy the pin, so it is the callers responsibility to free the argument
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_status_update_send_pin(
+ res: *const StatusUpdate,
+ c_pin: *mut c_char,
+) -> bool {
+ if res.is_null() || c_pin.is_null() {
+ return false;
+ }
+
+ match &*res {
+ StatusUpdate::PinError(_, sender) => {
+ if let Ok(pin) = CStr::from_ptr(c_pin).to_str() {
+ sender
+ .send(Pin::new(pin))
+ .map_err(|e| {
+ error!("Failed to send PIN to device-thread");
+ e
+ })
+ .is_ok()
+ } else {
+ error!("Failed to convert PIN from c_char to String");
+ false
+ }
+ }
+ _ => {
+ error!("Wrong state!");
+ false
+ }
+ }
+}
+
+/// # Safety
+///
+/// This function frees the memory of res!
+#[no_mangle]
+pub unsafe extern "C" fn rust_ctap2_destroy_status_update_res(res: *mut StatusUpdate) -> bool {
+ if res.is_null() {
+ return false;
+ }
+ // Dropping it when we go out of scope
+ drop(Box::from_raw(res));
+ true
+}
diff --git a/third_party/rust/authenticator/src/errors.rs b/third_party/rust/authenticator/src/errors.rs
new file mode 100644
index 0000000000..ee8959a9a8
--- /dev/null
+++ b/third_party/rust/authenticator/src/errors.rs
@@ -0,0 +1,140 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+pub use crate::ctap2::commands::{client_pin::PinError, CommandError};
+pub use crate::transport::errors::HIDError;
+use std::fmt;
+use std::io;
+use std::sync::mpsc;
+
+// This composite error type is patterned from Phil Daniels' blog:
+// https://www.philipdaniels.com/blog/2019/defining-rust-error-types/
+
+#[derive(Debug)]
+pub enum UnsupportedOption {
+ MaxPinLength,
+ HmacSecret,
+}
+
+#[derive(Debug)]
+pub enum AuthenticatorError {
+ // Errors from external libraries...
+ Io(io::Error),
+ // Errors raised by us...
+ InvalidRelyingPartyInput,
+ NoConfiguredTransports,
+ Platform,
+ InternalError(String),
+ U2FToken(U2FTokenError),
+ Custom(String),
+ VersionMismatch(&'static str, u32),
+ HIDError(HIDError),
+ CryptoError,
+ PinError(PinError),
+ UnsupportedOption(UnsupportedOption),
+}
+
+impl AuthenticatorError {
+ pub fn as_u2f_errorcode(&self) -> u8 {
+ match *self {
+ AuthenticatorError::U2FToken(ref err) => *err as u8,
+ // TODO: This is somewhat ugly, as we hardcode the error code here, instead of using the
+ // const defined in `u2fhid-capi.h`, which we should.
+ AuthenticatorError::PinError(PinError::PinRequired) => 6u8,
+ AuthenticatorError::PinError(PinError::InvalidPin(_)) => 7u8,
+ AuthenticatorError::PinError(PinError::PinAuthBlocked) => 8u8,
+ AuthenticatorError::PinError(PinError::PinBlocked) => 9u8,
+ _ => U2FTokenError::Unknown as u8,
+ }
+ }
+}
+
+impl std::error::Error for AuthenticatorError {}
+
+impl fmt::Display for AuthenticatorError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ AuthenticatorError::Io(ref err) => err.fmt(f),
+ AuthenticatorError::InvalidRelyingPartyInput => {
+ write!(f, "invalid input from relying party")
+ }
+ AuthenticatorError::NoConfiguredTransports => write!(
+ f,
+ "no transports were configured in the authenticator service"
+ ),
+ AuthenticatorError::Platform => write!(f, "unknown platform error"),
+ AuthenticatorError::InternalError(ref err) => write!(f, "internal error: {}", err),
+ AuthenticatorError::U2FToken(ref err) => {
+ write!(f, "A u2f token error occurred {:?}", err)
+ }
+ AuthenticatorError::Custom(ref err) => write!(f, "A custom error occurred {:?}", err),
+ AuthenticatorError::VersionMismatch(manager, version) => write!(
+ f,
+ "{} expected arguments of version CTAP{}",
+ manager, version
+ ),
+ AuthenticatorError::HIDError(ref e) => write!(f, "Device error: {}", e),
+ AuthenticatorError::CryptoError => {
+ write!(f, "The cryptography implementation encountered an error")
+ }
+ AuthenticatorError::PinError(ref e) => write!(f, "PIN Error: {}", e),
+ AuthenticatorError::UnsupportedOption(ref e) => {
+ write!(f, "Unsupported option: {:?}", e)
+ }
+ }
+ }
+}
+
+impl From<io::Error> for AuthenticatorError {
+ fn from(err: io::Error) -> AuthenticatorError {
+ AuthenticatorError::Io(err)
+ }
+}
+
+impl From<HIDError> for AuthenticatorError {
+ fn from(err: HIDError) -> AuthenticatorError {
+ AuthenticatorError::HIDError(err)
+ }
+}
+
+impl From<CommandError> for AuthenticatorError {
+ fn from(err: CommandError) -> AuthenticatorError {
+ AuthenticatorError::HIDError(HIDError::Command(err))
+ }
+}
+
+impl<T> From<mpsc::SendError<T>> for AuthenticatorError {
+ fn from(err: mpsc::SendError<T>) -> AuthenticatorError {
+ AuthenticatorError::InternalError(err.to_string())
+ }
+}
+
+#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
+pub enum U2FTokenError {
+ Unknown = 1,
+ NotSupported = 2,
+ InvalidState = 3,
+ ConstraintError = 4,
+ NotAllowed = 5,
+}
+
+impl U2FTokenError {
+ fn as_str(&self) -> &str {
+ match *self {
+ U2FTokenError::Unknown => "unknown",
+ U2FTokenError::NotSupported => "not supported",
+ U2FTokenError::InvalidState => "invalid state",
+ U2FTokenError::ConstraintError => "constraint error",
+ U2FTokenError::NotAllowed => "not allowed",
+ }
+ }
+}
+
+impl std::fmt::Display for U2FTokenError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.as_str())
+ }
+}
+
+impl std::error::Error for U2FTokenError {}
diff --git a/third_party/rust/authenticator/src/lib.rs b/third_party/rust/authenticator/src/lib.rs
new file mode 100644
index 0000000000..1b271af315
--- /dev/null
+++ b/third_party/rust/authenticator/src/lib.rs
@@ -0,0 +1,110 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#[macro_use]
+mod util;
+
+#[cfg(any(target_os = "linux"))]
+extern crate libudev;
+
+#[cfg(any(target_os = "freebsd"))]
+extern crate devd_rs;
+
+#[cfg(any(target_os = "macos"))]
+extern crate core_foundation;
+
+extern crate libc;
+#[macro_use]
+extern crate log;
+extern crate rand;
+extern crate runloop;
+
+#[macro_use]
+extern crate bitflags;
+
+pub mod authenticatorservice;
+mod consts;
+mod statemachine;
+mod u2fprotocol;
+mod u2ftypes;
+
+mod manager;
+pub use crate::manager::U2FManager;
+
+mod capi;
+pub use crate::capi::*;
+
+pub mod ctap2;
+pub use ctap2::attestation::AttestationObject;
+pub use ctap2::client_data::{CollectedClientData, CollectedClientDataWrapper};
+pub use ctap2::commands::client_pin::{Pin, PinError};
+pub use ctap2::AssertionObject;
+
+mod ctap2_capi;
+pub use crate::ctap2_capi::*;
+
+pub mod errors;
+pub mod statecallback;
+mod transport;
+mod virtualdevices;
+
+mod status_update;
+pub use status_update::*;
+
+mod crypto;
+pub use crypto::COSEAlgorithm;
+
+// Keep this in sync with the constants in u2fhid-capi.h.
+bitflags! {
+ pub struct RegisterFlags: u64 {
+ const REQUIRE_RESIDENT_KEY = 1;
+ const REQUIRE_USER_VERIFICATION = 2;
+ const REQUIRE_PLATFORM_ATTACHMENT = 4;
+ }
+}
+bitflags! {
+ pub struct SignFlags: u64 {
+ const REQUIRE_USER_VERIFICATION = 1;
+ }
+}
+bitflags! {
+ pub struct AuthenticatorTransports: u8 {
+ const USB = 1;
+ const NFC = 2;
+ const BLE = 4;
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct KeyHandle {
+ pub credential: Vec<u8>,
+ pub transports: AuthenticatorTransports,
+}
+
+pub type AppId = Vec<u8>;
+
+pub enum RegisterResult {
+ CTAP1(Vec<u8>, u2ftypes::U2FDeviceInfo),
+ CTAP2(AttestationObject, CollectedClientDataWrapper),
+}
+
+pub enum SignResult {
+ CTAP1(AppId, Vec<u8>, Vec<u8>, u2ftypes::U2FDeviceInfo),
+ CTAP2(AssertionObject, CollectedClientDataWrapper),
+}
+
+pub type ResetResult = ();
+
+pub type Result<T> = std::result::Result<T, errors::AuthenticatorError>;
+
+#[cfg(test)]
+#[macro_use]
+extern crate assert_matches;
+
+#[cfg(fuzzing)]
+pub use consts::*;
+#[cfg(fuzzing)]
+pub use u2fprotocol::*;
+#[cfg(fuzzing)]
+pub use u2ftypes::*;
diff --git a/third_party/rust/authenticator/src/manager.rs b/third_party/rust/authenticator/src/manager.rs
new file mode 100644
index 0000000000..5b95c252d9
--- /dev/null
+++ b/third_party/rust/authenticator/src/manager.rs
@@ -0,0 +1,576 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::authenticatorservice::AuthenticatorTransport;
+use crate::authenticatorservice::{RegisterArgs, RegisterArgsCtap1, SignArgs};
+use crate::consts::PARAMETER_SIZE;
+use crate::crypto::COSEAlgorithm;
+use crate::ctap2::client_data::{CollectedClientData, WebauthnType};
+use crate::ctap2::commands::get_assertion::{GetAssertion, GetAssertionOptions};
+use crate::ctap2::commands::make_credentials::MakeCredentials;
+use crate::ctap2::commands::make_credentials::MakeCredentialsOptions;
+use crate::ctap2::server::{
+ PublicKeyCredentialParameters, RelyingParty, RelyingPartyWrapper, RpIdHash,
+};
+use crate::errors::*;
+use crate::statecallback::StateCallback;
+use crate::statemachine::{StateMachine, StateMachineCtap2};
+use crate::{Pin, SignFlags};
+use runloop::RunLoop;
+use std::io;
+use std::sync::mpsc::{channel, RecvTimeoutError, Sender};
+use std::time::Duration;
+
+enum QueueAction {
+ RegisterCtap1 {
+ timeout: u64,
+ ctap_args: RegisterArgsCtap1,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::RegisterResult>>,
+ },
+ RegisterCtap2 {
+ timeout: u64,
+ make_credentials: MakeCredentials,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::RegisterResult>>,
+ },
+ SignCtap1 {
+ flags: crate::SignFlags,
+ timeout: u64,
+ challenge: Vec<u8>,
+ app_ids: Vec<crate::AppId>,
+ key_handles: Vec<crate::KeyHandle>,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::SignResult>>,
+ },
+ SignCtap2 {
+ timeout: u64,
+ get_assertion: GetAssertion,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::SignResult>>,
+ },
+ Cancel,
+ Reset {
+ timeout: u64,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ResetResult>>,
+ },
+ SetPin {
+ timeout: u64,
+ new_pin: Pin,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ResetResult>>,
+ },
+}
+
+pub struct U2FManager {
+ queue: RunLoop,
+ tx: Sender<QueueAction>,
+}
+
+impl U2FManager {
+ pub fn new() -> io::Result<Self> {
+ let (tx, rx) = channel();
+
+ // Start a new work queue thread.
+ let queue = RunLoop::new(move |alive| {
+ let mut sm = StateMachine::new();
+
+ while alive() {
+ match rx.recv_timeout(Duration::from_millis(50)) {
+ Ok(QueueAction::RegisterCtap1 {
+ timeout,
+ ctap_args,
+ status,
+ callback,
+ }) => {
+ // This must not block, otherwise we can't cancel.
+ sm.register(
+ ctap_args.flags,
+ timeout,
+ ctap_args.challenge,
+ ctap_args.application,
+ ctap_args.key_handles,
+ status,
+ callback,
+ );
+ }
+ Ok(QueueAction::SignCtap1 {
+ flags,
+ timeout,
+ challenge,
+ app_ids,
+ key_handles,
+ status,
+ callback,
+ }) => {
+ // This must not block, otherwise we can't cancel.
+ sm.sign(
+ flags,
+ timeout,
+ challenge,
+ app_ids,
+ key_handles,
+ status,
+ callback,
+ );
+ }
+ Ok(QueueAction::Cancel) => {
+ // Cancelling must block so that we don't start a new
+ // polling thread before the old one has shut down.
+ sm.cancel();
+ }
+ Ok(QueueAction::RegisterCtap2 { .. }) => {
+ // TODO(MS): What to do here? Error out? Silently ignore?
+ unimplemented!();
+ }
+ Ok(QueueAction::SignCtap2 { .. }) => {
+ // TODO(MS): What to do here? Error out? Silently ignore?
+ unimplemented!();
+ }
+ Ok(QueueAction::Reset { .. }) | Ok(QueueAction::SetPin { .. }) => {
+ unimplemented!();
+ }
+ Err(RecvTimeoutError::Disconnected) => {
+ break;
+ }
+ _ => { /* continue */ }
+ }
+ }
+
+ // Cancel any ongoing activity.
+ sm.cancel();
+ })?;
+
+ Ok(Self { queue, tx })
+ }
+}
+
+impl AuthenticatorTransport for U2FManager {
+ fn register(
+ &mut self,
+ timeout: u64,
+ ctap_args: RegisterArgs,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::RegisterResult>>,
+ ) -> crate::Result<()> {
+ let args = match ctap_args {
+ RegisterArgs::CTAP1(args) => args,
+ RegisterArgs::CTAP2(_) => {
+ return Err(AuthenticatorError::VersionMismatch("U2FManager", 1));
+ }
+ };
+ if args.challenge.len() != PARAMETER_SIZE || args.application.len() != PARAMETER_SIZE {
+ return Err(AuthenticatorError::InvalidRelyingPartyInput);
+ }
+
+ for key_handle in &args.key_handles {
+ if key_handle.credential.len() >= 256 {
+ return Err(AuthenticatorError::InvalidRelyingPartyInput);
+ }
+ }
+
+ let action = QueueAction::RegisterCtap1 {
+ timeout,
+ ctap_args: args,
+ status,
+ callback,
+ };
+ Ok(self.tx.send(action)?)
+ }
+
+ fn sign(
+ &mut self,
+ timeout: u64,
+ ctap_args: SignArgs,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::SignResult>>,
+ ) -> crate::Result<()> {
+ let args = match ctap_args {
+ SignArgs::CTAP1(args) => args,
+ SignArgs::CTAP2(_) => {
+ return Err(AuthenticatorError::VersionMismatch("U2FManager", 1));
+ }
+ };
+
+ if args.challenge.len() != PARAMETER_SIZE {
+ return Err(AuthenticatorError::InvalidRelyingPartyInput);
+ }
+
+ if args.app_ids.is_empty() {
+ return Err(AuthenticatorError::InvalidRelyingPartyInput);
+ }
+
+ for app_id in &args.app_ids {
+ if app_id.len() != PARAMETER_SIZE {
+ return Err(AuthenticatorError::InvalidRelyingPartyInput);
+ }
+ }
+
+ for key_handle in &args.key_handles {
+ if key_handle.credential.len() >= 256 {
+ return Err(AuthenticatorError::InvalidRelyingPartyInput);
+ }
+ }
+
+ let action = QueueAction::SignCtap1 {
+ flags: args.flags,
+ timeout,
+ challenge: args.challenge,
+ app_ids: args.app_ids,
+ key_handles: args.key_handles,
+ status,
+ callback,
+ };
+ Ok(self.tx.send(action)?)
+ }
+
+ fn cancel(&mut self) -> crate::Result<()> {
+ Ok(self.tx.send(QueueAction::Cancel)?)
+ }
+
+ fn reset(
+ &mut self,
+ timeout: u64,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ResetResult>>,
+ ) -> crate::Result<()> {
+ Ok(self.tx.send(QueueAction::Reset {
+ timeout,
+ status,
+ callback,
+ })?)
+ }
+
+ fn set_pin(
+ &mut self,
+ timeout: u64,
+ new_pin: Pin,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ResetResult>>,
+ ) -> crate::Result<()> {
+ Ok(self.tx.send(QueueAction::SetPin {
+ timeout,
+ new_pin,
+ status,
+ callback,
+ })?)
+ }
+}
+
+impl Drop for U2FManager {
+ fn drop(&mut self) {
+ self.queue.cancel();
+ }
+}
+
+pub struct Manager {
+ queue: RunLoop,
+ tx: Sender<QueueAction>,
+}
+
+impl Manager {
+ pub fn new() -> io::Result<Self> {
+ let (tx, rx) = channel();
+
+ // Start a new work queue thread.
+ let queue = RunLoop::new(move |alive| {
+ let mut sm = StateMachineCtap2::new();
+
+ while alive() {
+ match rx.recv_timeout(Duration::from_millis(50)) {
+ Ok(QueueAction::RegisterCtap2 {
+ timeout,
+ make_credentials,
+ status,
+ callback,
+ }) => {
+ // This must not block, otherwise we can't cancel.
+ sm.register(timeout, make_credentials, status, callback);
+ }
+
+ Ok(QueueAction::SignCtap2 {
+ timeout,
+ get_assertion,
+ status,
+ callback,
+ }) => {
+ // This must not block, otherwise we can't cancel.
+ sm.sign(timeout, get_assertion, status, callback);
+ }
+
+ Ok(QueueAction::Cancel) => {
+ // Cancelling must block so that we don't start a new
+ // polling thread before the old one has shut down.
+ sm.cancel();
+ }
+
+ Ok(QueueAction::Reset {
+ timeout,
+ status,
+ callback,
+ }) => {
+ // Reset the token: Delete all keypairs, reset PIN
+ sm.reset(timeout, status, callback);
+ }
+
+ Ok(QueueAction::SetPin {
+ timeout,
+ new_pin,
+ status,
+ callback,
+ }) => {
+ // This must not block, otherwise we can't cancel.
+ sm.set_pin(timeout, new_pin, status, callback);
+ }
+
+ Ok(QueueAction::RegisterCtap1 {
+ timeout: _,
+ ctap_args: _,
+ status: _,
+ callback: _,
+ }) => {
+ // TODO(MS): Remove QueueAction::RegisterCtap1 once U2FManager is deleted.
+ // The repackaging from CTAP1 to CTAP2 happens in self.register()
+ unimplemented!();
+ }
+
+ Ok(QueueAction::SignCtap1 {
+ timeout: _,
+ callback: _,
+ flags: _,
+ challenge: _,
+ app_ids: _,
+ key_handles: _,
+ status: _,
+ }) => {
+ // TODO(MS): Remove QueueAction::SignCtap1 once U2FManager is deleted.
+ // The repackaging from CTAP1 to CTAP2 happens in self.sign()
+ unimplemented!()
+ }
+
+ Err(RecvTimeoutError::Disconnected) => {
+ break;
+ }
+
+ _ => { /* continue */ }
+ }
+ }
+
+ // Cancel any ongoing activity.
+ sm.cancel();
+ })?;
+
+ Ok(Self { queue, tx })
+ }
+}
+
+impl Drop for Manager {
+ fn drop(&mut self) {
+ self.queue.cancel();
+ }
+}
+
+impl AuthenticatorTransport for Manager {
+ fn register(
+ &mut self,
+ timeout: u64,
+ ctap_args: RegisterArgs,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::RegisterResult>>,
+ ) -> Result<(), AuthenticatorError> {
+ let make_credentials = match ctap_args {
+ RegisterArgs::CTAP2(args) => {
+ let client_data = CollectedClientData {
+ webauthn_type: WebauthnType::Create,
+ challenge: args.challenge.into(),
+ origin: args.origin,
+ cross_origin: false,
+ token_binding: None,
+ };
+
+ MakeCredentials::new(
+ client_data,
+ RelyingPartyWrapper::Data(args.relying_party),
+ Some(args.user),
+ args.pub_cred_params,
+ args.exclude_list,
+ args.options,
+ args.extensions,
+ args.pin,
+ // pin_auth will be filled in Statemachine, once we have a device
+ )
+ }
+ RegisterArgs::CTAP1(args) => {
+ let client_data = CollectedClientData {
+ webauthn_type: WebauthnType::Create,
+ challenge: args.challenge.into(),
+ origin: String::new(),
+ cross_origin: false,
+ token_binding: None,
+ };
+
+ MakeCredentials::new(
+ client_data,
+ RelyingPartyWrapper::Hash(RpIdHash::from(&args.application)?),
+ None,
+ vec![PublicKeyCredentialParameters {
+ alg: COSEAlgorithm::ES256,
+ }],
+ args.key_handles
+ .iter()
+ .map(|k| k.into())
+ .collect::<Vec<_>>(),
+ MakeCredentialsOptions {
+ resident_key: None,
+ user_verification: None,
+ },
+ Default::default(),
+ None,
+ )
+ }
+ }?;
+
+ let action = QueueAction::RegisterCtap2 {
+ timeout,
+ make_credentials,
+ status,
+ callback,
+ };
+ Ok(self.tx.send(action)?)
+ }
+
+ fn sign(
+ &mut self,
+ timeout: u64,
+ ctap_args: SignArgs,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::SignResult>>,
+ ) -> crate::Result<()> {
+ match ctap_args {
+ SignArgs::CTAP1(args) => {
+ if args.challenge.len() != PARAMETER_SIZE {
+ return Err(AuthenticatorError::InvalidRelyingPartyInput);
+ }
+
+ if args.app_ids.is_empty() {
+ return Err(AuthenticatorError::InvalidRelyingPartyInput);
+ }
+
+ let client_data = CollectedClientData {
+ webauthn_type: WebauthnType::Get,
+ challenge: args.challenge.into(),
+ origin: String::new(),
+ cross_origin: false,
+ token_binding: None,
+ };
+ let options = if args.flags == SignFlags::empty() {
+ GetAssertionOptions::default()
+ } else {
+ GetAssertionOptions {
+ user_verification: Some(
+ args.flags.contains(SignFlags::REQUIRE_USER_VERIFICATION),
+ ),
+ ..GetAssertionOptions::default()
+ }
+ };
+
+ for app_id in &args.app_ids {
+ if app_id.len() != PARAMETER_SIZE {
+ return Err(AuthenticatorError::InvalidRelyingPartyInput);
+ }
+ for key_handle in &args.key_handles {
+ if key_handle.credential.len() >= 256 {
+ return Err(AuthenticatorError::InvalidRelyingPartyInput);
+ }
+ let rp = RelyingPartyWrapper::Hash(RpIdHash::from(&app_id)?);
+
+ let allow_list = vec![key_handle.into()];
+
+ let get_assertion = GetAssertion::new(
+ client_data.clone(),
+ rp,
+ allow_list,
+ options,
+ Default::default(),
+ None,
+ )?;
+
+ let action = QueueAction::SignCtap2 {
+ timeout,
+ get_assertion,
+ status: status.clone(),
+ callback: callback.clone(),
+ };
+ self.tx.send(action)?;
+ }
+ }
+ }
+
+ SignArgs::CTAP2(args) => {
+ let client_data = CollectedClientData {
+ webauthn_type: WebauthnType::Get,
+ challenge: args.challenge.into(),
+ origin: args.origin,
+ cross_origin: false,
+ token_binding: None,
+ };
+
+ let get_assertion = GetAssertion::new(
+ client_data.clone(),
+ RelyingPartyWrapper::Data(RelyingParty {
+ id: args.relying_party_id,
+ name: None,
+ icon: None,
+ }),
+ args.allow_list,
+ args.options,
+ args.extensions,
+ args.pin,
+ )?;
+
+ let action = QueueAction::SignCtap2 {
+ timeout,
+ get_assertion,
+ status,
+ callback,
+ };
+ self.tx.send(action)?;
+ }
+ };
+ Ok(())
+ }
+
+ fn cancel(&mut self) -> Result<(), AuthenticatorError> {
+ Ok(self.tx.send(QueueAction::Cancel)?)
+ }
+
+ fn reset(
+ &mut self,
+ timeout: u64,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ResetResult>>,
+ ) -> Result<(), AuthenticatorError> {
+ Ok(self.tx.send(QueueAction::Reset {
+ timeout,
+ status,
+ callback,
+ })?)
+ }
+
+ fn set_pin(
+ &mut self,
+ timeout: u64,
+ new_pin: Pin,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ResetResult>>,
+ ) -> crate::Result<()> {
+ Ok(self.tx.send(QueueAction::SetPin {
+ timeout,
+ new_pin,
+ status,
+ callback,
+ })?)
+ }
+}
diff --git a/third_party/rust/authenticator/src/statecallback.rs b/third_party/rust/authenticator/src/statecallback.rs
new file mode 100644
index 0000000000..ce1caf3e7c
--- /dev/null
+++ b/third_party/rust/authenticator/src/statecallback.rs
@@ -0,0 +1,166 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use std::sync::{Arc, Condvar, Mutex};
+
+pub struct StateCallback<T> {
+ callback: Arc<Mutex<Option<Box<dyn Fn(T) + Send>>>>,
+ observer: Arc<Mutex<Option<Box<dyn Fn() + Send>>>>,
+ condition: Arc<(Mutex<bool>, Condvar)>,
+}
+
+impl<T> StateCallback<T> {
+ // This is used for the Condvar, which requires this kind of construction
+ #[allow(clippy::mutex_atomic)]
+ pub fn new(cb: Box<dyn Fn(T) + Send>) -> Self {
+ Self {
+ callback: Arc::new(Mutex::new(Some(cb))),
+ observer: Arc::new(Mutex::new(None)),
+ condition: Arc::new((Mutex::new(true), Condvar::new())),
+ }
+ }
+
+ pub fn add_uncloneable_observer(&mut self, obs: Box<dyn Fn() + Send>) {
+ let mut opt = self.observer.lock().unwrap();
+ if opt.is_some() {
+ error!("Replacing an already-set observer.")
+ }
+ opt.replace(obs);
+ }
+
+ pub fn call(&self, rv: T) {
+ if let Some(cb) = self.callback.lock().unwrap().take() {
+ cb(rv);
+
+ if let Some(obs) = self.observer.lock().unwrap().take() {
+ obs();
+ }
+ }
+
+ let (lock, cvar) = &*self.condition;
+ let mut pending = lock.lock().unwrap();
+ *pending = false;
+ cvar.notify_all();
+ }
+
+ pub fn wait(&self) {
+ let (lock, cvar) = &*self.condition;
+ let _useless_guard = cvar
+ .wait_while(lock.lock().unwrap(), |pending| *pending)
+ .unwrap();
+ }
+}
+
+impl<T> Clone for StateCallback<T> {
+ fn clone(&self) -> Self {
+ Self {
+ callback: self.callback.clone(),
+ observer: Arc::new(Mutex::new(None)),
+ condition: self.condition.clone(),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::StateCallback;
+ use std::sync::atomic::{AtomicUsize, Ordering};
+ use std::sync::{Arc, Barrier};
+ use std::thread;
+
+ #[test]
+ fn test_statecallback_is_single_use() {
+ let counter = Arc::new(AtomicUsize::new(0));
+ let counter_clone = counter.clone();
+ let sc = StateCallback::new(Box::new(move |_| {
+ counter_clone.fetch_add(1, Ordering::SeqCst);
+ }));
+
+ assert_eq!(counter.load(Ordering::SeqCst), 0);
+ for _ in 0..10 {
+ sc.call(());
+ assert_eq!(counter.load(Ordering::SeqCst), 1);
+ }
+
+ for _ in 0..10 {
+ sc.clone().call(());
+ assert_eq!(counter.load(Ordering::SeqCst), 1);
+ }
+ }
+
+ #[test]
+ fn test_statecallback_observer_is_single_use() {
+ let counter = Arc::new(AtomicUsize::new(0));
+ let counter_clone = counter.clone();
+ let mut sc = StateCallback::<()>::new(Box::new(move |_| {}));
+
+ sc.add_uncloneable_observer(Box::new(move || {
+ counter_clone.fetch_add(1, Ordering::SeqCst);
+ }));
+
+ assert_eq!(counter.load(Ordering::SeqCst), 0);
+ for _ in 0..10 {
+ sc.call(());
+ assert_eq!(counter.load(Ordering::SeqCst), 1);
+ }
+
+ for _ in 0..10 {
+ sc.clone().call(());
+ assert_eq!(counter.load(Ordering::SeqCst), 1);
+ }
+ }
+
+ #[test]
+ fn test_statecallback_observer_only_runs_for_completing_callback() {
+ let cb_counter = Arc::new(AtomicUsize::new(0));
+ let cb_counter_clone = cb_counter.clone();
+ let sc = StateCallback::new(Box::new(move |_| {
+ cb_counter_clone.fetch_add(1, Ordering::SeqCst);
+ }));
+
+ let obs_counter = Arc::new(AtomicUsize::new(0));
+
+ for _ in 0..10 {
+ let obs_counter_clone = obs_counter.clone();
+ let mut c = sc.clone();
+ c.add_uncloneable_observer(Box::new(move || {
+ obs_counter_clone.fetch_add(1, Ordering::SeqCst);
+ }));
+
+ c.call(());
+
+ assert_eq!(cb_counter.load(Ordering::SeqCst), 1);
+ assert_eq!(obs_counter.load(Ordering::SeqCst), 1);
+ }
+ }
+
+ #[test]
+ #[allow(clippy::redundant_clone)]
+ fn test_statecallback_observer_unclonable() {
+ let mut sc = StateCallback::<()>::new(Box::new(move |_| {}));
+ sc.add_uncloneable_observer(Box::new(move || {}));
+
+ assert!(sc.observer.lock().unwrap().is_some());
+ // This is deliberate, to force an extra clone
+ assert!(sc.clone().observer.lock().unwrap().is_none());
+ }
+
+ #[test]
+ fn test_statecallback_wait() {
+ let sc = StateCallback::<()>::new(Box::new(move |_| {}));
+ let barrier = Arc::new(Barrier::new(2));
+
+ {
+ let c = sc.clone();
+ let b = barrier.clone();
+ thread::spawn(move || {
+ b.wait();
+ c.call(());
+ });
+ }
+
+ barrier.wait();
+ sc.wait();
+ }
+}
diff --git a/third_party/rust/authenticator/src/statemachine.rs b/third_party/rust/authenticator/src/statemachine.rs
new file mode 100644
index 0000000000..e6a27a7550
--- /dev/null
+++ b/third_party/rust/authenticator/src/statemachine.rs
@@ -0,0 +1,829 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::consts::PARAMETER_SIZE;
+use crate::ctap2::commands::client_pin::{ChangeExistingPin, Pin, PinError, SetNewPin};
+use crate::ctap2::commands::get_assertion::{GetAssertion, GetAssertionResult};
+use crate::ctap2::commands::make_credentials::{MakeCredentials, MakeCredentialsResult};
+use crate::ctap2::commands::reset::Reset;
+use crate::ctap2::commands::{
+ repackage_pin_errors, CommandError, PinAuthCommand, Request, StatusCode,
+};
+use crate::errors::{self, AuthenticatorError, UnsupportedOption};
+use crate::statecallback::StateCallback;
+use crate::transport::device_selector::{
+ BlinkResult, Device, DeviceBuildParameters, DeviceCommand, DeviceSelectorEvent,
+};
+use crate::transport::platform::transaction::Transaction;
+use crate::transport::{errors::HIDError, hid::HIDDevice, FidoDevice, Nonce};
+use crate::u2fprotocol::{u2f_init_device, u2f_is_keyhandle_valid, u2f_register, u2f_sign};
+use crate::u2ftypes::U2FDevice;
+use crate::{send_status, RegisterResult, SignResult, StatusUpdate};
+use std::sync::mpsc::{channel, Sender};
+use std::thread;
+use std::time::Duration;
+
+fn is_valid_transport(transports: crate::AuthenticatorTransports) -> bool {
+ transports.is_empty() || transports.contains(crate::AuthenticatorTransports::USB)
+}
+
+fn find_valid_key_handles<'a, F>(
+ app_ids: &'a [crate::AppId],
+ key_handles: &'a [crate::KeyHandle],
+ mut is_valid: F,
+) -> (&'a crate::AppId, Vec<&'a crate::KeyHandle>)
+where
+ F: FnMut(&Vec<u8>, &crate::KeyHandle) -> bool,
+{
+ // Try all given app_ids in order.
+ for app_id in app_ids {
+ // Find all valid key handles for the current app_id.
+ let valid_handles = key_handles
+ .iter()
+ .filter(|key_handle| is_valid(app_id, key_handle))
+ .collect::<Vec<_>>();
+
+ // If there's at least one, stop.
+ if !valid_handles.is_empty() {
+ return (app_id, valid_handles);
+ }
+ }
+
+ (&app_ids[0], vec![])
+}
+
+#[derive(Default)]
+pub struct StateMachine {
+ transaction: Option<Transaction>,
+}
+
+impl StateMachine {
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ pub fn register(
+ &mut self,
+ flags: crate::RegisterFlags,
+ timeout: u64,
+ challenge: Vec<u8>,
+ application: crate::AppId,
+ key_handles: Vec<crate::KeyHandle>,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::RegisterResult>>,
+ ) {
+ // Abort any prior register/sign calls.
+ self.cancel();
+
+ let cbc = callback.clone();
+
+ let transaction = Transaction::new(
+ timeout,
+ cbc.clone(),
+ status,
+ move |info, _, status, alive| {
+ // Create a new device.
+ let dev = &mut match Device::new(info) {
+ Ok(dev) => dev,
+ _ => return,
+ };
+
+ // Try initializing it.
+ if !dev.is_u2f() || !u2f_init_device(dev) {
+ return;
+ }
+
+ // We currently support none of the authenticator selection
+ // criteria because we can't ask tokens whether they do support
+ // those features. If flags are set, ignore all tokens for now.
+ //
+ // Technically, this is a ConstraintError because we shouldn't talk
+ // to this authenticator in the first place. But the result is the
+ // same anyway.
+ if !flags.is_empty() {
+ return;
+ }
+
+ send_status(
+ &status,
+ crate::StatusUpdate::DeviceAvailable {
+ dev_info: dev.get_device_info(),
+ },
+ );
+
+ // Iterate the exclude list and see if there are any matches.
+ // If so, we'll keep polling the device anyway to test for user
+ // consent, to be consistent with CTAP2 device behavior.
+ let excluded = key_handles.iter().any(|key_handle| {
+ is_valid_transport(key_handle.transports)
+ && u2f_is_keyhandle_valid(
+ dev,
+ &challenge,
+ &application,
+ &key_handle.credential,
+ )
+ .unwrap_or(false) /* no match on failure */
+ });
+
+ while alive() {
+ if excluded {
+ let blank = vec![0u8; PARAMETER_SIZE];
+ if u2f_register(dev, &blank, &blank).is_ok() {
+ callback.call(Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::InvalidState,
+ )));
+ break;
+ }
+ } else if let Ok(bytes) = u2f_register(dev, &challenge, &application) {
+ let dev_info = dev.get_device_info();
+ send_status(
+ &status,
+ crate::StatusUpdate::Success {
+ dev_info: dev.get_device_info(),
+ },
+ );
+ callback.call(Ok(RegisterResult::CTAP1(bytes, dev_info)));
+ break;
+ }
+
+ // Sleep a bit before trying again.
+ thread::sleep(Duration::from_millis(100));
+ }
+
+ send_status(
+ &status,
+ crate::StatusUpdate::DeviceUnavailable {
+ dev_info: dev.get_device_info(),
+ },
+ );
+ },
+ );
+
+ self.transaction = Some(try_or!(transaction, |e| cbc.call(Err(e))));
+ }
+
+ pub fn sign(
+ &mut self,
+ flags: crate::SignFlags,
+ timeout: u64,
+ challenge: Vec<u8>,
+ app_ids: Vec<crate::AppId>,
+ key_handles: Vec<crate::KeyHandle>,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::SignResult>>,
+ ) {
+ // Abort any prior register/sign calls.
+ self.cancel();
+
+ let cbc = callback.clone();
+
+ let transaction = Transaction::new(
+ timeout,
+ cbc.clone(),
+ status,
+ move |info, _, status, alive| {
+ // Create a new device.
+ let dev = &mut match Device::new(info) {
+ Ok(dev) => dev,
+ _ => return,
+ };
+
+ // Try initializing it.
+ if !dev.is_u2f() || !u2f_init_device(dev) {
+ return;
+ }
+
+ // We currently don't support user verification because we can't
+ // ask tokens whether they do support that. If the flag is set,
+ // ignore all tokens for now.
+ //
+ // Technically, this is a ConstraintError because we shouldn't talk
+ // to this authenticator in the first place. But the result is the
+ // same anyway.
+ if !flags.is_empty() {
+ return;
+ }
+
+ // For each appId, try all key handles. If there's at least one
+ // valid key handle for an appId, we'll use that appId below.
+ let (app_id, valid_handles) =
+ find_valid_key_handles(&app_ids, &key_handles, |app_id, key_handle| {
+ u2f_is_keyhandle_valid(dev, &challenge, app_id, &key_handle.credential)
+ .unwrap_or(false) /* no match on failure */
+ });
+
+ // Aggregate distinct transports from all given credentials.
+ let transports = key_handles
+ .iter()
+ .fold(crate::AuthenticatorTransports::empty(), |t, k| {
+ t | k.transports
+ });
+
+ // We currently only support USB. If the RP specifies transports
+ // and doesn't include USB it's probably lying.
+ if !is_valid_transport(transports) {
+ return;
+ }
+
+ send_status(
+ &status,
+ crate::StatusUpdate::DeviceAvailable {
+ dev_info: dev.get_device_info(),
+ },
+ );
+
+ 'outer: while alive() {
+ // If the device matches none of the given key handles
+ // then just make it blink with bogus data.
+ if valid_handles.is_empty() {
+ let blank = vec![0u8; PARAMETER_SIZE];
+ if u2f_register(dev, &blank, &blank).is_ok() {
+ callback.call(Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::InvalidState,
+ )));
+ break;
+ }
+ } else {
+ // Otherwise, try to sign.
+ for key_handle in &valid_handles {
+ if let Ok(bytes) =
+ u2f_sign(dev, &challenge, app_id, &key_handle.credential)
+ {
+ let dev_info = dev.get_device_info();
+ send_status(
+ &status,
+ crate::StatusUpdate::Success {
+ dev_info: dev.get_device_info(),
+ },
+ );
+ callback.call(Ok(SignResult::CTAP1(
+ app_id.clone(),
+ key_handle.credential.clone(),
+ bytes,
+ dev_info,
+ )));
+ break 'outer;
+ }
+ }
+ }
+
+ // Sleep a bit before trying again.
+ thread::sleep(Duration::from_millis(100));
+ }
+
+ send_status(
+ &status,
+ crate::StatusUpdate::DeviceUnavailable {
+ dev_info: dev.get_device_info(),
+ },
+ );
+ },
+ );
+
+ self.transaction = Some(try_or!(transaction, |e| cbc.call(Err(e))));
+ }
+
+ // This blocks.
+ pub fn cancel(&mut self) {
+ if let Some(mut transaction) = self.transaction.take() {
+ transaction.cancel();
+ }
+ }
+}
+
+#[derive(Default)]
+// TODO(MS): To be renamed to `StateMachine` once U2FManager and the original StateMachine can be removed.
+pub struct StateMachineCtap2 {
+ transaction: Option<Transaction>,
+}
+
+impl StateMachineCtap2 {
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ fn init_and_select(
+ info: DeviceBuildParameters,
+ selector: &Sender<DeviceSelectorEvent>,
+ ctap2_only: bool,
+ ) -> Option<Device> {
+ // Create a new device.
+ let mut dev = match Device::new(info) {
+ Ok(dev) => dev,
+ Err((e, id)) => {
+ info!("error happened with device: {}", e);
+ selector.send(DeviceSelectorEvent::NotAToken(id)).ok()?;
+ return None;
+ }
+ };
+
+ // Try initializing it.
+ if let Err(e) = dev.init(Nonce::CreateRandom) {
+ warn!("error while initializing device: {}", e);
+ selector.send(DeviceSelectorEvent::NotAToken(dev.id())).ok();
+ return None;
+ }
+
+ if ctap2_only && dev.get_authenticator_info().is_none() {
+ info!("Device does not support CTAP2");
+ selector.send(DeviceSelectorEvent::NotAToken(dev.id())).ok();
+ return None;
+ }
+
+ let write_only_clone = match dev.clone_device_as_write_only() {
+ Ok(x) => x,
+ Err(_) => {
+ // There is probably something seriously wrong here, if this happens.
+ // So `NotAToken()` is probably too weak a response here.
+ warn!("error while cloning device: {:?}", dev.id());
+ selector
+ .send(DeviceSelectorEvent::NotAToken(dev.id()))
+ .ok()?;
+ return None;
+ }
+ };
+ let (tx, rx) = channel();
+ selector
+ .send(DeviceSelectorEvent::ImAToken((write_only_clone, tx)))
+ .ok()?;
+
+ // Blocking recv. DeviceSelector will tell us what to do
+ loop {
+ match rx.recv() {
+ Ok(DeviceCommand::Blink) => match dev.block_and_blink() {
+ BlinkResult::DeviceSelected => {
+ // User selected us. Let DeviceSelector know, so it can cancel all other
+ // outstanding open blink-requests.
+ selector
+ .send(DeviceSelectorEvent::SelectedToken(dev.id()))
+ .ok()?;
+ break;
+ }
+ BlinkResult::Cancelled => {
+ info!("Device {:?} was not selected", dev.id());
+ return None;
+ }
+ },
+ Ok(DeviceCommand::Removed) => {
+ info!("Device {:?} was removed", dev.id());
+ return None;
+ }
+ Ok(DeviceCommand::Continue) => {
+ break;
+ }
+ Err(_) => {
+ warn!("Error when trying to receive messages from DeviceSelector! Exiting.");
+ return None;
+ }
+ }
+ }
+ Some(dev)
+ }
+
+ fn ask_user_for_pin<U>(
+ error: PinError,
+ status: &Sender<StatusUpdate>,
+ callback: &StateCallback<crate::Result<U>>,
+ ) -> Result<Pin, ()> {
+ info!("PIN Error that requires user interaction detected. Sending it back and waiting for a reply");
+ let (tx, rx) = channel();
+ send_status(status, crate::StatusUpdate::PinError(error.clone(), tx));
+ match rx.recv() {
+ Ok(pin) => Ok(pin),
+ Err(_) => {
+ // recv() can only fail, if the other side is dropping the Sender. We are using this as a trick
+ // to let the callback decide if this PinError is recoverable (e.g. with User input) or not (e.g.
+ // locked token). If it is deemed unrecoverable, we error out the 'normal' way with the same error.
+ error!("Callback dropped the channel, so we forward the error to the results-callback: {:?}", error);
+ callback.call(Err(AuthenticatorError::PinError(error)));
+ return Err(());
+ }
+ }
+ }
+
+ fn determine_pin_auth<T: PinAuthCommand, U>(
+ cmd: &mut T,
+ dev: &mut Device,
+ status: &Sender<StatusUpdate>,
+ callback: &StateCallback<crate::Result<U>>,
+ ) -> Result<(), ()> {
+ loop {
+ match cmd.determine_pin_auth(dev) {
+ Ok(_) => {
+ break;
+ }
+ Err(AuthenticatorError::PinError(e)) => {
+ let pin = Self::ask_user_for_pin(e, status, callback)?;
+ cmd.set_pin(Some(pin));
+ continue;
+ }
+ Err(e) => {
+ error!("Error when determining pinAuth: {:?}", e);
+ callback.call(Err(e));
+ return Err(());
+ }
+ };
+ }
+
+ // CTAP 2.0 spec is a bit vague here, but CTAP 2.1 is very specific, that the request
+ // should either include pinAuth OR uv=true, but not both at the same time.
+ // Do not set user_verification, if pinAuth is provided
+ if cmd.pin_auth().is_some() {
+ cmd.unset_uv_option();
+ }
+
+ Ok(())
+ }
+
+ pub fn register(
+ &mut self,
+ timeout: u64,
+ params: MakeCredentials,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::RegisterResult>>,
+ ) {
+ // Abort any prior register/sign calls.
+ self.cancel();
+ let cbc = callback.clone();
+ let transaction = Transaction::new(
+ timeout,
+ cbc.clone(),
+ status,
+ move |info, selector, status, _alive| {
+ let mut dev = match Self::init_and_select(info, &selector, false) {
+ None => {
+ return;
+ }
+ Some(dev) => dev,
+ };
+
+ info!("Device {:?} continues with the register process", dev.id());
+ // TODO(baloo): not sure about this, have to ask
+ // We currently support none of the authenticator selection
+ // criteria because we can't ask tokens whether they do support
+ // those features. If flags are set, ignore all tokens for now.
+ //
+ // Technically, this is a ConstraintError because we shouldn't talk
+ // to this authenticator in the first place. But the result is the
+ // same anyway.
+ //if !flags.is_empty() {
+ // return;
+ //}
+
+ // TODO(baloo): not sure about this, have to ask
+ // Iterate the exclude list and see if there are any matches.
+ // If so, we'll keep polling the device anyway to test for user
+ // consent, to be consistent with CTAP2 device behavior.
+ //let excluded = key_handles.iter().any(|key_handle| {
+ // is_valid_transport(key_handle.transports)
+ // && u2f_is_keyhandle_valid(dev, &challenge, &application, &key_handle.credential)
+ // .unwrap_or(false) /* no match on failure */
+ //});
+
+ // TODO(MS): This is wasteful, but the current setup with read only-functions doesn't allow me
+ // to modify "params" directly.
+ let mut makecred = params.clone();
+ if params.is_ctap2_request() {
+ // First check if extensions have been requested that are not supported by the device
+ if let Some(true) = params.extensions.hmac_secret {
+ if let Some(auth) = dev.get_authenticator_info() {
+ if !auth.supports_hmac_secret() {
+ callback.call(Err(AuthenticatorError::UnsupportedOption(
+ UnsupportedOption::HmacSecret,
+ )));
+ return;
+ }
+ }
+ }
+
+ // Second, ask for PIN and get the shared secret
+ if Self::determine_pin_auth(&mut makecred, &mut dev, &status, &callback)
+ .is_err()
+ {
+ return;
+ }
+ }
+ debug!("------------------------------------------------------------------");
+ debug!("{:?}", makecred);
+ debug!("------------------------------------------------------------------");
+ let resp = dev.send_msg(&makecred);
+ if resp.is_ok() {
+ send_status(
+ &status,
+ crate::StatusUpdate::Success {
+ dev_info: dev.get_device_info(),
+ },
+ );
+ // The DeviceSelector could already be dead, but it might also wait
+ // for us to respond, in order to cancel all other tokens in case
+ // we skipped the "blinking"-action and went straight for the actual
+ // request.
+ let _ = selector.send(DeviceSelectorEvent::SelectedToken(dev.id()));
+ }
+ match resp {
+ Ok(MakeCredentialsResult::CTAP2(attestation, client_data)) => {
+ callback.call(Ok(RegisterResult::CTAP2(attestation, client_data)))
+ }
+ Ok(MakeCredentialsResult::CTAP1(data)) => {
+ callback.call(Ok(RegisterResult::CTAP1(data, dev.get_device_info())))
+ }
+
+ Err(HIDError::DeviceNotSupported) | Err(HIDError::UnsupportedCommand) => {}
+ Err(HIDError::Command(CommandError::StatusCode(
+ StatusCode::ChannelBusy,
+ _,
+ ))) => {}
+ Err(e) => {
+ warn!("error happened: {}", e);
+ callback.call(Err(AuthenticatorError::HIDError(e)));
+ }
+ }
+ },
+ );
+
+ self.transaction = Some(try_or!(transaction, |e| cbc.call(Err(e))));
+ }
+
+ pub fn sign(
+ &mut self,
+ timeout: u64,
+ params: GetAssertion,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::SignResult>>,
+ ) {
+ // Abort any prior register/sign calls.
+ self.cancel();
+ let cbc = callback.clone();
+
+ let transaction = Transaction::new(
+ timeout,
+ callback.clone(),
+ status,
+ move |info, selector, status, _alive| {
+ let mut dev = match Self::init_and_select(info, &selector, false) {
+ None => {
+ return;
+ }
+ Some(dev) => dev,
+ };
+
+ info!("Device {:?} continues with the signing process", dev.id());
+ // TODO(MS): This is wasteful, but the current setup with read only-functions doesn't allow me
+ // to modify "params" directly.
+ let mut getassertion = params.clone();
+ if params.is_ctap2_request() {
+ // First check if extensions have been requested that are not supported by the device
+ if params.extensions.hmac_secret.is_some() {
+ if let Some(auth) = dev.get_authenticator_info() {
+ if !auth.supports_hmac_secret() {
+ callback.call(Err(AuthenticatorError::UnsupportedOption(
+ UnsupportedOption::HmacSecret,
+ )));
+ return;
+ }
+ }
+ }
+
+ // Second, ask for PIN and get the shared secret
+ if Self::determine_pin_auth(&mut getassertion, &mut dev, &status, &callback)
+ .is_err()
+ {
+ return;
+ }
+
+ // Third, use the shared secret in the extensions, if requested
+ if let Some(extension) = getassertion.extensions.hmac_secret.as_mut() {
+ if let Some(secret) = dev.get_shared_secret() {
+ match extension.calculate(secret) {
+ Ok(x) => x,
+ Err(e) => {
+ callback.call(Err(e));
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ debug!("------------------------------------------------------------------");
+ debug!("{:?}", getassertion);
+ debug!("------------------------------------------------------------------");
+
+ let resp = dev.send_msg(&getassertion);
+ if resp.is_ok() {
+ send_status(
+ &status,
+ crate::StatusUpdate::Success {
+ dev_info: dev.get_device_info(),
+ },
+ );
+ // The DeviceSelector could already be dead, but it might also wait
+ // for us to respond, in order to cancel all other tokens in case
+ // we skipped the "blinking"-action and went straight for the actual
+ // request.
+ let _ = selector.send(DeviceSelectorEvent::SelectedToken(dev.id()));
+ }
+ match resp {
+ Ok(GetAssertionResult::CTAP1(resp)) => {
+ let app_id = getassertion.rp.hash().as_ref().to_vec();
+ let key_handle = getassertion.allow_list[0].id.clone();
+
+ callback.call(Ok(SignResult::CTAP1(
+ app_id,
+ key_handle,
+ resp,
+ dev.get_device_info(),
+ )))
+ }
+ Ok(GetAssertionResult::CTAP2(assertion, client_data)) => {
+ callback.call(Ok(SignResult::CTAP2(assertion, client_data)))
+ }
+ // TODO(baloo): if key_handle is invalid for this device, it
+ // should reply something like:
+ // CTAP2_ERR_INVALID_CREDENTIAL
+ // have to check
+ Err(HIDError::DeviceNotSupported) | Err(HIDError::UnsupportedCommand) => {}
+ Err(HIDError::Command(CommandError::StatusCode(
+ StatusCode::ChannelBusy,
+ _,
+ ))) => {}
+ Err(e) => {
+ warn!("error happened: {}", e);
+ callback.call(Err(AuthenticatorError::HIDError(e)));
+ }
+ }
+ },
+ );
+
+ self.transaction = Some(try_or!(transaction, move |e| cbc.call(Err(e))));
+ }
+
+ // This blocks.
+ pub fn cancel(&mut self) {
+ if let Some(mut transaction) = self.transaction.take() {
+ info!("Statemachine was cancelled. Cancelling transaction now.");
+ transaction.cancel();
+ }
+ }
+
+ pub fn reset(
+ &mut self,
+ timeout: u64,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ResetResult>>,
+ ) {
+ // Abort any prior register/sign calls.
+ self.cancel();
+ let cbc = callback.clone();
+
+ let transaction = Transaction::new(
+ timeout,
+ callback.clone(),
+ status,
+ move |info, selector, status, _alive| {
+ let reset = Reset {};
+ let mut dev = match Self::init_and_select(info, &selector, true) {
+ None => {
+ return;
+ }
+ Some(dev) => dev,
+ };
+
+ info!("Device {:?} continues with the reset process", dev.id());
+ debug!("------------------------------------------------------------------");
+ debug!("{:?}", reset);
+ debug!("------------------------------------------------------------------");
+
+ let resp = dev.send_cbor(&reset);
+ if resp.is_ok() {
+ send_status(
+ &status,
+ crate::StatusUpdate::Success {
+ dev_info: dev.get_device_info(),
+ },
+ );
+ // The DeviceSelector could already be dead, but it might also wait
+ // for us to respond, in order to cancel all other tokens in case
+ // we skipped the "blinking"-action and went straight for the actual
+ // request.
+ let _ = selector.send(DeviceSelectorEvent::SelectedToken(dev.id()));
+ }
+
+ match resp {
+ Ok(()) => callback.call(Ok(())),
+ Err(HIDError::DeviceNotSupported) | Err(HIDError::UnsupportedCommand) => {}
+ Err(HIDError::Command(CommandError::StatusCode(
+ StatusCode::ChannelBusy,
+ _,
+ ))) => {}
+ Err(e) => {
+ warn!("error happened: {}", e);
+ callback.call(Err(AuthenticatorError::HIDError(e)));
+ }
+ }
+ },
+ );
+
+ self.transaction = Some(try_or!(transaction, move |e| cbc.call(Err(e))));
+ }
+
+ pub fn set_pin(
+ &mut self,
+ timeout: u64,
+ new_pin: Pin,
+ status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::ResetResult>>,
+ ) {
+ // Abort any prior register/sign calls.
+ self.cancel();
+
+ let cbc = callback.clone();
+
+ let transaction = Transaction::new(
+ timeout,
+ callback.clone(),
+ status,
+ move |info, selector, status, _alive| {
+ let mut dev = match Self::init_and_select(info, &selector, true) {
+ None => {
+ return;
+ }
+ Some(dev) => dev,
+ };
+
+ let (mut shared_secret, authinfo) = match dev.establish_shared_secret() {
+ Ok(s) => s,
+ Err(e) => {
+ callback.call(Err(AuthenticatorError::HIDError(e)));
+ return;
+ }
+ };
+
+ // With CTAP2.1 we will have an adjustable required length for PINs
+ if new_pin.as_bytes().len() < 4 {
+ callback.call(Err(AuthenticatorError::PinError(PinError::PinIsTooShort)));
+ return;
+ }
+
+ if new_pin.as_bytes().len() > 64 {
+ callback.call(Err(AuthenticatorError::PinError(PinError::PinIsTooLong(
+ new_pin.as_bytes().len(),
+ ))));
+ return;
+ }
+
+ // Check if a client-pin is already set, or if a new one should be created
+ let res = if authinfo.options.client_pin.unwrap_or_default() {
+ let mut res;
+ let mut error = PinError::PinRequired;
+ loop {
+ let current_pin = match Self::ask_user_for_pin(error, &status, &callback) {
+ Ok(pin) => pin,
+ _ => {
+ return;
+ }
+ };
+
+ res = ChangeExistingPin::new(
+ &authinfo,
+ &shared_secret,
+ &current_pin,
+ &new_pin,
+ )
+ .map_err(HIDError::Command)
+ .and_then(|msg| dev.send_cbor(&msg))
+ .map_err(AuthenticatorError::HIDError)
+ .map_err(|e| repackage_pin_errors(&mut dev, e));
+
+ if let Err(AuthenticatorError::PinError(e)) = res {
+ error = e;
+ // We need to re-establish the shared secret for the next round.
+ match dev.establish_shared_secret() {
+ Ok((s, _)) => {
+ shared_secret = s;
+ }
+ Err(e) => {
+ callback.call(Err(AuthenticatorError::HIDError(e)));
+ return;
+ }
+ };
+
+ continue;
+ } else {
+ break;
+ }
+ }
+ res
+ } else {
+ SetNewPin::new(&authinfo, &shared_secret, &new_pin)
+ .map_err(HIDError::Command)
+ .and_then(|msg| dev.send_cbor(&msg))
+ .map_err(AuthenticatorError::HIDError)
+ };
+ callback.call(res);
+ },
+ );
+ self.transaction = Some(try_or!(transaction, move |e| cbc.call(Err(e))));
+ }
+}
diff --git a/third_party/rust/authenticator/src/status_update.rs b/third_party/rust/authenticator/src/status_update.rs
new file mode 100644
index 0000000000..528ca3afb3
--- /dev/null
+++ b/third_party/rust/authenticator/src/status_update.rs
@@ -0,0 +1,101 @@
+use super::{u2ftypes, Pin, PinError};
+use serde::ser::{Serialize, SerializeStruct};
+use std::sync::mpsc::Sender;
+
+#[derive(Debug)]
+pub enum StatusUpdate {
+ /// Device found
+ DeviceAvailable { dev_info: u2ftypes::U2FDeviceInfo },
+ /// Device got removed
+ DeviceUnavailable { dev_info: u2ftypes::U2FDeviceInfo },
+ /// We successfully finished the register or sign request
+ Success { dev_info: u2ftypes::U2FDeviceInfo },
+ /// Sent if a PIN is needed (or was wrong), or some other kind of PIN-related
+ /// error occurred. The Sender is for sending back a PIN (if needed).
+ PinError(PinError, Sender<Pin>),
+ /// Sent, if multiple devices are found and the user has to select one
+ SelectDeviceNotice,
+ /// Sent, once a device was selected (either automatically or by user-interaction)
+ /// and the register or signing process continues with this device
+ DeviceSelected(u2ftypes::U2FDeviceInfo),
+}
+
+impl Serialize for StatusUpdate {
+ fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let mut map = serializer.serialize_struct("StatusUpdate", 1)?;
+ match &*self {
+ StatusUpdate::DeviceAvailable { dev_info } => {
+ map.serialize_field("DeviceAvailable", &dev_info)?
+ }
+ StatusUpdate::DeviceUnavailable { dev_info } => {
+ map.serialize_field("DeviceUnavailable", &dev_info)?
+ }
+ StatusUpdate::Success { dev_info } => map.serialize_field("Success", &dev_info)?,
+ StatusUpdate::PinError(e, _) => map.serialize_field("PinError", &e)?,
+ StatusUpdate::SelectDeviceNotice => map.serialize_field("SelectDeviceNotice", &())?,
+ StatusUpdate::DeviceSelected(dev_info) => {
+ map.serialize_field("DeviceSelected", &dev_info)?
+ }
+ }
+ map.end()
+ }
+}
+
+pub(crate) fn send_status(status: &Sender<StatusUpdate>, msg: StatusUpdate) {
+ match status.send(msg) {
+ Ok(_) => {}
+ Err(e) => error!("Couldn't send status: {:?}", e),
+ };
+}
+
+#[cfg(test)]
+pub mod tests {
+ use crate::consts::U2F_AUTHENTICATE;
+
+ use super::*;
+ use crate::consts::Capability;
+ use serde_json::to_string;
+ use std::sync::mpsc::channel;
+
+ #[test]
+ fn serialize_select() {
+ let st = StatusUpdate::SelectDeviceNotice;
+ let json = to_string(&st).expect("Failed to serialize");
+ assert_eq!(&json, r#"{"SelectDeviceNotice":null}"#);
+ }
+
+ #[test]
+ fn serialize_invalid_pin() {
+ let (tx, _rx) = channel();
+ let st = StatusUpdate::PinError(PinError::InvalidPin(Some(3)), tx.clone());
+ let json = to_string(&st).expect("Failed to serialize");
+ assert_eq!(&json, r#"{"PinError":{"InvalidPin":3}}"#);
+
+ let st = StatusUpdate::PinError(PinError::InvalidPin(None), tx);
+ let json = to_string(&st).expect("Failed to serialize");
+ assert_eq!(&json, r#"{"PinError":{"InvalidPin":null}}"#);
+ }
+
+ #[test]
+ fn serialize_success() {
+ let cap = Capability::WINK | Capability::CBOR;
+ let dev = u2ftypes::U2FDeviceInfo {
+ vendor_name: String::from("ABC").into_bytes(),
+ device_name: String::from("DEF").into_bytes(),
+ version_interface: 2,
+ version_major: 5,
+ version_minor: 4,
+ version_build: 3,
+ cap_flags: cap,
+ };
+ let st = StatusUpdate::Success { dev_info: dev };
+ let json = to_string(&st).expect("Failed to serialize");
+ assert_eq!(
+ &json,
+ r#"{"Success":{"vendor_name":[65,66,67],"device_name":[68,69,70],"version_interface":2,"version_major":5,"version_minor":4,"version_build":3,"cap_flags":{"bits":5}}}"#
+ );
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/device_selector.rs b/third_party/rust/authenticator/src/transport/device_selector.rs
new file mode 100644
index 0000000000..05eea25a5b
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/device_selector.rs
@@ -0,0 +1,520 @@
+use crate::send_status;
+use crate::transport::hid::HIDDevice;
+pub use crate::transport::platform::device::Device;
+use crate::u2ftypes::U2FDevice;
+use runloop::RunLoop;
+use std::collections::{HashMap, HashSet};
+use std::sync::mpsc::{channel, RecvTimeoutError, Sender};
+use std::time::Duration;
+
+pub type DeviceID = <Device as HIDDevice>::Id;
+pub type DeviceBuildParameters = <Device as HIDDevice>::BuildParameters;
+
+trait DeviceSelectorEventMarker {}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum BlinkResult {
+ DeviceSelected,
+ Cancelled,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum DeviceCommand {
+ Blink,
+ Continue,
+ Removed,
+}
+
+#[derive(Debug)]
+pub enum DeviceSelectorEvent {
+ Cancel,
+ Timeout,
+ DevicesAdded(Vec<DeviceID>),
+ DeviceRemoved(DeviceID),
+ NotAToken(DeviceID),
+ ImAToken((Device, Sender<DeviceCommand>)),
+ SelectedToken(DeviceID),
+}
+
+pub struct DeviceSelector {
+ /// How to send a message to the event loop
+ sender: Sender<DeviceSelectorEvent>,
+ /// Thread of the event loop
+ runloop: RunLoop,
+}
+
+impl DeviceSelector {
+ pub fn run(status: Sender<crate::StatusUpdate>) -> Self {
+ let (selector_send, selector_rec) = channel();
+ // let new_device_callback = Arc::new(new_device_cb);
+ let runloop = RunLoop::new(move |alive| {
+ let mut blinking = false;
+ // Device was added, but we wait for its response, if it is a token or not
+ // We save both a write-only copy of the device (for cancellation) and it's thread
+ let mut waiting_for_response = HashSet::new();
+ // All devices that responded with "ImAToken"
+ let mut tokens = HashMap::new();
+ while alive() {
+ let d = Duration::from_secs(100);
+ let res = match selector_rec.recv_timeout(d) {
+ Err(RecvTimeoutError::Disconnected) => {
+ break;
+ }
+ Err(RecvTimeoutError::Timeout) => DeviceSelectorEvent::Timeout,
+ Ok(res) => res,
+ };
+
+ match res {
+ DeviceSelectorEvent::Timeout | DeviceSelectorEvent::Cancel => {
+ /* TODO */
+ Self::cancel_all(tokens, None);
+ break;
+ }
+ DeviceSelectorEvent::SelectedToken(id) => {
+ if let Some(dev) = tokens.keys().find(|d| d.id() == id) {
+ send_status(
+ &status,
+ crate::StatusUpdate::DeviceSelected(dev.get_device_info()),
+ );
+ }
+ Self::cancel_all(tokens, Some(&id));
+ break; // We are done here. The selected device continues without us.
+ }
+ DeviceSelectorEvent::DevicesAdded(ids) => {
+ for id in ids {
+ debug!("Device added event: {:?}", id);
+ waiting_for_response.insert(id);
+ }
+ continue;
+ }
+ DeviceSelectorEvent::DeviceRemoved(id) => {
+ debug!("Device removed event: {:?}", id);
+ if !waiting_for_response.remove(&id) {
+ // Note: We _could_ check here if we had multiple tokens and are already blinking
+ // and the removal of this one leads to only one token left. So we could in theory
+ // stop blinking and select it right away. At the moment, I think this is a
+ // too surprising behavior and therefore, we let the remaining device keep on blinking
+ // since the user could add yet another device, instead of using the remaining one.
+ tokens.iter().for_each(|(dev, send)| {
+ if dev.id() == id {
+ let _ = send.send(DeviceCommand::Removed);
+ send_status(
+ &status,
+ crate::StatusUpdate::DeviceUnavailable {
+ dev_info: dev.get_device_info(),
+ },
+ );
+ }
+ });
+ tokens.retain(|dev, _| dev.id() != id);
+ if tokens.is_empty() {
+ blinking = false;
+ continue;
+ }
+ }
+ // We are already blinking, so no need to run the code below this match
+ // that figures out if we should blink or not. In fact, currently, we do
+ // NOT want to run this code again, because if you have 2 blinking tokens
+ // and one got removed, we WANT the remaining one to continue blinking.
+ // This is a design choice, because I currently think it is the "less surprising"
+ // option to the user.
+ if blinking {
+ continue;
+ }
+ }
+ DeviceSelectorEvent::NotAToken(id) => {
+ debug!("Device not a token event: {:?}", id);
+ waiting_for_response.remove(&id);
+ }
+ DeviceSelectorEvent::ImAToken((dev, tx)) => {
+ send_status(
+ &status,
+ crate::StatusUpdate::DeviceAvailable {
+ dev_info: dev.get_device_info(),
+ },
+ );
+ let id = dev.id();
+ let _ = waiting_for_response.remove(&id);
+ tokens.insert(dev, tx.clone());
+ if blinking {
+ // We are already blinking, so this new device should blink too.
+ if tx.send(DeviceCommand::Blink).is_err() {
+ // Device thread died in the meantime (which shouldn't happen)
+ tokens.retain(|dev, _| dev.id() != id);
+ }
+ continue;
+ }
+ }
+ }
+
+ // All known devices told us, whether they are tokens or not and we have at least one token
+ if waiting_for_response.is_empty() && !tokens.is_empty() {
+ if tokens.len() == 1 {
+ let (dev, tx) = tokens.drain().next().unwrap(); // We just checked that it can't be empty
+ if tx.send(DeviceCommand::Continue).is_err() {
+ // Device thread died in the meantime (which shouldn't happen).
+ // Tokens is empty, so we just start over again
+ continue;
+ }
+ send_status(
+ &status,
+ crate::StatusUpdate::DeviceSelected(dev.get_device_info()),
+ );
+ Self::cancel_all(tokens, Some(&dev.id()));
+ break; // We are done here
+ } else {
+ blinking = true;
+
+ tokens.iter().for_each(|(_dev, tx)| {
+ // A send operation can only fail if the receiving end of a channel is disconnected, implying that the data could never be received.
+ // We ignore errors here for now, but should probably remove the device in such a case (even though it theoretically can't happen)
+ let _ = tx.send(DeviceCommand::Blink);
+ });
+ send_status(&status, crate::StatusUpdate::SelectDeviceNotice);
+ }
+ }
+ }
+ });
+ Self {
+ runloop: runloop.unwrap(), // TODO
+ sender: selector_send,
+ }
+ }
+
+ pub fn clone_sender(&self) -> Sender<DeviceSelectorEvent> {
+ self.sender.clone()
+ }
+
+ fn cancel_all(tokens: HashMap<Device, Sender<DeviceCommand>>, exclude: Option<&DeviceID>) {
+ tokens
+ .into_keys()
+ .filter(|x| exclude.map_or(true, |y| y != &x.id()))
+ .for_each(|mut dev| dev.cancel().unwrap()); // TODO
+ }
+
+ pub fn stop(&mut self) {
+ // We ignore a possible error here, since we don't really care
+ let _ = self.sender.send(DeviceSelectorEvent::Cancel);
+ self.runloop.cancel();
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ use super::*;
+ use crate::{
+ consts::Capability,
+ ctap2::commands::get_info::{AuthenticatorInfo, AuthenticatorOptions},
+ u2ftypes::U2FDeviceInfo,
+ StatusUpdate,
+ };
+ use std::sync::mpsc::Receiver;
+
+ enum ExpectedUpdate {
+ DeviceAvailable,
+ DeviceUnavailable,
+ SelectDeviceNotice,
+ DeviceSelected,
+ }
+
+ fn gen_info(id: String) -> U2FDeviceInfo {
+ U2FDeviceInfo {
+ vendor_name: String::from("ExampleVendor").into_bytes(),
+ device_name: id.into_bytes(),
+ version_interface: 1,
+ version_major: 3,
+ version_minor: 2,
+ version_build: 1,
+ cap_flags: Capability::WINK | Capability::CBOR | Capability::NMSG,
+ }
+ }
+
+ fn make_device_simple_u2f(dev: &mut Device) {
+ dev.set_device_info(gen_info(dev.id()));
+ dev.create_channel();
+ }
+
+ fn make_device_with_pin(dev: &mut Device) {
+ dev.set_device_info(gen_info(dev.id()));
+ dev.create_channel();
+ let info = AuthenticatorInfo {
+ options: AuthenticatorOptions {
+ client_pin: Some(true),
+ ..Default::default()
+ },
+ ..Default::default()
+ };
+ dev.set_authenticator_info(info.clone());
+ }
+
+ fn send_i_am_token(dev: &Device, selector: &DeviceSelector) {
+ selector
+ .sender
+ .send(DeviceSelectorEvent::ImAToken((
+ dev.clone_device_as_write_only().unwrap(),
+ dev.sender.clone().unwrap(),
+ )))
+ .unwrap();
+ }
+
+ fn send_no_token(dev: &Device, selector: &DeviceSelector) {
+ selector
+ .sender
+ .send(DeviceSelectorEvent::NotAToken(dev.id()))
+ .unwrap()
+ }
+
+ fn remove_device(dev: &Device, selector: &DeviceSelector) {
+ selector
+ .sender
+ .send(DeviceSelectorEvent::DeviceRemoved(dev.id()))
+ .unwrap();
+ assert_eq!(
+ dev.receiver.as_ref().unwrap().recv().unwrap(),
+ DeviceCommand::Removed
+ );
+ }
+
+ fn recv_status(dev: &Device, status_rx: &Receiver<StatusUpdate>, expected: ExpectedUpdate) {
+ let res = status_rx.recv().unwrap();
+ // Marking it with _, as cargo warns about "unused exp", as it doesn't view the assert below as a usage
+ let _exp = match expected {
+ ExpectedUpdate::DeviceAvailable => StatusUpdate::DeviceUnavailable {
+ dev_info: gen_info(dev.id()),
+ },
+ ExpectedUpdate::DeviceUnavailable => StatusUpdate::DeviceUnavailable {
+ dev_info: gen_info(dev.id()),
+ },
+ ExpectedUpdate::DeviceSelected => StatusUpdate::DeviceSelected(gen_info(dev.id())),
+ ExpectedUpdate::SelectDeviceNotice => StatusUpdate::SelectDeviceNotice,
+ };
+ assert!(matches!(res, _exp));
+ }
+
+ fn add_devices<'a, T>(iter: T, selector: &DeviceSelector)
+ where
+ T: Iterator<Item = &'a Device>,
+ {
+ selector
+ .sender
+ .send(DeviceSelectorEvent::DevicesAdded(
+ iter.map(|f| f.id()).collect(),
+ ))
+ .unwrap();
+ }
+
+ #[test]
+ fn test_device_selector_one_token_no_late_adds() {
+ let mut devices = vec![
+ Device::new("device selector 1").unwrap(),
+ Device::new("device selector 2").unwrap(),
+ Device::new("device selector 3").unwrap(),
+ Device::new("device selector 4").unwrap(),
+ ];
+
+ // Make those actual tokens. The rest is interpreted as non-u2f-devices
+ make_device_with_pin(&mut devices[2]);
+ let (status_tx, status_rx) = channel();
+ let selector = DeviceSelector::run(status_tx);
+
+ // Adding all
+ add_devices(devices.iter(), &selector);
+ devices.iter_mut().for_each(|d| {
+ if !d.is_u2f() {
+ send_no_token(&d, &selector);
+ }
+ });
+
+ send_i_am_token(&devices[2], &selector);
+
+ recv_status(&devices[2], &status_rx, ExpectedUpdate::DeviceAvailable);
+ assert_eq!(
+ devices[2].receiver.as_ref().unwrap().recv().unwrap(),
+ DeviceCommand::Continue
+ );
+ recv_status(&devices[2], &status_rx, ExpectedUpdate::DeviceSelected);
+ }
+
+ #[test]
+ fn test_device_selector_all_pins_with_late_add() {
+ let mut devices = vec![
+ Device::new("device selector 1").unwrap(),
+ Device::new("device selector 2").unwrap(),
+ Device::new("device selector 3").unwrap(),
+ Device::new("device selector 4").unwrap(),
+ Device::new("device selector 5").unwrap(),
+ Device::new("device selector 6").unwrap(),
+ ];
+
+ // Make those actual tokens. The rest is interpreted as non-u2f-devices
+ make_device_with_pin(&mut devices[2]);
+ make_device_with_pin(&mut devices[4]);
+ make_device_with_pin(&mut devices[5]);
+
+ let (status_tx, status_rx) = channel();
+ let selector = DeviceSelector::run(status_tx);
+
+ // Adding all, except the last one (we simulate that this one is not yet plugged in)
+ add_devices(devices.iter().take(5), &selector);
+
+ // Interleave tokens and non-tokens
+ send_i_am_token(&devices[2], &selector);
+ recv_status(&devices[2], &status_rx, ExpectedUpdate::DeviceAvailable);
+
+ devices.iter_mut().for_each(|d| {
+ if !d.is_u2f() {
+ send_no_token(d, &selector);
+ }
+ });
+
+ send_i_am_token(&devices[4], &selector);
+ recv_status(&devices[4], &status_rx, ExpectedUpdate::DeviceAvailable);
+
+ // We added 2 devices that are tokens. They should get the blink-command now
+ assert_eq!(
+ devices[2].receiver.as_ref().unwrap().recv().unwrap(),
+ DeviceCommand::Blink
+ );
+ assert_eq!(
+ devices[4].receiver.as_ref().unwrap().recv().unwrap(),
+ DeviceCommand::Blink
+ );
+ recv_status(&devices[2], &status_rx, ExpectedUpdate::SelectDeviceNotice);
+
+ // Plug in late device
+ send_i_am_token(&devices[5], &selector);
+ recv_status(&devices[5], &status_rx, ExpectedUpdate::DeviceAvailable);
+ assert_eq!(
+ devices[5].receiver.as_ref().unwrap().recv().unwrap(),
+ DeviceCommand::Blink
+ );
+ }
+
+ #[test]
+ fn test_device_selector_no_pins_late_mixed_adds() {
+ // Multiple tokes, none of them support a PIN
+ let mut devices = vec![
+ Device::new("device selector 1").unwrap(),
+ Device::new("device selector 2").unwrap(),
+ Device::new("device selector 3").unwrap(),
+ Device::new("device selector 4").unwrap(),
+ Device::new("device selector 5").unwrap(),
+ Device::new("device selector 6").unwrap(),
+ Device::new("device selector 7").unwrap(),
+ ];
+
+ // Make those actual tokens. The rest is interpreted as non-u2f-devices
+ make_device_simple_u2f(&mut devices[2]);
+ make_device_simple_u2f(&mut devices[4]);
+ make_device_simple_u2f(&mut devices[5]);
+
+ let (status_tx, status_rx) = channel();
+ let selector = DeviceSelector::run(status_tx);
+
+ // Adding all, except the last one (we simulate that this one is not yet plugged in)
+ add_devices(devices.iter().take(5), &selector);
+
+ // Interleave tokens and non-tokens
+ send_i_am_token(&devices[2], &selector);
+ recv_status(&devices[2], &status_rx, ExpectedUpdate::DeviceAvailable);
+
+ devices.iter_mut().for_each(|d| {
+ if !d.is_u2f() {
+ send_no_token(d, &selector);
+ }
+ });
+
+ send_i_am_token(&devices[4], &selector);
+ recv_status(&devices[4], &status_rx, ExpectedUpdate::DeviceAvailable);
+
+ // We added 2 devices that are tokens. They should get the blink-command now
+ assert_eq!(
+ devices[2].receiver.as_ref().unwrap().recv().unwrap(),
+ DeviceCommand::Blink
+ );
+ assert_eq!(
+ devices[4].receiver.as_ref().unwrap().recv().unwrap(),
+ DeviceCommand::Blink
+ );
+ recv_status(&devices[2], &status_rx, ExpectedUpdate::SelectDeviceNotice);
+
+ // Plug in late device
+ send_i_am_token(&devices[5], &selector);
+ recv_status(&devices[5], &status_rx, ExpectedUpdate::DeviceAvailable);
+ assert_eq!(
+ devices[5].receiver.as_ref().unwrap().recv().unwrap(),
+ DeviceCommand::Blink
+ );
+ // Remove device again
+ remove_device(&devices[5], &selector);
+ recv_status(&devices[5], &status_rx, ExpectedUpdate::DeviceUnavailable);
+
+ // Now we add a token that has a PIN, it should not get "Continue" but "Blink"
+ make_device_with_pin(&mut devices[6]);
+ send_i_am_token(&devices[6], &selector);
+ recv_status(&devices[6], &status_rx, ExpectedUpdate::DeviceAvailable);
+ assert_eq!(
+ devices[6].receiver.as_ref().unwrap().recv().unwrap(),
+ DeviceCommand::Blink
+ );
+ }
+
+ #[test]
+ fn test_device_selector_mixed_pins_remove_all() {
+ // Multiple tokes, none of them support a PIN, so we should get Continue-commands
+ // for all of them
+ let mut devices = vec![
+ Device::new("device selector 1").unwrap(),
+ Device::new("device selector 2").unwrap(),
+ Device::new("device selector 3").unwrap(),
+ Device::new("device selector 4").unwrap(),
+ Device::new("device selector 5").unwrap(),
+ Device::new("device selector 6").unwrap(),
+ ];
+
+ // Make those actual tokens. The rest is interpreted as non-u2f-devices
+ make_device_with_pin(&mut devices[2]);
+ make_device_with_pin(&mut devices[4]);
+ make_device_with_pin(&mut devices[5]);
+
+ let (status_tx, status_rx) = channel();
+ let selector = DeviceSelector::run(status_tx);
+
+ // Adding all, except the last one (we simulate that this one is not yet plugged in)
+ add_devices(devices.iter(), &selector);
+
+ devices.iter_mut().for_each(|d| {
+ if d.is_u2f() {
+ send_i_am_token(d, &selector);
+ } else {
+ send_no_token(d, &selector);
+ }
+ });
+
+ for idx in [2, 4, 5] {
+ recv_status(&devices[idx], &status_rx, ExpectedUpdate::DeviceAvailable);
+ assert_eq!(
+ devices[idx].receiver.as_ref().unwrap().recv().unwrap(),
+ DeviceCommand::Blink
+ );
+ }
+ recv_status(&devices[2], &status_rx, ExpectedUpdate::SelectDeviceNotice);
+
+ // Remove all tokens
+ for idx in [2, 4, 5] {
+ remove_device(&devices[idx], &selector);
+ recv_status(&devices[idx], &status_rx, ExpectedUpdate::DeviceUnavailable);
+ }
+
+ // Adding one again
+ send_i_am_token(&devices[4], &selector);
+ recv_status(&devices[4], &status_rx, ExpectedUpdate::DeviceAvailable);
+
+ // This should now get a "Continue" instead of "Blinking", because it's the only device
+ assert_eq!(
+ devices[4].receiver.as_ref().unwrap().recv().unwrap(),
+ DeviceCommand::Continue
+ );
+ recv_status(&devices[4], &status_rx, ExpectedUpdate::DeviceSelected);
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/errors.rs b/third_party/rust/authenticator/src/transport/errors.rs
new file mode 100644
index 0000000000..463b7cb5cb
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/errors.rs
@@ -0,0 +1,98 @@
+use crate::consts::{SW_CONDITIONS_NOT_SATISFIED, SW_NO_ERROR, SW_WRONG_DATA, SW_WRONG_LENGTH};
+use crate::ctap2::commands::CommandError;
+use std::fmt;
+use std::io;
+use std::path;
+
+#[allow(unused)]
+#[derive(Debug, PartialEq, Eq)]
+pub enum ApduErrorStatus {
+ ConditionsNotSatisfied,
+ WrongData,
+ WrongLength,
+ Unknown([u8; 2]),
+}
+
+impl ApduErrorStatus {
+ pub fn from(status: [u8; 2]) -> Result<(), ApduErrorStatus> {
+ match status {
+ s if s == SW_NO_ERROR => Ok(()),
+ s if s == SW_CONDITIONS_NOT_SATISFIED => Err(ApduErrorStatus::ConditionsNotSatisfied),
+ s if s == SW_WRONG_DATA => Err(ApduErrorStatus::WrongData),
+ s if s == SW_WRONG_LENGTH => Err(ApduErrorStatus::WrongLength),
+ other => Err(ApduErrorStatus::Unknown(other)),
+ }
+ }
+}
+
+impl fmt::Display for ApduErrorStatus {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ ApduErrorStatus::ConditionsNotSatisfied => write!(f, "Apdu: condition not satisfied"),
+ ApduErrorStatus::WrongData => write!(f, "Apdu: wrong data"),
+ ApduErrorStatus::WrongLength => write!(f, "Apdu: wrong length"),
+ ApduErrorStatus::Unknown(ref u) => write!(f, "Apdu: unknown error: {:?}", u),
+ }
+ }
+}
+
+#[allow(unused)]
+#[derive(Debug)]
+pub enum HIDError {
+ /// Transport replied with a status not expected
+ DeviceError,
+ UnexpectedInitReplyLen,
+ NonceMismatch,
+ DeviceNotInitialized,
+ DeviceNotSupported,
+ UnsupportedCommand,
+ UnexpectedVersion,
+ IO(Option<path::PathBuf>, io::Error),
+ UnexpectedCmd(u8),
+ Command(CommandError),
+ ApduStatus(ApduErrorStatus),
+}
+
+impl From<io::Error> for HIDError {
+ fn from(e: io::Error) -> HIDError {
+ HIDError::IO(None, e)
+ }
+}
+
+impl From<CommandError> for HIDError {
+ fn from(e: CommandError) -> HIDError {
+ HIDError::Command(e)
+ }
+}
+
+impl From<ApduErrorStatus> for HIDError {
+ fn from(e: ApduErrorStatus) -> HIDError {
+ HIDError::ApduStatus(e)
+ }
+}
+
+impl fmt::Display for HIDError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ HIDError::UnexpectedInitReplyLen => {
+ write!(f, "Error: Unexpected reply len when initilizaling")
+ }
+ HIDError::NonceMismatch => write!(f, "Error: Nonce mismatch"),
+ HIDError::DeviceError => write!(f, "Error: device returned error"),
+ HIDError::DeviceNotInitialized => write!(f, "Error: using not initiliazed device"),
+ HIDError::DeviceNotSupported => {
+ write!(f, "Error: requested operation is not available on device")
+ }
+ HIDError::UnexpectedVersion => write!(f, "Error: Unexpected protocol version"),
+ HIDError::UnsupportedCommand => {
+ write!(f, "Error: command is not supported on this device")
+ }
+ HIDError::IO(ref p, ref e) => write!(f, "Error: Ioerror({:?}): {}", p, e),
+ HIDError::Command(ref e) => write!(f, "Error: Error issuing command: {}", e),
+ HIDError::UnexpectedCmd(s) => write!(f, "Error: Unexpected status: {}", s),
+ HIDError::ApduStatus(ref status) => {
+ write!(f, "Error: Unexpected apdu status: {:?}", status)
+ }
+ }
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/freebsd/device.rs b/third_party/rust/authenticator/src/transport/freebsd/device.rs
new file mode 100644
index 0000000000..a73b48bab1
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/freebsd/device.rs
@@ -0,0 +1,228 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate libc;
+
+use crate::consts::{CID_BROADCAST, MAX_HID_RPT_SIZE};
+use crate::transport::hid::HIDDevice;
+use crate::transport::platform::uhid;
+use crate::transport::{AuthenticatorInfo, ECDHSecret, FidoDevice, HIDError};
+use crate::u2ftypes::{U2FDevice, U2FDeviceInfo};
+use crate::util::from_unix_result;
+use crate::util::io_err;
+use std::ffi::{CString, OsString};
+use std::hash::{Hash, Hasher};
+use std::io::{self, Read, Write};
+use std::mem;
+use std::os::unix::prelude::*;
+
+#[derive(Debug)]
+pub struct Device {
+ path: OsString,
+ fd: libc::c_int,
+ cid: [u8; 4],
+ dev_info: Option<U2FDeviceInfo>,
+ secret: Option<ECDHSecret>,
+ authenticator_info: Option<AuthenticatorInfo>,
+}
+
+impl Device {
+ fn ping(&mut self) -> io::Result<()> {
+ for i in 0..10 {
+ let mut buf = vec![0u8; 1 + MAX_HID_RPT_SIZE];
+
+ buf[0] = 0; // report number
+ buf[1] = 0xff; // CID_BROADCAST
+ buf[2] = 0xff;
+ buf[3] = 0xff;
+ buf[4] = 0xff;
+ buf[5] = 0x81; // ping
+ buf[6] = 0;
+ buf[7] = 1; // one byte
+
+ self.write(&buf[..])?;
+
+ // Wait for response
+ let mut pfd: libc::pollfd = unsafe { mem::zeroed() };
+ pfd.fd = self.fd;
+ pfd.events = libc::POLLIN;
+ let nfds = unsafe { libc::poll(&mut pfd, 1, 100) };
+ if nfds == -1 {
+ return Err(io::Error::last_os_error());
+ }
+ if nfds == 0 {
+ debug!("device timeout {}", i);
+ continue;
+ }
+
+ // Read response
+ self.read(&mut buf[..])?;
+
+ return Ok(());
+ }
+
+ Err(io_err("no response from device"))
+ }
+}
+
+impl Drop for Device {
+ fn drop(&mut self) {
+ // Close the fd, ignore any errors.
+ let _ = unsafe { libc::close(self.fd) };
+ }
+}
+
+impl PartialEq for Device {
+ fn eq(&self, other: &Device) -> bool {
+ self.path == other.path
+ }
+}
+
+impl Eq for Device {}
+
+impl Hash for Device {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.path.hash(state);
+ }
+}
+
+impl Read for Device {
+ fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+ let bufp = buf.as_mut_ptr() as *mut libc::c_void;
+ let rv = unsafe { libc::read(self.fd, bufp, buf.len()) };
+ from_unix_result(rv as usize)
+ }
+}
+
+impl Write for Device {
+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ let report_id = buf[0] as i64;
+ // Skip report number when not using numbered reports.
+ let start = if report_id == 0x0 { 1 } else { 0 };
+ let data = &buf[start..];
+
+ let data_ptr = data.as_ptr() as *const libc::c_void;
+ let rv = unsafe { libc::write(self.fd, data_ptr, data.len()) };
+ from_unix_result(rv as usize + 1)
+ }
+
+ // USB HID writes don't buffer, so this will be a nop.
+ fn flush(&mut self) -> io::Result<()> {
+ Ok(())
+ }
+}
+
+impl U2FDevice for Device {
+ fn get_cid(&self) -> &[u8; 4] {
+ &self.cid
+ }
+
+ fn set_cid(&mut self, cid: [u8; 4]) {
+ self.cid = cid;
+ }
+
+ fn in_rpt_size(&self) -> usize {
+ MAX_HID_RPT_SIZE
+ }
+
+ fn out_rpt_size(&self) -> usize {
+ MAX_HID_RPT_SIZE
+ }
+
+ fn get_property(&self, _prop_name: &str) -> io::Result<String> {
+ Err(io::Error::new(io::ErrorKind::Other, "Not implemented"))
+ }
+
+ fn get_device_info(&self) -> U2FDeviceInfo {
+ // unwrap is okay, as dev_info must have already been set, else
+ // a programmer error
+ self.dev_info.clone().unwrap()
+ }
+
+ fn set_device_info(&mut self, dev_info: U2FDeviceInfo) {
+ self.dev_info = Some(dev_info);
+ }
+}
+
+impl HIDDevice for Device {
+ type BuildParameters = OsString;
+ type Id = OsString;
+
+ fn new(path: OsString) -> Result<Self, (HIDError, Self::Id)> {
+ let cstr =
+ CString::new(path.as_bytes()).map_err(|_| (HIDError::DeviceError, path.clone()))?;
+ let fd = unsafe { libc::open(cstr.as_ptr(), libc::O_RDWR) };
+ let fd = from_unix_result(fd).map_err(|e| (e.into(), path.clone()))?;
+ let mut res = Self {
+ path,
+ fd,
+ cid: CID_BROADCAST,
+ dev_info: None,
+ secret: None,
+ authenticator_info: None,
+ };
+ if res.is_u2f() {
+ info!("new device {:?}", res.path);
+ Ok(res)
+ } else {
+ Err((HIDError::DeviceNotSupported, res.path.clone()))
+ }
+ }
+
+ fn initialized(&self) -> bool {
+ // During successful init, the broadcast channel id gets repplaced by an actual one
+ self.cid != CID_BROADCAST
+ }
+
+ fn id(&self) -> Self::Id {
+ self.path.clone()
+ }
+
+ fn is_u2f(&mut self) -> bool {
+ if !uhid::is_u2f_device(self.fd) {
+ return false;
+ }
+ if let Err(_) = self.ping() {
+ return false;
+ }
+ true
+ }
+
+ fn get_shared_secret(&self) -> Option<&ECDHSecret> {
+ self.secret.as_ref()
+ }
+
+ fn set_shared_secret(&mut self, secret: ECDHSecret) {
+ self.secret = Some(secret);
+ }
+
+ fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> {
+ self.authenticator_info.as_ref()
+ }
+
+ fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) {
+ self.authenticator_info = Some(authenticator_info);
+ }
+
+ /// This is used for cancellation of blocking read()-requests.
+ /// With this, we can clone the Device, pass it to another thread and call "cancel()" on that.
+ fn clone_device_as_write_only(&self) -> Result<Self, HIDError> {
+ // Try to open the device.
+ // This can't really error out as we already did this conversion
+ let cstr = CString::new(self.path.as_bytes()).map_err(|_| (HIDError::DeviceError))?;
+ let fd = unsafe { libc::open(cstr.as_ptr(), libc::O_WRONLY) };
+ let fd =
+ from_unix_result(fd).map_err(|e| (HIDError::IO(Some(self.path.clone().into()), e)))?;
+ Ok(Self {
+ path: self.path.clone(),
+ fd,
+ cid: self.cid,
+ dev_info: self.dev_info.clone(),
+ secret: self.secret.clone(),
+ authenticator_info: self.authenticator_info.clone(),
+ })
+ }
+}
+
+impl FidoDevice for Device {}
diff --git a/third_party/rust/authenticator/src/transport/freebsd/mod.rs b/third_party/rust/authenticator/src/transport/freebsd/mod.rs
new file mode 100644
index 0000000000..7ed5727157
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/freebsd/mod.rs
@@ -0,0 +1,9 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+pub mod device;
+pub mod transaction;
+
+mod monitor;
+mod uhid;
diff --git a/third_party/rust/authenticator/src/transport/freebsd/monitor.rs b/third_party/rust/authenticator/src/transport/freebsd/monitor.rs
new file mode 100644
index 0000000000..02b2e08110
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/freebsd/monitor.rs
@@ -0,0 +1,165 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::transport::device_selector::DeviceSelectorEvent;
+use devd_rs;
+use runloop::RunLoop;
+use std::collections::HashMap;
+use std::error::Error;
+use std::ffi::OsString;
+use std::sync::{mpsc::Sender, Arc};
+use std::thread;
+use std::time::Duration;
+use std::{fs, io};
+
+const POLL_TIMEOUT: usize = 100;
+
+pub enum Event {
+ Add(OsString),
+ Remove(OsString),
+}
+
+impl Event {
+ fn from_devd(event: devd_rs::Event) -> Option<Self> {
+ match event {
+ devd_rs::Event::Attach {
+ ref dev,
+ parent: _,
+ location: _,
+ } if dev.starts_with("uhid") => Some(Event::Add(("/dev/".to_owned() + dev).into())),
+ devd_rs::Event::Detach {
+ ref dev,
+ parent: _,
+ location: _,
+ } if dev.starts_with("uhid") => Some(Event::Remove(("/dev/".to_owned() + dev).into())),
+ _ => None,
+ }
+ }
+}
+
+fn convert_error(e: devd_rs::Error) -> io::Error {
+ e.into()
+}
+
+pub struct Monitor<F>
+where
+ F: Fn(OsString, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool)
+ + Sync,
+{
+ runloops: HashMap<OsString, RunLoop>,
+ new_device_cb: Arc<F>,
+ selector_sender: Sender<DeviceSelectorEvent>,
+ status_sender: Sender<crate::StatusUpdate>,
+}
+
+impl<F> Monitor<F>
+where
+ F: Fn(OsString, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool)
+ + Send
+ + Sync
+ + 'static,
+{
+ pub fn new(
+ new_device_cb: F,
+ selector_sender: Sender<DeviceSelectorEvent>,
+ status_sender: Sender<crate::StatusUpdate>,
+ ) -> Self {
+ Self {
+ runloops: HashMap::new(),
+ new_device_cb: Arc::new(new_device_cb),
+ selector_sender,
+ status_sender,
+ }
+ }
+
+ pub fn run(&mut self, alive: &dyn Fn() -> bool) -> Result<(), Box<dyn Error>> {
+ let mut ctx = devd_rs::Context::new().map_err(convert_error)?;
+
+ let mut initial_devs = Vec::new();
+ // Iterate all existing devices.
+ for dev in fs::read_dir("/dev")? {
+ if let Ok(dev) = dev {
+ let filename_ = dev.file_name();
+ let filename = filename_.to_str().unwrap_or("");
+ if filename.starts_with("uhid") {
+ let path = OsString::from("/dev/".to_owned() + filename);
+ initial_devs.push(path.clone());
+ self.add_device(path);
+ }
+ }
+ }
+ let _ = self
+ .selector_sender
+ .send(DeviceSelectorEvent::DevicesAdded(initial_devs));
+
+ // Loop until we're stopped by the controlling thread, or fail.
+ while alive() {
+ // Wait for new events, break on failure.
+ match ctx.wait_for_event(POLL_TIMEOUT) {
+ Err(devd_rs::Error::Timeout) => (),
+ Err(e) => return Err(convert_error(e).into()),
+ Ok(event) => {
+ if let Some(event) = Event::from_devd(event) {
+ self.process_event(event);
+ }
+ }
+ }
+ }
+
+ // Remove all tracked devices.
+ self.remove_all_devices();
+
+ Ok(())
+ }
+
+ fn process_event(&mut self, event: Event) {
+ match event {
+ Event::Add(path) => {
+ let _ = self
+ .selector_sender
+ .send(DeviceSelectorEvent::DevicesAdded(vec![path.clone()]));
+ self.add_device(path);
+ }
+ Event::Remove(path) => {
+ self.remove_device(path);
+ }
+ }
+ }
+
+ fn add_device(&mut self, path: OsString) {
+ let f = self.new_device_cb.clone();
+ let selector_sender = self.selector_sender.clone();
+ let status_sender = self.status_sender.clone();
+ let key = path.clone();
+ debug!("Adding device {}", key.to_string_lossy());
+
+ let runloop = RunLoop::new(move |alive| {
+ if alive() {
+ f(path, selector_sender, status_sender, alive);
+ }
+ });
+
+ if let Ok(runloop) = runloop {
+ self.runloops.insert(key, runloop);
+ }
+ }
+
+ fn remove_device(&mut self, path: OsString) {
+ let _ = self
+ .selector_sender
+ .send(DeviceSelectorEvent::DeviceRemoved(path.clone()));
+
+ debug!("Removing device {}", path.to_string_lossy());
+ if let Some(runloop) = self.runloops.remove(&path) {
+ runloop.cancel();
+ }
+ }
+
+ fn remove_all_devices(&mut self) {
+ while !self.runloops.is_empty() {
+ let path = self.runloops.keys().next().unwrap().clone();
+ self.remove_device(path);
+ }
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/freebsd/transaction.rs b/third_party/rust/authenticator/src/transport/freebsd/transaction.rs
new file mode 100644
index 0000000000..35baa8f3c4
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/freebsd/transaction.rs
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::errors;
+use crate::statecallback::StateCallback;
+use crate::transport::device_selector::{
+ DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent,
+};
+use crate::transport::platform::monitor::Monitor;
+use runloop::RunLoop;
+use std::sync::mpsc::Sender;
+
+pub struct Transaction {
+ // Handle to the thread loop.
+ thread: RunLoop,
+ device_selector: DeviceSelector,
+}
+
+impl Transaction {
+ pub fn new<F, T>(
+ timeout: u64,
+ callback: StateCallback<crate::Result<T>>,
+ status: Sender<crate::StatusUpdate>,
+ new_device_cb: F,
+ ) -> crate::Result<Self>
+ where
+ F: Fn(
+ DeviceBuildParameters,
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Sync
+ + Send
+ + 'static,
+ T: 'static,
+ {
+ let status_sender = status.clone();
+ let device_selector = DeviceSelector::run(status);
+ let selector_sender = device_selector.clone_sender();
+ let thread = RunLoop::new_with_timeout(
+ move |alive| {
+ // Create a new device monitor.
+ let mut monitor = Monitor::new(new_device_cb, selector_sender, status_sender);
+
+ // Start polling for new devices.
+ try_or!(monitor.run(alive), |_| callback
+ .call(Err(errors::AuthenticatorError::Platform)));
+
+ // Send an error, if the callback wasn't called already.
+ callback.call(Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::NotAllowed,
+ )));
+ },
+ timeout,
+ )
+ .map_err(|_| errors::AuthenticatorError::Platform)?;
+
+ Ok(Self {
+ thread,
+ device_selector,
+ })
+ }
+
+ pub fn cancel(&mut self) {
+ info!("Transaction was cancelled.");
+ self.device_selector.stop();
+ self.thread.cancel();
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/freebsd/uhid.rs b/third_party/rust/authenticator/src/transport/freebsd/uhid.rs
new file mode 100644
index 0000000000..0fc2131cb1
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/freebsd/uhid.rs
@@ -0,0 +1,92 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate libc;
+
+use std::io;
+use std::os::unix::io::RawFd;
+use std::ptr;
+
+use crate::transport::hidproto::*;
+use crate::util::from_unix_result;
+
+#[allow(non_camel_case_types)]
+#[repr(C)]
+#[derive(Debug)]
+pub struct GenDescriptor {
+ ugd_data: *mut u8,
+ ugd_lang_id: u16,
+ ugd_maxlen: u16,
+ ugd_actlen: u16,
+ ugd_offset: u16,
+ ugd_config_index: u8,
+ ugd_string_index: u8,
+ ugd_iface_index: u8,
+ ugd_altif_index: u8,
+ ugd_endpt_index: u8,
+ ugd_report_index: u8,
+ reserved: [u8; 16],
+}
+
+impl Default for GenDescriptor {
+ fn default() -> GenDescriptor {
+ GenDescriptor {
+ ugd_data: ptr::null_mut(),
+ ugd_lang_id: 0,
+ ugd_maxlen: 65535,
+ ugd_actlen: 0,
+ ugd_offset: 0,
+ ugd_config_index: 0,
+ ugd_string_index: 0,
+ ugd_iface_index: 0,
+ ugd_altif_index: 0,
+ ugd_endpt_index: 0,
+ ugd_report_index: 0,
+ reserved: [0; 16],
+ }
+ }
+}
+
+const IOWR: u32 = 0x40000000 | 0x80000000;
+
+const IOCPARM_SHIFT: u32 = 13;
+const IOCPARM_MASK: u32 = (1 << IOCPARM_SHIFT) - 1;
+
+const TYPESHIFT: u32 = 8;
+const SIZESHIFT: u32 = 16;
+
+macro_rules! ioctl {
+ ($dir:expr, $name:ident, $ioty:expr, $nr:expr, $size:expr; $ty:ty) => {
+ pub unsafe fn $name(fd: libc::c_int, val: *mut $ty) -> io::Result<libc::c_int> {
+ let ioc = ($dir as u32)
+ | (($size as u32 & IOCPARM_MASK) << SIZESHIFT)
+ | (($ioty as u32) << TYPESHIFT)
+ | ($nr as u32);
+ from_unix_result(libc::ioctl(fd, ioc as libc::c_ulong, val))
+ }
+ };
+}
+
+// https://github.com/freebsd/freebsd/blob/master/sys/dev/usb/usb_ioctl.h
+ioctl!(IOWR, usb_get_report_desc, b'U', 21, 32; /*struct*/ GenDescriptor);
+
+fn read_report_descriptor(fd: RawFd) -> io::Result<ReportDescriptor> {
+ let mut desc = GenDescriptor::default();
+ let _ = unsafe { usb_get_report_desc(fd, &mut desc)? };
+ desc.ugd_maxlen = desc.ugd_actlen;
+ let mut value = Vec::with_capacity(desc.ugd_actlen as usize);
+ unsafe {
+ value.set_len(desc.ugd_actlen as usize);
+ }
+ desc.ugd_data = value.as_mut_ptr();
+ let _ = unsafe { usb_get_report_desc(fd, &mut desc)? };
+ Ok(ReportDescriptor { value })
+}
+
+pub fn is_u2f_device(fd: RawFd) -> bool {
+ match read_report_descriptor(fd) {
+ Ok(desc) => has_fido_usage(desc),
+ Err(_) => false, // Upon failure, just say it's not a U2F device.
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/hid.rs b/third_party/rust/authenticator/src/transport/hid.rs
new file mode 100644
index 0000000000..90f40526ae
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/hid.rs
@@ -0,0 +1,136 @@
+use crate::consts::{HIDCmd, CID_BROADCAST};
+use crate::crypto::ECDHSecret;
+use crate::ctap2::commands::get_info::AuthenticatorInfo;
+use crate::transport::{errors::HIDError, Nonce};
+use crate::u2ftypes::{U2FDevice, U2FDeviceInfo, U2FHIDCont, U2FHIDInit, U2FHIDInitResp};
+use rand::{thread_rng, RngCore};
+use std::cmp::Eq;
+use std::fmt;
+use std::hash::Hash;
+use std::io;
+
+pub trait HIDDevice
+where
+ Self: io::Read,
+ Self: io::Write,
+ Self: U2FDevice,
+ Self: Sized,
+ Self: fmt::Debug,
+{
+ type BuildParameters: Sized;
+ type Id: fmt::Debug + PartialEq + Eq + Hash + Sized;
+
+ // Open device, verify that it is indeed a CTAP device and potentially read initial values
+ fn new(parameters: Self::BuildParameters) -> Result<Self, (HIDError, Self::Id)>;
+ fn id(&self) -> Self::Id;
+ fn initialized(&self) -> bool;
+ // Check if the device is actually a token
+ fn is_u2f(&mut self) -> bool;
+ fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo>;
+ fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo);
+ fn set_shared_secret(&mut self, secret: ECDHSecret);
+ fn get_shared_secret(&self) -> Option<&ECDHSecret>;
+ fn clone_device_as_write_only(&self) -> Result<Self, HIDError>;
+
+ // Initialize on a protocol-level
+ fn initialize(&mut self, noncecmd: Nonce) -> Result<(), HIDError> {
+ if self.initialized() {
+ return Ok(());
+ }
+
+ let nonce = match noncecmd {
+ Nonce::Use(x) => x,
+ Nonce::CreateRandom => {
+ let mut nonce = [0u8; 8];
+ thread_rng().fill_bytes(&mut nonce);
+ nonce
+ }
+ };
+
+ // Send Init to broadcast address to create a new channel
+ self.set_cid(CID_BROADCAST);
+ let (cmd, raw) = self.sendrecv(HIDCmd::Init, &nonce)?;
+ if cmd != HIDCmd::Init {
+ return Err(HIDError::DeviceError);
+ }
+
+ let rsp = U2FHIDInitResp::read(&raw, &nonce)?;
+ // Get the new Channel ID
+ self.set_cid(rsp.cid);
+
+ let vendor = self
+ .get_property("Manufacturer")
+ .unwrap_or_else(|_| String::from("Unknown Vendor"));
+ let product = self
+ .get_property("Product")
+ .unwrap_or_else(|_| String::from("Unknown Device"));
+
+ self.set_device_info(U2FDeviceInfo {
+ vendor_name: vendor.as_bytes().to_vec(),
+ device_name: product.as_bytes().to_vec(),
+ version_interface: rsp.version_interface,
+ version_major: rsp.version_major,
+ version_minor: rsp.version_minor,
+ version_build: rsp.version_build,
+ cap_flags: rsp.cap_flags,
+ });
+
+ // A CTAPHID host SHALL accept a response size that is longer than the
+ // anticipated size to allow for future extensions of the protocol, yet
+ // maintaining backwards compatibility. Future versions will maintain
+ // the response structure of the current version, but additional fields
+ // may be added.
+
+ Ok(())
+ }
+
+ fn sendrecv(&mut self, cmd: HIDCmd, send: &[u8]) -> io::Result<(HIDCmd, Vec<u8>)> {
+ let cmd: u8 = cmd.into();
+ self.u2f_write(cmd, send)?;
+ loop {
+ let (cmd, data) = self.u2f_read()?;
+ if cmd != HIDCmd::Keepalive {
+ break Ok((cmd, data));
+ }
+ }
+ }
+
+ fn u2f_write(&mut self, cmd: u8, send: &[u8]) -> io::Result<()> {
+ let mut count = U2FHIDInit::write(self, cmd, send)?;
+
+ // Send continuation packets.
+ let mut sequence = 0u8;
+ while count < send.len() {
+ count += U2FHIDCont::write(self, sequence, &send[count..])?;
+ sequence += 1;
+ }
+
+ Ok(())
+ }
+
+ fn u2f_read(&mut self) -> io::Result<(HIDCmd, Vec<u8>)> {
+ // Now we read. This happens in 2 chunks: The initial packet, which has
+ // the size we expect overall, then continuation packets, which will
+ // fill in data until we have everything.
+ let (cmd, data) = {
+ let (cmd, mut data) = U2FHIDInit::read(self)?;
+
+ trace!("init frame data read: {:04X?}", &data);
+ let mut sequence = 0u8;
+ while data.len() < data.capacity() {
+ let max = data.capacity() - data.len();
+ data.extend_from_slice(&U2FHIDCont::read(self, sequence, max)?);
+ sequence += 1;
+ }
+ (cmd, data)
+ };
+ trace!("u2f_read({:?}) cmd={:?}: {:04X?}", self.id(), cmd, &data);
+ Ok((cmd, data))
+ }
+
+ fn cancel(&mut self) -> Result<(), HIDError> {
+ let cancel: u8 = HIDCmd::Cancel.into();
+ self.u2f_write(cancel, &[])?;
+ Ok(())
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/hidproto.rs b/third_party/rust/authenticator/src/transport/hidproto.rs
new file mode 100644
index 0000000000..5679e6e5d7
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/hidproto.rs
@@ -0,0 +1,253 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Shared code for platforms that use raw HID access (Linux, FreeBSD, etc.)
+
+#![cfg_attr(
+ feature = "cargo-clippy",
+ allow(clippy::cast_lossless, clippy::needless_lifetimes)
+)]
+
+use std::io;
+use std::mem;
+
+use crate::consts::{FIDO_USAGE_PAGE, FIDO_USAGE_U2FHID, INIT_HEADER_SIZE, MAX_HID_RPT_SIZE};
+
+// The 4 MSBs (the tag) are set when it's a long item.
+const HID_MASK_LONG_ITEM_TAG: u8 = 0b1111_0000;
+// The 2 LSBs denote the size of a short item.
+const HID_MASK_SHORT_ITEM_SIZE: u8 = 0b0000_0011;
+// The 6 MSBs denote the tag (4) and type (2).
+const HID_MASK_ITEM_TAGTYPE: u8 = 0b1111_1100;
+// tag=0000, type=10 (local)
+const HID_ITEM_TAGTYPE_USAGE: u8 = 0b0000_1000;
+// tag=0000, type=01 (global)
+const HID_ITEM_TAGTYPE_USAGE_PAGE: u8 = 0b0000_0100;
+// tag=1000, type=00 (main)
+const HID_ITEM_TAGTYPE_INPUT: u8 = 0b1000_0000;
+// tag=1001, type=00 (main)
+const HID_ITEM_TAGTYPE_OUTPUT: u8 = 0b1001_0000;
+// tag=1001, type=01 (global)
+const HID_ITEM_TAGTYPE_REPORT_COUNT: u8 = 0b1001_0100;
+
+pub struct ReportDescriptor {
+ pub value: Vec<u8>,
+}
+
+impl ReportDescriptor {
+ fn iter(self) -> ReportDescriptorIterator {
+ ReportDescriptorIterator::new(self)
+ }
+}
+
+#[derive(Debug)]
+pub enum Data {
+ UsagePage { data: u32 },
+ Usage { data: u32 },
+ Input,
+ Output,
+ ReportCount { data: u32 },
+}
+
+pub struct ReportDescriptorIterator {
+ desc: ReportDescriptor,
+ pos: usize,
+}
+
+impl ReportDescriptorIterator {
+ fn new(desc: ReportDescriptor) -> Self {
+ Self { desc, pos: 0 }
+ }
+
+ fn next_item(&mut self) -> Option<Data> {
+ let item = get_hid_item(&self.desc.value[self.pos..]);
+ if item.is_none() {
+ self.pos = self.desc.value.len(); // Close, invalid data.
+ return None;
+ }
+
+ let (tag_type, key_len, data) = item.unwrap();
+
+ // Advance if we have a valid item.
+ self.pos += key_len + data.len();
+
+ // We only check short items.
+ if key_len > 1 {
+ return None; // Check next item.
+ }
+
+ // Short items have max. length of 4 bytes.
+ assert!(data.len() <= mem::size_of::<u32>());
+
+ // Convert data bytes to a uint.
+ let data = read_uint_le(data);
+ match tag_type {
+ HID_ITEM_TAGTYPE_USAGE_PAGE => Some(Data::UsagePage { data }),
+ HID_ITEM_TAGTYPE_USAGE => Some(Data::Usage { data }),
+ HID_ITEM_TAGTYPE_INPUT => Some(Data::Input),
+ HID_ITEM_TAGTYPE_OUTPUT => Some(Data::Output),
+ HID_ITEM_TAGTYPE_REPORT_COUNT => Some(Data::ReportCount { data }),
+ _ => None,
+ }
+ }
+}
+
+impl Iterator for ReportDescriptorIterator {
+ type Item = Data;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.pos >= self.desc.value.len() {
+ return None;
+ }
+
+ self.next_item().or_else(|| self.next())
+ }
+}
+
+fn get_hid_item<'a>(buf: &'a [u8]) -> Option<(u8, usize, &'a [u8])> {
+ if (buf[0] & HID_MASK_LONG_ITEM_TAG) == HID_MASK_LONG_ITEM_TAG {
+ get_hid_long_item(buf)
+ } else {
+ get_hid_short_item(buf)
+ }
+}
+
+fn get_hid_long_item<'a>(buf: &'a [u8]) -> Option<(u8, usize, &'a [u8])> {
+ // A valid long item has at least three bytes.
+ if buf.len() < 3 {
+ return None;
+ }
+
+ let len = buf[1] as usize;
+
+ // Ensure that there are enough bytes left in the buffer.
+ if len > buf.len() - 3 {
+ return None;
+ }
+
+ Some((buf[2], 3 /* key length */, &buf[3..]))
+}
+
+fn get_hid_short_item<'a>(buf: &'a [u8]) -> Option<(u8, usize, &'a [u8])> {
+ // This is a short item. The bottom two bits of the key
+ // contain the length of the data section (value) for this key.
+ let len = match buf[0] & HID_MASK_SHORT_ITEM_SIZE {
+ s @ 0..=2 => s as usize,
+ _ => 4, /* _ == 3 */
+ };
+
+ // Ensure that there are enough bytes left in the buffer.
+ if len > buf.len() - 1 {
+ return None;
+ }
+
+ Some((
+ buf[0] & HID_MASK_ITEM_TAGTYPE,
+ 1, /* key length */
+ &buf[1..=len],
+ ))
+}
+
+fn read_uint_le(buf: &[u8]) -> u32 {
+ assert!(buf.len() <= 4);
+ // Parse the number in little endian byte order.
+ buf.iter()
+ .rev()
+ .fold(0, |num, b| (num << 8) | (u32::from(*b)))
+}
+
+pub fn has_fido_usage(desc: ReportDescriptor) -> bool {
+ let mut usage_page = None;
+ let mut usage = None;
+
+ for data in desc.iter() {
+ match data {
+ Data::UsagePage { data } => usage_page = Some(data),
+ Data::Usage { data } => usage = Some(data),
+ _ => {}
+ }
+
+ // Check the values we found.
+ if let (Some(usage_page), Some(usage)) = (usage_page, usage) {
+ return usage_page == u32::from(FIDO_USAGE_PAGE)
+ && usage == u32::from(FIDO_USAGE_U2FHID);
+ }
+ }
+
+ false
+}
+
+pub fn read_hid_rpt_sizes(desc: ReportDescriptor) -> io::Result<(usize, usize)> {
+ let mut in_rpt_count = None;
+ let mut out_rpt_count = None;
+ let mut last_rpt_count = None;
+
+ for data in desc.iter() {
+ match data {
+ Data::ReportCount { data } => {
+ if last_rpt_count != None {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "Duplicate HID_ReportCount",
+ ));
+ }
+ last_rpt_count = Some(data as usize);
+ }
+ Data::Input => {
+ if last_rpt_count.is_none() {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "HID_Input should be preceded by HID_ReportCount",
+ ));
+ }
+ if in_rpt_count.is_some() {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "Duplicate HID_ReportCount",
+ ));
+ }
+ in_rpt_count = last_rpt_count;
+ last_rpt_count = None
+ }
+ Data::Output => {
+ if last_rpt_count.is_none() {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "HID_Output should be preceded by HID_ReportCount",
+ ));
+ }
+ if out_rpt_count.is_some() {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "Duplicate HID_ReportCount",
+ ));
+ }
+ out_rpt_count = last_rpt_count;
+ last_rpt_count = None;
+ }
+ _ => {}
+ }
+ }
+
+ match (in_rpt_count, out_rpt_count) {
+ (Some(in_count), Some(out_count)) => {
+ if in_count > INIT_HEADER_SIZE
+ && in_count <= MAX_HID_RPT_SIZE
+ && out_count > INIT_HEADER_SIZE
+ && out_count <= MAX_HID_RPT_SIZE
+ {
+ Ok((in_count, out_count))
+ } else {
+ Err(io::Error::new(
+ io::ErrorKind::InvalidData,
+ "Report size is too small or too large",
+ ))
+ }
+ }
+ _ => Err(io::Error::new(
+ io::ErrorKind::InvalidData,
+ "Failed to extract report sizes from report descriptor",
+ )),
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/linux/device.rs b/third_party/rust/authenticator/src/transport/linux/device.rs
new file mode 100644
index 0000000000..3806a01888
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/device.rs
@@ -0,0 +1,182 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate libc;
+use crate::consts::CID_BROADCAST;
+use crate::transport::hid::HIDDevice;
+use crate::transport::platform::{hidraw, monitor};
+use crate::transport::{AuthenticatorInfo, ECDHSecret, FidoDevice, HIDError};
+use crate::u2ftypes::{U2FDevice, U2FDeviceInfo};
+use crate::util::from_unix_result;
+use std::fs::OpenOptions;
+use std::hash::{Hash, Hasher};
+use std::io;
+use std::io::{Read, Write};
+use std::os::unix::io::AsRawFd;
+use std::path::PathBuf;
+
+#[derive(Debug)]
+pub struct Device {
+ path: PathBuf,
+ fd: std::fs::File,
+ in_rpt_size: usize,
+ out_rpt_size: usize,
+ cid: [u8; 4],
+ dev_info: Option<U2FDeviceInfo>,
+ secret: Option<ECDHSecret>,
+ authenticator_info: Option<AuthenticatorInfo>,
+}
+
+impl PartialEq for Device {
+ fn eq(&self, other: &Device) -> bool {
+ // The path should be the only identifying member for a device
+ // If the path is the same, its the same device
+ self.path == other.path
+ }
+}
+
+impl Eq for Device {}
+
+impl Hash for Device {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ // The path should be the only identifying member for a device
+ // If the path is the same, its the same device
+ self.path.hash(state);
+ }
+}
+
+impl Read for Device {
+ fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+ let bufp = buf.as_mut_ptr() as *mut libc::c_void;
+ let rv = unsafe { libc::read(self.fd.as_raw_fd(), bufp, buf.len()) };
+ from_unix_result(rv as usize)
+ }
+}
+
+impl Write for Device {
+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ let bufp = buf.as_ptr() as *const libc::c_void;
+ let rv = unsafe { libc::write(self.fd.as_raw_fd(), bufp, buf.len()) };
+ from_unix_result(rv as usize)
+ }
+
+ // USB HID writes don't buffer, so this will be a nop.
+ fn flush(&mut self) -> io::Result<()> {
+ Ok(())
+ }
+}
+
+impl U2FDevice for Device {
+ fn get_cid(&self) -> &[u8; 4] {
+ &self.cid
+ }
+
+ fn set_cid(&mut self, cid: [u8; 4]) {
+ self.cid = cid;
+ }
+
+ fn in_rpt_size(&self) -> usize {
+ self.in_rpt_size
+ }
+
+ fn out_rpt_size(&self) -> usize {
+ self.out_rpt_size
+ }
+
+ fn get_property(&self, prop_name: &str) -> io::Result<String> {
+ monitor::get_property_linux(&self.path, prop_name)
+ }
+
+ fn get_device_info(&self) -> U2FDeviceInfo {
+ // unwrap is okay, as dev_info must have already been set, else
+ // a programmer error
+ self.dev_info.clone().unwrap()
+ }
+
+ fn set_device_info(&mut self, dev_info: U2FDeviceInfo) {
+ self.dev_info = Some(dev_info);
+ }
+}
+
+impl HIDDevice for Device {
+ type BuildParameters = PathBuf;
+ type Id = PathBuf;
+
+ fn new(path: PathBuf) -> Result<Self, (HIDError, Self::Id)> {
+ debug!("Opening device {:?}", path);
+ let fd = OpenOptions::new()
+ .read(true)
+ .write(true)
+ .open(&path)
+ .map_err(|e| (HIDError::IO(Some(path.clone()), e), path.clone()))?;
+ let (in_rpt_size, out_rpt_size) = hidraw::read_hid_rpt_sizes_or_defaults(fd.as_raw_fd());
+ let mut res = Self {
+ path,
+ fd,
+ in_rpt_size,
+ out_rpt_size,
+ cid: CID_BROADCAST,
+ dev_info: None,
+ secret: None,
+ authenticator_info: None,
+ };
+ if res.is_u2f() {
+ info!("new device {:?}", res.path);
+ Ok(res)
+ } else {
+ Err((HIDError::DeviceNotSupported, res.path.clone()))
+ }
+ }
+
+ fn initialized(&self) -> bool {
+ // During successful init, the broadcast channel id gets repplaced by an actual one
+ self.cid != CID_BROADCAST
+ }
+
+ fn id(&self) -> Self::Id {
+ self.path.clone()
+ }
+
+ fn is_u2f(&mut self) -> bool {
+ hidraw::is_u2f_device(self.fd.as_raw_fd())
+ }
+
+ fn get_shared_secret(&self) -> Option<&ECDHSecret> {
+ self.secret.as_ref()
+ }
+
+ fn set_shared_secret(&mut self, secret: ECDHSecret) {
+ self.secret = Some(secret);
+ }
+
+ fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> {
+ self.authenticator_info.as_ref()
+ }
+
+ fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) {
+ self.authenticator_info = Some(authenticator_info);
+ }
+
+ /// This is used for cancellation of blocking read()-requests.
+ /// With this, we can clone the Device, pass it to another thread and call "cancel()" on that.
+ fn clone_device_as_write_only(&self) -> Result<Self, HIDError> {
+ let fd = OpenOptions::new()
+ .write(true)
+ .open(&self.path)
+ .map_err(|e| (HIDError::IO(Some(self.path.clone()), e)))?;
+
+ Ok(Self {
+ path: self.path.clone(),
+ fd,
+ in_rpt_size: self.in_rpt_size,
+ out_rpt_size: self.out_rpt_size,
+ cid: self.cid,
+ dev_info: self.dev_info.clone(),
+ secret: self.secret.clone(),
+ authenticator_info: self.authenticator_info.clone(),
+ })
+ }
+}
+
+impl FidoDevice for Device {}
diff --git a/third_party/rust/authenticator/src/transport/linux/hidraw.rs b/third_party/rust/authenticator/src/transport/linux/hidraw.rs
new file mode 100644
index 0000000000..16d687f358
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/hidraw.rs
@@ -0,0 +1,80 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#![cfg_attr(feature = "cargo-clippy", allow(clippy::cast_lossless))]
+
+extern crate libc;
+
+use std::io;
+use std::os::unix::io::RawFd;
+
+use super::hidwrapper::{_HIDIOCGRDESC, _HIDIOCGRDESCSIZE};
+use crate::consts::MAX_HID_RPT_SIZE;
+use crate::transport::hidproto::*;
+use crate::util::{from_unix_result, io_err};
+
+#[allow(non_camel_case_types)]
+#[repr(C)]
+pub struct LinuxReportDescriptor {
+ size: ::libc::c_int,
+ value: [u8; 4096],
+}
+
+const HID_MAX_DESCRIPTOR_SIZE: usize = 4096;
+
+#[cfg(not(target_env = "musl"))]
+type IocType = libc::c_ulong;
+#[cfg(target_env = "musl")]
+type IocType = libc::c_int;
+
+pub unsafe fn hidiocgrdescsize(
+ fd: libc::c_int,
+ val: *mut ::libc::c_int,
+) -> io::Result<libc::c_int> {
+ from_unix_result(libc::ioctl(fd, _HIDIOCGRDESCSIZE as IocType, val))
+}
+
+pub unsafe fn hidiocgrdesc(
+ fd: libc::c_int,
+ val: *mut LinuxReportDescriptor,
+) -> io::Result<libc::c_int> {
+ from_unix_result(libc::ioctl(fd, _HIDIOCGRDESC as IocType, val))
+}
+
+pub fn is_u2f_device(fd: RawFd) -> bool {
+ match read_report_descriptor(fd) {
+ Ok(desc) => has_fido_usage(desc),
+ Err(_) => false, // Upon failure, just say it's not a U2F device.
+ }
+}
+
+pub fn read_hid_rpt_sizes_or_defaults(fd: RawFd) -> (usize, usize) {
+ let default_rpt_sizes = (MAX_HID_RPT_SIZE, MAX_HID_RPT_SIZE);
+ let desc = read_report_descriptor(fd);
+ if let Ok(desc) = desc {
+ if let Ok(rpt_sizes) = read_hid_rpt_sizes(desc) {
+ rpt_sizes
+ } else {
+ default_rpt_sizes
+ }
+ } else {
+ default_rpt_sizes
+ }
+}
+
+fn read_report_descriptor(fd: RawFd) -> io::Result<ReportDescriptor> {
+ let mut desc = LinuxReportDescriptor {
+ size: 0,
+ value: [0; HID_MAX_DESCRIPTOR_SIZE],
+ };
+
+ let _ = unsafe { hidiocgrdescsize(fd, &mut desc.size)? };
+ if desc.size == 0 || desc.size as usize > desc.value.len() {
+ return Err(io_err("unexpected hidiocgrdescsize() result"));
+ }
+
+ let _ = unsafe { hidiocgrdesc(fd, &mut desc)? };
+ let mut value = Vec::from(&desc.value[..]);
+ value.truncate(desc.size as usize);
+ Ok(ReportDescriptor { value })
+}
diff --git a/third_party/rust/authenticator/src/transport/linux/hidwrapper.h b/third_party/rust/authenticator/src/transport/linux/hidwrapper.h
new file mode 100644
index 0000000000..ce77e0f1ca
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/hidwrapper.h
@@ -0,0 +1,12 @@
+#include<sys/ioctl.h>
+#include<linux/hidraw.h>
+
+/* we define these constants to work around the fact that bindgen
+ can't deal with the _IOR macro function. We let cpp deal with it
+ for us. */
+
+const __u32 _HIDIOCGRDESCSIZE = HIDIOCGRDESCSIZE;
+#undef HIDIOCGRDESCSIZE
+
+const __u32 _HIDIOCGRDESC = HIDIOCGRDESC;
+#undef HIDIOCGRDESC
diff --git a/third_party/rust/authenticator/src/transport/linux/hidwrapper.rs b/third_party/rust/authenticator/src/transport/linux/hidwrapper.rs
new file mode 100644
index 0000000000..82aabc6301
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/hidwrapper.rs
@@ -0,0 +1,51 @@
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+// sadly we need this file so we can avoid the suprious warnings that
+// would come with bindgen, as well as to avoid cluttering the mod.rs
+// with spurious architecture specific modules.
+
+#[cfg(target_arch = "x86")]
+include!("ioctl_x86.rs");
+
+#[cfg(target_arch = "x86_64")]
+include!("ioctl_x86_64.rs");
+
+#[cfg(all(target_arch = "mips", target_endian = "little"))]
+include!("ioctl_mipsle.rs");
+
+#[cfg(all(target_arch = "mips", target_endian = "big"))]
+include!("ioctl_mipsbe.rs");
+
+#[cfg(all(target_arch = "mips64", target_endian = "little"))]
+include!("ioctl_mips64le.rs");
+
+#[cfg(all(target_arch = "powerpc", target_endian = "little"))]
+include!("ioctl_powerpcle.rs");
+
+#[cfg(all(target_arch = "powerpc", target_endian = "big"))]
+include!("ioctl_powerpcbe.rs");
+
+#[cfg(all(target_arch = "powerpc64", target_endian = "little"))]
+include!("ioctl_powerpc64le.rs");
+
+#[cfg(all(target_arch = "powerpc64", target_endian = "big"))]
+include!("ioctl_powerpc64be.rs");
+
+#[cfg(all(target_arch = "arm", target_endian = "little"))]
+include!("ioctl_armle.rs");
+
+#[cfg(all(target_arch = "arm", target_endian = "big"))]
+include!("ioctl_armbe.rs");
+
+#[cfg(all(target_arch = "aarch64", target_endian = "little"))]
+include!("ioctl_aarch64le.rs");
+
+#[cfg(all(target_arch = "aarch64", target_endian = "big"))]
+include!("ioctl_aarch64be.rs");
+
+#[cfg(all(target_arch = "s390x", target_endian = "big"))]
+include!("ioctl_s390xbe.rs");
+
+#[cfg(all(target_arch = "riscv64", target_endian = "little"))]
+include!("ioctl_riscv64.rs");
diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_aarch64le.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_aarch64le.rs
new file mode 100644
index 0000000000..a784e9bf46
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/ioctl_aarch64le.rs
@@ -0,0 +1,5 @@
+/* automatically generated by rust-bindgen */
+
+pub type __u32 = ::std::os::raw::c_uint;
+pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225;
+pub const _HIDIOCGRDESC: __u32 = 2416199682;
diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_armle.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_armle.rs
new file mode 100644
index 0000000000..a784e9bf46
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/ioctl_armle.rs
@@ -0,0 +1,5 @@
+/* automatically generated by rust-bindgen */
+
+pub type __u32 = ::std::os::raw::c_uint;
+pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225;
+pub const _HIDIOCGRDESC: __u32 = 2416199682;
diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_mips64le.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_mips64le.rs
new file mode 100644
index 0000000000..1ca187fa1f
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/ioctl_mips64le.rs
@@ -0,0 +1,5 @@
+/* automatically generated by rust-bindgen */
+
+pub type __u32 = ::std::os::raw::c_uint;
+pub const _HIDIOCGRDESCSIZE: __u32 = 1074022401;
+pub const _HIDIOCGRDESC: __u32 = 1342457858;
diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_mipsbe.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_mipsbe.rs
new file mode 100644
index 0000000000..1ca187fa1f
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/ioctl_mipsbe.rs
@@ -0,0 +1,5 @@
+/* automatically generated by rust-bindgen */
+
+pub type __u32 = ::std::os::raw::c_uint;
+pub const _HIDIOCGRDESCSIZE: __u32 = 1074022401;
+pub const _HIDIOCGRDESC: __u32 = 1342457858;
diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_mipsle.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_mipsle.rs
new file mode 100644
index 0000000000..1ca187fa1f
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/ioctl_mipsle.rs
@@ -0,0 +1,5 @@
+/* automatically generated by rust-bindgen */
+
+pub type __u32 = ::std::os::raw::c_uint;
+pub const _HIDIOCGRDESCSIZE: __u32 = 1074022401;
+pub const _HIDIOCGRDESC: __u32 = 1342457858;
diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64be.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64be.rs
new file mode 100644
index 0000000000..1ca187fa1f
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64be.rs
@@ -0,0 +1,5 @@
+/* automatically generated by rust-bindgen */
+
+pub type __u32 = ::std::os::raw::c_uint;
+pub const _HIDIOCGRDESCSIZE: __u32 = 1074022401;
+pub const _HIDIOCGRDESC: __u32 = 1342457858;
diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64le.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64le.rs
new file mode 100644
index 0000000000..1ca187fa1f
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/ioctl_powerpc64le.rs
@@ -0,0 +1,5 @@
+/* automatically generated by rust-bindgen */
+
+pub type __u32 = ::std::os::raw::c_uint;
+pub const _HIDIOCGRDESCSIZE: __u32 = 1074022401;
+pub const _HIDIOCGRDESC: __u32 = 1342457858;
diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_powerpcbe.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_powerpcbe.rs
new file mode 100644
index 0000000000..1ca187fa1f
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/ioctl_powerpcbe.rs
@@ -0,0 +1,5 @@
+/* automatically generated by rust-bindgen */
+
+pub type __u32 = ::std::os::raw::c_uint;
+pub const _HIDIOCGRDESCSIZE: __u32 = 1074022401;
+pub const _HIDIOCGRDESC: __u32 = 1342457858;
diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_riscv64.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_riscv64.rs
new file mode 100644
index 0000000000..a784e9bf46
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/ioctl_riscv64.rs
@@ -0,0 +1,5 @@
+/* automatically generated by rust-bindgen */
+
+pub type __u32 = ::std::os::raw::c_uint;
+pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225;
+pub const _HIDIOCGRDESC: __u32 = 2416199682;
diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_s390xbe.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_s390xbe.rs
new file mode 100644
index 0000000000..a784e9bf46
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/ioctl_s390xbe.rs
@@ -0,0 +1,5 @@
+/* automatically generated by rust-bindgen */
+
+pub type __u32 = ::std::os::raw::c_uint;
+pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225;
+pub const _HIDIOCGRDESC: __u32 = 2416199682;
diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_x86.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_x86.rs
new file mode 100644
index 0000000000..a784e9bf46
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/ioctl_x86.rs
@@ -0,0 +1,5 @@
+/* automatically generated by rust-bindgen */
+
+pub type __u32 = ::std::os::raw::c_uint;
+pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225;
+pub const _HIDIOCGRDESC: __u32 = 2416199682;
diff --git a/third_party/rust/authenticator/src/transport/linux/ioctl_x86_64.rs b/third_party/rust/authenticator/src/transport/linux/ioctl_x86_64.rs
new file mode 100644
index 0000000000..a784e9bf46
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/ioctl_x86_64.rs
@@ -0,0 +1,5 @@
+/* automatically generated by rust-bindgen */
+
+pub type __u32 = ::std::os::raw::c_uint;
+pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225;
+pub const _HIDIOCGRDESC: __u32 = 2416199682;
diff --git a/third_party/rust/authenticator/src/transport/linux/mod.rs b/third_party/rust/authenticator/src/transport/linux/mod.rs
new file mode 100644
index 0000000000..c4d490ecee
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/mod.rs
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#![allow(clippy::unreadable_literal)]
+
+pub mod device;
+pub mod transaction;
+
+mod hidraw;
+mod hidwrapper;
+mod monitor;
diff --git a/third_party/rust/authenticator/src/transport/linux/monitor.rs b/third_party/rust/authenticator/src/transport/linux/monitor.rs
new file mode 100644
index 0000000000..003a034c87
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/monitor.rs
@@ -0,0 +1,194 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::transport::device_selector::DeviceSelectorEvent;
+use libc::{c_int, c_short, c_ulong};
+use libudev::EventType;
+use runloop::RunLoop;
+use std::collections::HashMap;
+use std::error::Error;
+use std::io;
+use std::os::unix::io::AsRawFd;
+use std::path::PathBuf;
+use std::sync::{mpsc::Sender, Arc};
+
+const UDEV_SUBSYSTEM: &str = "hidraw";
+const POLLIN: c_short = 0x0001;
+const POLL_TIMEOUT: c_int = 100;
+
+fn poll(fds: &mut Vec<::libc::pollfd>) -> io::Result<()> {
+ let nfds = fds.len() as c_ulong;
+
+ let rv = unsafe { ::libc::poll((&mut fds[..]).as_mut_ptr(), nfds, POLL_TIMEOUT) };
+
+ if rv < 0 {
+ Err(io::Error::from_raw_os_error(rv))
+ } else {
+ Ok(())
+ }
+}
+
+pub struct Monitor<F>
+where
+ F: Fn(PathBuf, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool)
+ + Sync,
+{
+ runloops: HashMap<PathBuf, RunLoop>,
+ new_device_cb: Arc<F>,
+ selector_sender: Sender<DeviceSelectorEvent>,
+ status_sender: Sender<crate::StatusUpdate>,
+}
+
+impl<F> Monitor<F>
+where
+ F: Fn(PathBuf, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool)
+ + Send
+ + Sync
+ + 'static,
+{
+ pub fn new(
+ new_device_cb: F,
+ selector_sender: Sender<DeviceSelectorEvent>,
+ status_sender: Sender<crate::StatusUpdate>,
+ ) -> Self {
+ Self {
+ runloops: HashMap::new(),
+ new_device_cb: Arc::new(new_device_cb),
+ selector_sender,
+ status_sender,
+ }
+ }
+
+ pub fn run(&mut self, alive: &dyn Fn() -> bool) -> Result<(), Box<dyn Error>> {
+ let ctx = libudev::Context::new()?;
+
+ let mut enumerator = libudev::Enumerator::new(&ctx)?;
+ enumerator.match_subsystem(UDEV_SUBSYSTEM)?;
+
+ // Iterate all existing devices.
+ let paths: Vec<PathBuf> = enumerator
+ .scan_devices()?
+ .filter_map(|dev| dev.devnode().map(|p| p.to_owned()))
+ .collect();
+
+ // Add them all in one go to avoid race conditions in DeviceSelector
+ // (8 devices should be added, but the first returns already before all
+ // others are known to DeviceSelector)
+ self.selector_sender
+ .send(DeviceSelectorEvent::DevicesAdded(paths.clone()))?;
+ for path in paths {
+ self.add_device(path);
+ }
+
+ let mut monitor = libudev::Monitor::new(&ctx)?;
+ monitor.match_subsystem(UDEV_SUBSYSTEM)?;
+
+ // Start listening for new devices.
+ let mut socket = monitor.listen()?;
+ let mut fds = vec![::libc::pollfd {
+ fd: socket.as_raw_fd(),
+ events: POLLIN,
+ revents: 0,
+ }];
+
+ while alive() {
+ // Wait for new events, break on failure.
+ poll(&mut fds)?;
+
+ if let Some(event) = socket.receive_event() {
+ self.process_event(&event);
+ }
+ }
+
+ // Remove all tracked devices.
+ self.remove_all_devices();
+
+ Ok(())
+ }
+
+ fn process_event(&mut self, event: &libudev::Event) {
+ let path = event.device().devnode().map(|dn| dn.to_owned());
+
+ match (event.event_type(), path) {
+ (EventType::Add, Some(path)) => {
+ let _ = self
+ .selector_sender
+ .send(DeviceSelectorEvent::DevicesAdded(vec![path.clone()]));
+ self.add_device(path);
+ }
+ (EventType::Remove, Some(path)) => {
+ self.remove_device(&path);
+ }
+ _ => { /* ignore other types and failures */ }
+ }
+ }
+
+ fn add_device(&mut self, path: PathBuf) {
+ let f = self.new_device_cb.clone();
+ let key = path.clone();
+ let selector_sender = self.selector_sender.clone();
+ let status_sender = self.status_sender.clone();
+ debug!("Adding device {}", path.to_string_lossy());
+
+ let runloop = RunLoop::new(move |alive| {
+ if alive() {
+ f(path, selector_sender, status_sender, alive);
+ }
+ });
+
+ if let Ok(runloop) = runloop {
+ self.runloops.insert(key, runloop);
+ }
+ }
+
+ fn remove_device(&mut self, path: &PathBuf) {
+ let _ = self
+ .selector_sender
+ .send(DeviceSelectorEvent::DeviceRemoved(path.clone()));
+
+ debug!("Removing device {}", path.to_string_lossy());
+ if let Some(runloop) = self.runloops.remove(path) {
+ runloop.cancel();
+ }
+ }
+
+ fn remove_all_devices(&mut self) {
+ while !self.runloops.is_empty() {
+ let path = self.runloops.keys().next().unwrap().clone();
+ self.remove_device(&path);
+ }
+ }
+}
+
+pub fn get_property_linux(path: &PathBuf, prop_name: &str) -> io::Result<String> {
+ let ctx = libudev::Context::new()?;
+
+ let mut enumerator = libudev::Enumerator::new(&ctx)?;
+ enumerator.match_subsystem(UDEV_SUBSYSTEM)?;
+
+ // Iterate all existing devices, since we don't have a syspath
+ // and libudev-rs doesn't implement opening by devnode.
+ for dev in enumerator.scan_devices()? {
+ if dev.devnode().is_some() && dev.devnode().unwrap() == path {
+ debug!(
+ "get_property_linux Querying property {} from {}",
+ prop_name,
+ dev.syspath().display()
+ );
+
+ let value = dev
+ .attribute_value(prop_name)
+ .ok_or(io::ErrorKind::Other)?
+ .to_string_lossy();
+
+ debug!("get_property_linux Fetched Result, {}={}", prop_name, value);
+ return Ok(value.to_string());
+ }
+ }
+
+ Err(io::Error::new(
+ io::ErrorKind::Other,
+ "Unable to find device",
+ ))
+}
diff --git a/third_party/rust/authenticator/src/transport/linux/transaction.rs b/third_party/rust/authenticator/src/transport/linux/transaction.rs
new file mode 100644
index 0000000000..35baa8f3c4
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/linux/transaction.rs
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::errors;
+use crate::statecallback::StateCallback;
+use crate::transport::device_selector::{
+ DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent,
+};
+use crate::transport::platform::monitor::Monitor;
+use runloop::RunLoop;
+use std::sync::mpsc::Sender;
+
+pub struct Transaction {
+ // Handle to the thread loop.
+ thread: RunLoop,
+ device_selector: DeviceSelector,
+}
+
+impl Transaction {
+ pub fn new<F, T>(
+ timeout: u64,
+ callback: StateCallback<crate::Result<T>>,
+ status: Sender<crate::StatusUpdate>,
+ new_device_cb: F,
+ ) -> crate::Result<Self>
+ where
+ F: Fn(
+ DeviceBuildParameters,
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Sync
+ + Send
+ + 'static,
+ T: 'static,
+ {
+ let status_sender = status.clone();
+ let device_selector = DeviceSelector::run(status);
+ let selector_sender = device_selector.clone_sender();
+ let thread = RunLoop::new_with_timeout(
+ move |alive| {
+ // Create a new device monitor.
+ let mut monitor = Monitor::new(new_device_cb, selector_sender, status_sender);
+
+ // Start polling for new devices.
+ try_or!(monitor.run(alive), |_| callback
+ .call(Err(errors::AuthenticatorError::Platform)));
+
+ // Send an error, if the callback wasn't called already.
+ callback.call(Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::NotAllowed,
+ )));
+ },
+ timeout,
+ )
+ .map_err(|_| errors::AuthenticatorError::Platform)?;
+
+ Ok(Self {
+ thread,
+ device_selector,
+ })
+ }
+
+ pub fn cancel(&mut self) {
+ info!("Transaction was cancelled.");
+ self.device_selector.stop();
+ self.thread.cancel();
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/macos/device.rs b/third_party/rust/authenticator/src/transport/macos/device.rs
new file mode 100644
index 0000000000..757e93810f
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/macos/device.rs
@@ -0,0 +1,227 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate log;
+
+use crate::consts::{CID_BROADCAST, MAX_HID_RPT_SIZE};
+use crate::transport::hid::HIDDevice;
+use crate::transport::platform::iokit::*;
+use crate::transport::{AuthenticatorInfo, ECDHSecret, FidoDevice, HIDError};
+use crate::u2ftypes::{U2FDevice, U2FDeviceInfo};
+use core_foundation::base::*;
+use core_foundation::string::*;
+use std::convert::TryInto;
+use std::fmt;
+use std::hash::{Hash, Hasher};
+use std::io;
+use std::io::{Read, Write};
+use std::sync::mpsc::{Receiver, RecvTimeoutError};
+use std::time::Duration;
+
+const READ_TIMEOUT: u64 = 15;
+
+pub struct Device {
+ device_ref: IOHIDDeviceRef,
+ cid: [u8; 4],
+ report_rx: Option<Receiver<Vec<u8>>>,
+ dev_info: Option<U2FDeviceInfo>,
+ secret: Option<ECDHSecret>,
+ authenticator_info: Option<AuthenticatorInfo>,
+}
+
+impl Device {
+ unsafe fn get_property_macos(&self, prop_name: &str) -> io::Result<String> {
+ let prop_ref = IOHIDDeviceGetProperty(
+ self.device_ref,
+ CFString::new(prop_name).as_concrete_TypeRef(),
+ );
+ if prop_ref.is_null() {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidData,
+ format!(
+ "IOHIDDeviceGetProperty received nullptr for property {}",
+ prop_name
+ ),
+ ));
+ }
+
+ if CFGetTypeID(prop_ref) != CFStringGetTypeID() {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ format!(
+ "IOHIDDeviceGetProperty returned non-string type for property {}",
+ prop_name
+ ),
+ ));
+ }
+
+ Ok(CFString::from_void(prop_ref).to_string())
+ }
+}
+
+impl fmt::Debug for Device {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("Device").field("cid", &self.cid).finish()
+ }
+}
+
+impl PartialEq for Device {
+ fn eq(&self, other_device: &Device) -> bool {
+ self.device_ref == other_device.device_ref
+ }
+}
+
+impl Eq for Device {}
+
+impl Hash for Device {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ // The path should be the only identifying member for a device
+ // If the path is the same, its the same device
+ self.device_ref.hash(state);
+ }
+}
+
+impl Read for Device {
+ fn read(&mut self, mut bytes: &mut [u8]) -> io::Result<usize> {
+ if let Some(rx) = &self.report_rx {
+ let timeout = Duration::from_secs(READ_TIMEOUT);
+ let data = match rx.recv_timeout(timeout) {
+ Ok(v) => v,
+ Err(e) if e == RecvTimeoutError::Timeout => {
+ return Err(io::Error::new(io::ErrorKind::TimedOut, e));
+ }
+ Err(e) => {
+ return Err(io::Error::new(io::ErrorKind::UnexpectedEof, e));
+ }
+ };
+ bytes.write(&data)
+ } else {
+ Err(io::Error::from(io::ErrorKind::Unsupported))
+ }
+ }
+}
+
+impl Write for Device {
+ fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
+ assert_eq!(bytes.len(), self.out_rpt_size() + 1);
+
+ let report_id = i64::from(bytes[0]);
+ // Skip report number when not using numbered reports.
+ let start = if report_id == 0x0 { 1 } else { 0 };
+ let data = &bytes[start..];
+
+ let result = unsafe {
+ IOHIDDeviceSetReport(
+ self.device_ref,
+ kIOHIDReportTypeOutput,
+ report_id.try_into().unwrap(),
+ data.as_ptr(),
+ data.len() as CFIndex,
+ )
+ };
+ if result != 0 {
+ warn!("set_report sending failure = {0:X}", result);
+ return Err(io::Error::from_raw_os_error(result));
+ }
+ trace!("set_report sending success = {0:X}", result);
+
+ Ok(bytes.len())
+ }
+
+ // USB HID writes don't buffer, so this will be a nop.
+ fn flush(&mut self) -> io::Result<()> {
+ Ok(())
+ }
+}
+
+impl U2FDevice for Device {
+ fn get_cid(&self) -> &[u8; 4] {
+ &self.cid
+ }
+
+ fn set_cid(&mut self, cid: [u8; 4]) {
+ self.cid = cid;
+ }
+
+ fn in_rpt_size(&self) -> usize {
+ MAX_HID_RPT_SIZE
+ }
+
+ fn out_rpt_size(&self) -> usize {
+ MAX_HID_RPT_SIZE
+ }
+
+ fn get_property(&self, prop_name: &str) -> io::Result<String> {
+ unsafe { self.get_property_macos(prop_name) }
+ }
+
+ fn get_device_info(&self) -> U2FDeviceInfo {
+ // unwrap is okay, as dev_info must have already been set, else
+ // a programmer error
+ self.dev_info.clone().unwrap()
+ }
+
+ fn set_device_info(&mut self, dev_info: U2FDeviceInfo) {
+ self.dev_info = Some(dev_info);
+ }
+}
+
+impl HIDDevice for Device {
+ type BuildParameters = (IOHIDDeviceRef, Receiver<Vec<u8>>);
+ type Id = IOHIDDeviceRef;
+
+ fn new(dev_ids: Self::BuildParameters) -> Result<Self, (HIDError, Self::Id)> {
+ let (device_ref, report_rx) = dev_ids;
+ Ok(Self {
+ device_ref,
+ cid: CID_BROADCAST,
+ report_rx: Some(report_rx),
+ dev_info: None,
+ secret: None,
+ authenticator_info: None,
+ })
+ }
+
+ fn initialized(&self) -> bool {
+ self.cid != CID_BROADCAST
+ }
+
+ fn id(&self) -> Self::Id {
+ self.device_ref
+ }
+
+ fn is_u2f(&mut self) -> bool {
+ true
+ }
+ fn get_shared_secret(&self) -> Option<&ECDHSecret> {
+ self.secret.as_ref()
+ }
+
+ fn set_shared_secret(&mut self, secret: ECDHSecret) {
+ self.secret = Some(secret);
+ }
+
+ fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> {
+ self.authenticator_info.as_ref()
+ }
+
+ fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) {
+ self.authenticator_info = Some(authenticator_info);
+ }
+
+ /// This is used for cancellation of blocking read()-requests.
+ /// With this, we can clone the Device, pass it to another thread and call "cancel()" on that.
+ fn clone_device_as_write_only(&self) -> Result<Self, HIDError> {
+ Ok(Self {
+ device_ref: self.device_ref,
+ cid: self.cid,
+ report_rx: None,
+ dev_info: self.dev_info.clone(),
+ secret: self.secret.clone(),
+ authenticator_info: self.authenticator_info.clone(),
+ })
+ }
+}
+
+impl FidoDevice for Device {}
diff --git a/third_party/rust/authenticator/src/transport/macos/iokit.rs b/third_party/rust/authenticator/src/transport/macos/iokit.rs
new file mode 100644
index 0000000000..656cdb045d
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/macos/iokit.rs
@@ -0,0 +1,292 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals)]
+
+extern crate libc;
+
+use crate::consts::{FIDO_USAGE_PAGE, FIDO_USAGE_U2FHID};
+use core_foundation::array::*;
+use core_foundation::base::*;
+use core_foundation::dictionary::*;
+use core_foundation::number::*;
+use core_foundation::runloop::*;
+use core_foundation::string::*;
+use std::ops::Deref;
+use std::os::raw::c_void;
+
+type IOOptionBits = u32;
+
+pub type IOReturn = libc::c_int;
+
+pub type IOHIDManagerRef = *mut __IOHIDManager;
+pub type IOHIDManagerOptions = IOOptionBits;
+
+pub type IOHIDDeviceCallback = extern "C" fn(
+ context: *mut c_void,
+ result: IOReturn,
+ sender: *mut c_void,
+ device: IOHIDDeviceRef,
+);
+
+pub type IOHIDReportType = IOOptionBits;
+pub type IOHIDReportCallback = extern "C" fn(
+ context: *mut c_void,
+ result: IOReturn,
+ sender: IOHIDDeviceRef,
+ report_type: IOHIDReportType,
+ report_id: u32,
+ report: *mut u8,
+ report_len: CFIndex,
+);
+
+pub const kIOHIDManagerOptionNone: IOHIDManagerOptions = 0;
+
+pub const kIOHIDReportTypeOutput: IOHIDReportType = 1;
+
+#[repr(C)]
+pub struct __IOHIDManager {
+ __private: c_void,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
+pub struct IOHIDDeviceRef(*const c_void);
+
+unsafe impl Send for IOHIDDeviceRef {}
+unsafe impl Sync for IOHIDDeviceRef {}
+
+pub struct SendableRunLoop(CFRunLoopRef);
+
+impl SendableRunLoop {
+ pub fn new(runloop: CFRunLoopRef) -> Self {
+ // Keep the CFRunLoop alive for as long as we are.
+ unsafe { CFRetain(runloop as *mut c_void) };
+
+ SendableRunLoop(runloop)
+ }
+}
+
+unsafe impl Send for SendableRunLoop {}
+
+impl Deref for SendableRunLoop {
+ type Target = CFRunLoopRef;
+
+ fn deref(&self) -> &CFRunLoopRef {
+ &self.0
+ }
+}
+
+impl Drop for SendableRunLoop {
+ fn drop(&mut self) {
+ unsafe { CFRelease(self.0 as *mut c_void) };
+ }
+}
+
+#[repr(C)]
+pub struct CFRunLoopObserverContext {
+ pub version: CFIndex,
+ pub info: *mut c_void,
+ pub retain: Option<extern "C" fn(info: *const c_void) -> *const c_void>,
+ pub release: Option<extern "C" fn(info: *const c_void)>,
+ pub copyDescription: Option<extern "C" fn(info: *const c_void) -> CFStringRef>,
+}
+
+impl CFRunLoopObserverContext {
+ pub fn new(context: *mut c_void) -> Self {
+ Self {
+ version: 0 as CFIndex,
+ info: context,
+ retain: None,
+ release: None,
+ copyDescription: None,
+ }
+ }
+}
+
+pub struct CFRunLoopEntryObserver {
+ observer: CFRunLoopObserverRef,
+ // Keep alive until the observer goes away.
+ context_ptr: *mut CFRunLoopObserverContext,
+}
+
+impl CFRunLoopEntryObserver {
+ pub fn new(callback: CFRunLoopObserverCallBack, context: *mut c_void) -> Self {
+ let context = CFRunLoopObserverContext::new(context);
+ let context_ptr = Box::into_raw(Box::new(context));
+
+ let observer = unsafe {
+ CFRunLoopObserverCreate(
+ kCFAllocatorDefault,
+ kCFRunLoopEntry,
+ false as Boolean,
+ 0,
+ callback,
+ context_ptr,
+ )
+ };
+
+ Self {
+ observer,
+ context_ptr,
+ }
+ }
+
+ pub fn add_to_current_runloop(&self) {
+ unsafe {
+ CFRunLoopAddObserver(CFRunLoopGetCurrent(), self.observer, kCFRunLoopDefaultMode)
+ };
+ }
+}
+
+impl Drop for CFRunLoopEntryObserver {
+ fn drop(&mut self) {
+ unsafe {
+ CFRelease(self.observer as *mut c_void);
+
+ // Drop the CFRunLoopObserverContext.
+ let _ = Box::from_raw(self.context_ptr);
+ };
+ }
+}
+
+pub struct IOHIDDeviceMatcher {
+ pub dict: CFDictionary<CFString, CFNumber>,
+}
+
+impl IOHIDDeviceMatcher {
+ pub fn new() -> Self {
+ let dict = CFDictionary::<CFString, CFNumber>::from_CFType_pairs(&[
+ (
+ CFString::from_static_string("DeviceUsage"),
+ CFNumber::from(i32::from(FIDO_USAGE_U2FHID)),
+ ),
+ (
+ CFString::from_static_string("DeviceUsagePage"),
+ CFNumber::from(i32::from(FIDO_USAGE_PAGE)),
+ ),
+ ]);
+ Self { dict }
+ }
+}
+
+#[link(name = "IOKit", kind = "framework")]
+extern "C" {
+ // CFRunLoop
+ pub fn CFRunLoopObserverCreate(
+ allocator: CFAllocatorRef,
+ activities: CFOptionFlags,
+ repeats: Boolean,
+ order: CFIndex,
+ callout: CFRunLoopObserverCallBack,
+ context: *mut CFRunLoopObserverContext,
+ ) -> CFRunLoopObserverRef;
+
+ // IOHIDManager
+ pub fn IOHIDManagerCreate(
+ allocator: CFAllocatorRef,
+ options: IOHIDManagerOptions,
+ ) -> IOHIDManagerRef;
+ pub fn IOHIDManagerSetDeviceMatching(manager: IOHIDManagerRef, matching: CFDictionaryRef);
+ pub fn IOHIDManagerRegisterDeviceMatchingCallback(
+ manager: IOHIDManagerRef,
+ callback: IOHIDDeviceCallback,
+ context: *mut c_void,
+ );
+ pub fn IOHIDManagerRegisterDeviceRemovalCallback(
+ manager: IOHIDManagerRef,
+ callback: IOHIDDeviceCallback,
+ context: *mut c_void,
+ );
+ pub fn IOHIDManagerRegisterInputReportCallback(
+ manager: IOHIDManagerRef,
+ callback: IOHIDReportCallback,
+ context: *mut c_void,
+ );
+ pub fn IOHIDManagerOpen(manager: IOHIDManagerRef, options: IOHIDManagerOptions) -> IOReturn;
+ pub fn IOHIDManagerClose(manager: IOHIDManagerRef, options: IOHIDManagerOptions) -> IOReturn;
+ pub fn IOHIDManagerScheduleWithRunLoop(
+ manager: IOHIDManagerRef,
+ runLoop: CFRunLoopRef,
+ runLoopMode: CFStringRef,
+ );
+
+ // IOHIDDevice
+ pub fn IOHIDDeviceSetReport(
+ device: IOHIDDeviceRef,
+ reportType: IOHIDReportType,
+ reportID: CFIndex,
+ report: *const u8,
+ reportLength: CFIndex,
+ ) -> IOReturn;
+ pub fn IOHIDDeviceGetProperty(device: IOHIDDeviceRef, key: CFStringRef) -> CFTypeRef;
+}
+
+////////////////////////////////////////////////////////////////////////
+// Tests
+////////////////////////////////////////////////////////////////////////
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::os::raw::c_void;
+ use std::ptr;
+ use std::sync::mpsc::{channel, Sender};
+ use std::thread;
+
+ extern "C" fn observe(_: CFRunLoopObserverRef, _: CFRunLoopActivity, context: *mut c_void) {
+ let tx: &Sender<SendableRunLoop> = unsafe { &*(context as *mut _) };
+
+ // Send the current runloop to the receiver to unblock it.
+ let _ = tx.send(SendableRunLoop::new(unsafe { CFRunLoopGetCurrent() }));
+ }
+
+ #[test]
+ fn test_sendable_runloop() {
+ let (tx, rx) = channel();
+
+ let thread = thread::spawn(move || {
+ // Send the runloop to the owning thread.
+ let context = &tx as *const _ as *mut c_void;
+ let obs = CFRunLoopEntryObserver::new(observe, context);
+ obs.add_to_current_runloop();
+
+ unsafe {
+ // We need some source for the runloop to run.
+ let manager = IOHIDManagerCreate(kCFAllocatorDefault, 0);
+ assert!(!manager.is_null());
+
+ IOHIDManagerScheduleWithRunLoop(
+ manager,
+ CFRunLoopGetCurrent(),
+ kCFRunLoopDefaultMode,
+ );
+ IOHIDManagerSetDeviceMatching(manager, ptr::null_mut());
+
+ let rv = IOHIDManagerOpen(manager, 0);
+ assert_eq!(rv, 0);
+
+ // This will run until `CFRunLoopStop()` is called.
+ CFRunLoopRun();
+
+ let rv = IOHIDManagerClose(manager, 0);
+ assert_eq!(rv, 0);
+
+ CFRelease(manager as *mut c_void);
+ }
+ });
+
+ // Block until we enter the CFRunLoop.
+ let runloop: SendableRunLoop = rx.recv().expect("failed to receive runloop");
+
+ // Stop the runloop.
+ unsafe { CFRunLoopStop(*runloop) };
+
+ // Stop the thread.
+ thread.join().expect("failed to join the thread");
+
+ // Try to stop the runloop again (without crashing).
+ unsafe { CFRunLoopStop(*runloop) };
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/macos/mod.rs b/third_party/rust/authenticator/src/transport/macos/mod.rs
new file mode 100644
index 0000000000..44e85094d0
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/macos/mod.rs
@@ -0,0 +1,9 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+pub mod device;
+pub mod transaction;
+
+mod iokit;
+mod monitor;
diff --git a/third_party/rust/authenticator/src/transport/macos/monitor.rs b/third_party/rust/authenticator/src/transport/macos/monitor.rs
new file mode 100644
index 0000000000..eafa206505
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/macos/monitor.rs
@@ -0,0 +1,212 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate libc;
+extern crate log;
+
+use crate::transport::device_selector::DeviceSelectorEvent;
+use crate::transport::platform::iokit::*;
+use crate::util::io_err;
+use core_foundation::base::*;
+use core_foundation::runloop::*;
+use runloop::RunLoop;
+use std::collections::HashMap;
+use std::os::raw::c_void;
+use std::sync::mpsc::{channel, Receiver, Sender};
+use std::{io, slice};
+
+struct DeviceData {
+ tx: Sender<Vec<u8>>,
+ runloop: RunLoop,
+}
+
+pub struct Monitor<F>
+where
+ F: Fn(
+ (IOHIDDeviceRef, Receiver<Vec<u8>>),
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Send
+ + Sync
+ + 'static,
+{
+ manager: IOHIDManagerRef,
+ // Keep alive until the monitor goes away.
+ _matcher: IOHIDDeviceMatcher,
+ map: HashMap<IOHIDDeviceRef, DeviceData>,
+ new_device_cb: F,
+ selector_sender: Sender<DeviceSelectorEvent>,
+ status_sender: Sender<crate::StatusUpdate>,
+}
+
+impl<F> Monitor<F>
+where
+ F: Fn(
+ (IOHIDDeviceRef, Receiver<Vec<u8>>),
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Send
+ + Sync
+ + 'static,
+{
+ pub fn new(
+ new_device_cb: F,
+ selector_sender: Sender<DeviceSelectorEvent>,
+ status_sender: Sender<crate::StatusUpdate>,
+ ) -> Self {
+ let manager = unsafe { IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDManagerOptionNone) };
+
+ // Match FIDO devices only.
+ let _matcher = IOHIDDeviceMatcher::new();
+ unsafe { IOHIDManagerSetDeviceMatching(manager, _matcher.dict.as_concrete_TypeRef()) };
+
+ Self {
+ manager,
+ _matcher,
+ new_device_cb,
+ map: HashMap::new(),
+ selector_sender,
+ status_sender,
+ }
+ }
+
+ pub fn start(&mut self) -> io::Result<()> {
+ let context = self as *mut Self as *mut c_void;
+
+ unsafe {
+ IOHIDManagerRegisterDeviceMatchingCallback(
+ self.manager,
+ Monitor::<F>::on_device_matching,
+ context,
+ );
+ IOHIDManagerRegisterDeviceRemovalCallback(
+ self.manager,
+ Monitor::<F>::on_device_removal,
+ context,
+ );
+ IOHIDManagerRegisterInputReportCallback(
+ self.manager,
+ Monitor::<F>::on_input_report,
+ context,
+ );
+
+ IOHIDManagerScheduleWithRunLoop(
+ self.manager,
+ CFRunLoopGetCurrent(),
+ kCFRunLoopDefaultMode,
+ );
+
+ let rv = IOHIDManagerOpen(self.manager, kIOHIDManagerOptionNone);
+ if rv == 0 {
+ Ok(())
+ } else {
+ Err(io_err(&format!("Couldn't open HID Manager, rv={}", rv)))
+ }
+ }
+ }
+
+ pub fn stop(&mut self) {
+ // Remove all devices.
+ while !self.map.is_empty() {
+ let device_ref = *self.map.keys().next().unwrap();
+ self.remove_device(device_ref);
+ }
+
+ // Close the manager and its devices.
+ unsafe { IOHIDManagerClose(self.manager, kIOHIDManagerOptionNone) };
+ }
+
+ fn remove_device(&mut self, device_ref: IOHIDDeviceRef) {
+ if let Some(DeviceData { tx, runloop }) = self.map.remove(&device_ref) {
+ let _ = self
+ .selector_sender
+ .send(DeviceSelectorEvent::DeviceRemoved(device_ref));
+ // Dropping `tx` will make Device::read() fail eventually.
+ drop(tx);
+
+ // Wait until the runloop stopped.
+ runloop.cancel();
+ }
+ }
+
+ extern "C" fn on_input_report(
+ context: *mut c_void,
+ _: IOReturn,
+ device_ref: IOHIDDeviceRef,
+ _: IOHIDReportType,
+ _: u32,
+ report: *mut u8,
+ report_len: CFIndex,
+ ) {
+ let this = unsafe { &mut *(context as *mut Self) };
+ let mut send_failed = false;
+
+ // Ignore the report if we can't find a device for it.
+ if let Some(&DeviceData { ref tx, .. }) = this.map.get(&device_ref) {
+ let data = unsafe { slice::from_raw_parts(report, report_len as usize).to_vec() };
+ send_failed = tx.send(data).is_err();
+ }
+
+ // Remove the device if sending fails.
+ if send_failed {
+ this.remove_device(device_ref);
+ }
+ }
+
+ extern "C" fn on_device_matching(
+ context: *mut c_void,
+ _: IOReturn,
+ _: *mut c_void,
+ device_ref: IOHIDDeviceRef,
+ ) {
+ let this = unsafe { &mut *(context as *mut Self) };
+ let _ = this
+ .selector_sender
+ .send(DeviceSelectorEvent::DevicesAdded(vec![device_ref]));
+ let selector_sender = this.selector_sender.clone();
+ let status_sender = this.status_sender.clone();
+ let (tx, rx) = channel();
+ let f = &this.new_device_cb;
+
+ // Create a new per-device runloop.
+ let runloop = RunLoop::new(move |alive| {
+ // Ensure that the runloop is still alive.
+ if alive() {
+ f((device_ref, rx), selector_sender, status_sender, alive);
+ }
+ });
+
+ if let Ok(runloop) = runloop {
+ this.map.insert(device_ref, DeviceData { tx, runloop });
+ }
+ }
+
+ extern "C" fn on_device_removal(
+ context: *mut c_void,
+ _: IOReturn,
+ _: *mut c_void,
+ device_ref: IOHIDDeviceRef,
+ ) {
+ let this = unsafe { &mut *(context as *mut Self) };
+ this.remove_device(device_ref);
+ }
+}
+
+impl<F> Drop for Monitor<F>
+where
+ F: Fn(
+ (IOHIDDeviceRef, Receiver<Vec<u8>>),
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Send
+ + Sync
+ + 'static,
+{
+ fn drop(&mut self) {
+ unsafe { CFRelease(self.manager as *mut c_void) };
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/macos/transaction.rs b/third_party/rust/authenticator/src/transport/macos/transaction.rs
new file mode 100644
index 0000000000..7387a0ac8d
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/macos/transaction.rs
@@ -0,0 +1,108 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate libc;
+
+use crate::errors;
+use crate::statecallback::StateCallback;
+use crate::transport::device_selector::{
+ DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent,
+};
+use crate::transport::platform::iokit::{CFRunLoopEntryObserver, SendableRunLoop};
+use crate::transport::platform::monitor::Monitor;
+use core_foundation::runloop::*;
+use std::os::raw::c_void;
+use std::sync::mpsc::{channel, Sender};
+use std::thread;
+
+// A transaction will run the given closure in a new thread, thereby using a
+// separate per-thread state machine for each HID. It will either complete or
+// fail through user action, timeout, or be cancelled when overridden by a new
+// transaction.
+pub struct Transaction {
+ runloop: Option<SendableRunLoop>,
+ thread: Option<thread::JoinHandle<()>>,
+ device_selector: DeviceSelector,
+}
+
+impl Transaction {
+ pub fn new<F, T>(
+ timeout: u64,
+ callback: StateCallback<crate::Result<T>>,
+ status: Sender<crate::StatusUpdate>,
+ new_device_cb: F,
+ ) -> crate::Result<Self>
+ where
+ F: Fn(
+ DeviceBuildParameters,
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Sync
+ + Send
+ + 'static,
+ T: 'static,
+ {
+ let (tx, rx) = channel();
+ let timeout = (timeout as f64) / 1000.0;
+ let status_sender = status.clone();
+ let device_selector = DeviceSelector::run(status);
+ let selector_sender = device_selector.clone_sender();
+ let builder = thread::Builder::new();
+ let thread = builder
+ .spawn(move || {
+ // Add a runloop observer that will be notified when we enter the
+ // runloop and tx.send() the current runloop to the owning thread.
+ // We need to ensure the runloop was entered before unblocking
+ // Transaction::new(), so we can always properly cancel.
+ let context = &tx as *const _ as *mut c_void;
+ let obs = CFRunLoopEntryObserver::new(Transaction::observe, context);
+ obs.add_to_current_runloop();
+
+ // Create a new HID device monitor and start polling.
+ let mut monitor = Monitor::new(new_device_cb, selector_sender, status_sender);
+ try_or!(monitor.start(), |_| callback
+ .call(Err(errors::AuthenticatorError::Platform)));
+
+ // This will block until completion, abortion, or timeout.
+ unsafe { CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, 0) };
+
+ // Close the monitor and its devices.
+ monitor.stop();
+
+ // Send an error, if the callback wasn't called already.
+ callback.call(Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::NotAllowed,
+ )));
+ })
+ .map_err(|_| errors::AuthenticatorError::Platform)?;
+
+ // Block until we enter the CFRunLoop.
+ let runloop = rx
+ .recv()
+ .map_err(|_| errors::AuthenticatorError::Platform)?;
+
+ Ok(Self {
+ runloop: Some(runloop),
+ thread: Some(thread),
+ device_selector,
+ })
+ }
+
+ extern "C" fn observe(_: CFRunLoopObserverRef, _: CFRunLoopActivity, context: *mut c_void) {
+ let tx: &Sender<SendableRunLoop> = unsafe { &*(context as *mut _) };
+
+ // Send the current runloop to the receiver to unblock it.
+ let _ = tx.send(SendableRunLoop::new(unsafe { CFRunLoopGetCurrent() }));
+ }
+
+ pub fn cancel(&mut self) {
+ // This must never be None. This won't block.
+ unsafe { CFRunLoopStop(*self.runloop.take().unwrap()) };
+
+ self.device_selector.stop();
+ // This must never be None. Ignore return value.
+ let _ = self.thread.take().unwrap().join();
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/mock/device.rs b/third_party/rust/authenticator/src/transport/mock/device.rs
new file mode 100644
index 0000000000..e76a27db28
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/mock/device.rs
@@ -0,0 +1,194 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+use crate::consts::CID_BROADCAST;
+use crate::crypto::ECDHSecret;
+use crate::ctap2::commands::get_info::AuthenticatorInfo;
+use crate::transport::device_selector::DeviceCommand;
+use crate::transport::{hid::HIDDevice, FidoDevice, HIDError};
+use crate::u2ftypes::{U2FDevice, U2FDeviceInfo};
+use std::hash::{Hash, Hasher};
+use std::io::{self, Read, Write};
+use std::sync::mpsc::{channel, Receiver, Sender};
+
+pub(crate) const IN_HID_RPT_SIZE: usize = 64;
+const OUT_HID_RPT_SIZE: usize = 64;
+
+#[derive(Debug)]
+pub struct Device {
+ pub id: String,
+ pub cid: [u8; 4],
+ pub reads: Vec<[u8; IN_HID_RPT_SIZE]>,
+ pub writes: Vec<[u8; OUT_HID_RPT_SIZE + 1]>,
+ pub dev_info: Option<U2FDeviceInfo>,
+ pub authenticator_info: Option<AuthenticatorInfo>,
+ pub sender: Option<Sender<DeviceCommand>>,
+ pub receiver: Option<Receiver<DeviceCommand>>,
+}
+
+impl Device {
+ pub fn add_write(&mut self, packet: &[u8], fill_value: u8) {
+ // Add one to deal with record index check
+ let mut write = [fill_value; OUT_HID_RPT_SIZE + 1];
+ // Make sure we start with a 0, for HID record index
+ write[0] = 0;
+ // Clone packet data in at 1, since front is padded with HID record index
+ write[1..=packet.len()].clone_from_slice(packet);
+ self.writes.push(write);
+ }
+
+ pub fn add_read(&mut self, packet: &[u8], fill_value: u8) {
+ let mut read = [fill_value; IN_HID_RPT_SIZE];
+ read[..packet.len()].clone_from_slice(packet);
+ self.reads.push(read);
+ }
+
+ pub fn create_channel(&mut self) {
+ let (tx, rx) = channel();
+ self.sender = Some(tx);
+ self.receiver = Some(rx);
+ }
+}
+
+impl Write for Device {
+ fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
+ // Pop a vector from the expected writes, check for quality
+ // against bytes array.
+ assert!(
+ !self.writes.is_empty(),
+ "Ran out of expected write values! Wanted to write {:?}",
+ bytes
+ );
+ let check = self.writes.remove(0);
+ assert_eq!(check.len(), bytes.len());
+ assert_eq!(&check, bytes);
+ Ok(bytes.len())
+ }
+
+ // nop
+ fn flush(&mut self) -> io::Result<()> {
+ Ok(())
+ }
+}
+
+impl Read for Device {
+ fn read(&mut self, bytes: &mut [u8]) -> io::Result<usize> {
+ assert!(!self.reads.is_empty(), "Ran out of read values!");
+ let check = self.reads.remove(0);
+ assert_eq!(check.len(), bytes.len());
+ bytes.clone_from_slice(&check);
+ Ok(check.len())
+ }
+}
+
+impl Drop for Device {
+ fn drop(&mut self) {
+ if !std::thread::panicking() {
+ assert!(self.reads.is_empty());
+ assert!(self.writes.is_empty());
+ }
+ }
+}
+
+impl PartialEq for Device {
+ fn eq(&self, other: &Device) -> bool {
+ self.id == other.id
+ }
+}
+
+impl Eq for Device {}
+
+impl Hash for Device {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.id.hash(state);
+ }
+}
+
+impl U2FDevice for Device {
+ fn get_cid<'a>(&'a self) -> &'a [u8; 4] {
+ &self.cid
+ }
+
+ fn set_cid(&mut self, cid: [u8; 4]) {
+ self.cid = cid;
+ }
+
+ fn in_rpt_size(&self) -> usize {
+ IN_HID_RPT_SIZE
+ }
+
+ fn out_rpt_size(&self) -> usize {
+ OUT_HID_RPT_SIZE
+ }
+
+ fn get_property(&self, prop_name: &str) -> io::Result<String> {
+ Ok(format!("{} not implemented", prop_name))
+ }
+ fn get_device_info(&self) -> U2FDeviceInfo {
+ self.dev_info.clone().unwrap()
+ }
+
+ fn set_device_info(&mut self, dev_info: U2FDeviceInfo) {
+ self.dev_info = Some(dev_info);
+ }
+}
+
+impl HIDDevice for Device {
+ type Id = String;
+ type BuildParameters = &'static str; // None used
+
+ fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> {
+ self.authenticator_info.as_ref()
+ }
+
+ fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) {
+ self.authenticator_info = Some(authenticator_info);
+ }
+
+ fn set_shared_secret(&mut self, _: ECDHSecret) {
+ // Nothing
+ }
+ fn get_shared_secret(&self) -> std::option::Option<&ECDHSecret> {
+ None
+ }
+
+ fn new(id: Self::BuildParameters) -> Result<Self, (HIDError, Self::Id)> {
+ Ok(Device {
+ id: id.to_string(),
+ cid: CID_BROADCAST,
+ reads: vec![],
+ writes: vec![],
+ dev_info: None,
+ authenticator_info: None,
+ sender: None,
+ receiver: None,
+ })
+ }
+
+ fn initialized(&self) -> bool {
+ self.get_cid() != &CID_BROADCAST
+ }
+
+ fn id(&self) -> Self::Id {
+ self.id.clone()
+ }
+
+ fn clone_device_as_write_only(&self) -> Result<Self, HIDError> {
+ Ok(Device {
+ id: self.id.clone(),
+ cid: self.cid,
+ reads: self.reads.clone(),
+ writes: self.writes.clone(),
+ dev_info: self.dev_info.clone(),
+ authenticator_info: self.authenticator_info.clone(),
+ sender: self.sender.clone(),
+ receiver: None,
+ })
+ }
+
+ fn is_u2f(&mut self) -> bool {
+ self.sender.is_some()
+ }
+}
+
+impl FidoDevice for Device {}
diff --git a/third_party/rust/authenticator/src/transport/mock/mod.rs b/third_party/rust/authenticator/src/transport/mock/mod.rs
new file mode 100644
index 0000000000..d0e200a7ef
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/mock/mod.rs
@@ -0,0 +1,6 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+pub mod device;
+pub mod transaction;
diff --git a/third_party/rust/authenticator/src/transport/mock/transaction.rs b/third_party/rust/authenticator/src/transport/mock/transaction.rs
new file mode 100644
index 0000000000..e19b1cb56f
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/mock/transaction.rs
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::statecallback::StateCallback;
+use crate::transport::device_selector::{DeviceBuildParameters, DeviceSelectorEvent};
+use std::sync::mpsc::Sender;
+
+pub struct Transaction {}
+
+impl Transaction {
+ pub fn new<F, T>(
+ _timeout: u64,
+ _callback: StateCallback<crate::Result<T>>,
+ _status: Sender<crate::StatusUpdate>,
+ _new_device_cb: F,
+ ) -> crate::Result<Self>
+ where
+ F: Fn(
+ DeviceBuildParameters,
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Sync
+ + Send
+ + 'static,
+ T: 'static,
+ {
+ Ok(Self {})
+ }
+
+ pub fn cancel(&mut self) {
+ info!("Transaction was cancelled.");
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/mod.rs b/third_party/rust/authenticator/src/transport/mod.rs
new file mode 100644
index 0000000000..c68615eedc
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/mod.rs
@@ -0,0 +1,263 @@
+use crate::consts::{Capability, HIDCmd};
+use crate::crypto::ECDHSecret;
+
+use crate::ctap2::commands::client_pin::{GetKeyAgreement, PinAuth};
+use crate::ctap2::commands::get_info::{AuthenticatorInfo, GetInfo};
+use crate::ctap2::commands::get_version::GetVersion;
+use crate::ctap2::commands::make_credentials::dummy_make_credentials_cmd;
+use crate::ctap2::commands::selection::Selection;
+use crate::ctap2::commands::{
+ CommandError, PinAuthCommand, Request, RequestCtap1, RequestCtap2, Retryable, StatusCode,
+};
+use crate::transport::device_selector::BlinkResult;
+use crate::transport::errors::{ApduErrorStatus, HIDError};
+use crate::transport::hid::HIDDevice;
+use crate::util::io_err;
+use std::thread;
+use std::time::Duration;
+
+pub mod device_selector;
+pub mod errors;
+pub mod hid;
+
+#[cfg(all(
+ any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"),
+ not(test)
+))]
+pub mod hidproto;
+
+#[cfg(all(target_os = "linux", not(test)))]
+#[path = "linux/mod.rs"]
+pub mod platform;
+
+#[cfg(all(target_os = "freebsd", not(test)))]
+#[path = "freebsd/mod.rs"]
+pub mod platform;
+
+#[cfg(all(target_os = "netbsd", not(test)))]
+#[path = "netbsd/mod.rs"]
+pub mod platform;
+
+#[cfg(all(target_os = "openbsd", not(test)))]
+#[path = "openbsd/mod.rs"]
+pub mod platform;
+
+#[cfg(all(target_os = "macos", not(test)))]
+#[path = "macos/mod.rs"]
+pub mod platform;
+
+#[cfg(all(target_os = "windows", not(test)))]
+#[path = "windows/mod.rs"]
+pub mod platform;
+
+#[cfg(not(any(
+ target_os = "linux",
+ target_os = "freebsd",
+ target_os = "openbsd",
+ target_os = "netbsd",
+ target_os = "macos",
+ target_os = "windows",
+ test
+)))]
+#[path = "stub/mod.rs"]
+pub mod platform;
+
+#[cfg(test)]
+#[path = "mock/mod.rs"]
+pub mod platform;
+
+#[derive(Debug)]
+pub enum Nonce {
+ CreateRandom,
+ Use([u8; 8]),
+}
+
+// TODO(MS): This is the lazy way: FidoDevice currently only extends HIDDevice by more functions,
+// but the goal is to remove U2FDevice entirely and copy over the trait-definition here
+pub trait FidoDevice: HIDDevice {
+ fn send_msg<'msg, Out, Req: Request<Out>>(&mut self, msg: &'msg Req) -> Result<Out, HIDError> {
+ if !self.initialized() {
+ return Err(HIDError::DeviceNotInitialized);
+ }
+
+ if self.supports_ctap2() && msg.is_ctap2_request() {
+ self.send_cbor(msg)
+ } else {
+ self.send_ctap1(msg)
+ }
+ }
+
+ fn send_cbor<'msg, Req: RequestCtap2>(
+ &mut self,
+ msg: &'msg Req,
+ ) -> Result<Req::Output, HIDError> {
+ debug!("sending {:?} to {:?}", msg, self);
+
+ let mut data = msg.wire_format(self)?;
+ let mut buf: Vec<u8> = Vec::with_capacity(data.len() + 1);
+ // CTAP2 command
+ buf.push(Req::command() as u8);
+ // payload
+ buf.append(&mut data);
+ let buf = buf;
+
+ let (cmd, resp) = self.sendrecv(HIDCmd::Cbor, &buf)?;
+ debug!(
+ "got from Device {:?} status={:?}: {:?}",
+ self.id(),
+ cmd,
+ resp
+ );
+ if cmd == HIDCmd::Cbor {
+ Ok(msg.handle_response_ctap2(self, &resp)?)
+ } else {
+ Err(HIDError::UnexpectedCmd(cmd.into()))
+ }
+ }
+
+ fn send_ctap1<'msg, Req: RequestCtap1>(
+ &mut self,
+ msg: &'msg Req,
+ ) -> Result<Req::Output, HIDError> {
+ debug!("sending {:?} to {:?}", msg, self);
+ let data = msg.ctap1_format(self)?;
+
+ loop {
+ let (cmd, mut data) = self.sendrecv(HIDCmd::Msg, &data)?;
+ debug!(
+ "got from Device {:?} status={:?}: {:?}",
+ self.id(),
+ cmd,
+ data
+ );
+ if cmd == HIDCmd::Msg {
+ if data.len() < 2 {
+ return Err(io_err("Unexpected Response: shorter than expected").into());
+ }
+ let split_at = data.len() - 2;
+ let status = data.split_off(split_at);
+ // This will bubble up error if status != no error
+ let status = ApduErrorStatus::from([status[0], status[1]]);
+
+ match msg.handle_response_ctap1(status, &data) {
+ Ok(out) => return Ok(out),
+ Err(Retryable::Retry) => {
+ // sleep 100ms then loop again
+ // TODO(baloo): meh, use tokio instead?
+ thread::sleep(Duration::from_millis(100));
+ }
+ Err(Retryable::Error(e)) => return Err(e),
+ }
+ } else {
+ return Err(HIDError::UnexpectedCmd(cmd.into()));
+ }
+ }
+ }
+
+ // This is ugly as we have 2 init-functions now, but the fastest way currently.
+ fn init(&mut self, nonce: Nonce) -> Result<(), HIDError> {
+ let resp = <Self as HIDDevice>::initialize(self, nonce)?;
+ // TODO(baloo): this logic should be moved to
+ // transport/mod.rs::Device trait
+ if self.supports_ctap2() {
+ let command = GetInfo::default();
+ let info = self.send_cbor(&command)?;
+ debug!("{:?} infos: {:?}", self.id(), info);
+
+ self.set_authenticator_info(info);
+ }
+ if self.supports_ctap1() {
+ let command = GetVersion::default();
+ // We don't really use the result here
+ self.send_ctap1(&command)?;
+ }
+ Ok(resp)
+ }
+
+ fn block_and_blink(&mut self) -> BlinkResult {
+ let resp;
+ let supports_select_cmd = self
+ .get_authenticator_info()
+ .map_or(false, |i| i.versions.contains(&String::from("FIDO_2_1")));
+ if supports_select_cmd {
+ let msg = Selection {};
+ resp = self.send_cbor(&msg);
+ } else {
+ // We need to fake a blink-request, because FIDO2.0 forgot to specify one
+ // See: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#using-pinToken-in-authenticatorMakeCredential
+ let mut msg = match dummy_make_credentials_cmd() {
+ Ok(m) => m,
+ Err(_) => {
+ return BlinkResult::Cancelled;
+ }
+ };
+ // Using a zero-length pinAuth will trigger the device to blink
+ // For CTAP1, this gets ignored anyways and we do a 'normal' register
+ // command, which also just blinks.
+ msg.set_pin_auth(Some(PinAuth::empty_pin_auth()), None);
+ info!("Trying to blink: {:?}", &msg);
+ // We don't care about the Ok-value, just if it is Ok or not
+ resp = self.send_msg(&msg).map(|_| ());
+ }
+ match resp {
+ // Spec only says PinInvalid or PinNotSet should be returned on the fake touch-request,
+ // but Yubikeys for example return PinAuthInvalid. A successful return is also possible
+ // for CTAP1-tokens so we catch those here as well.
+ Ok(_)
+ | Err(HIDError::Command(CommandError::StatusCode(StatusCode::PinInvalid, _)))
+ | Err(HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthInvalid, _)))
+ | Err(HIDError::Command(CommandError::StatusCode(StatusCode::PinNotSet, _))) => {
+ BlinkResult::DeviceSelected
+ }
+ // We cancelled the receive, because another device was selected.
+ Err(HIDError::Command(CommandError::StatusCode(StatusCode::KeepaliveCancel, _)))
+ | Err(HIDError::Command(CommandError::StatusCode(StatusCode::OperationDenied, _)))
+ | Err(HIDError::Command(CommandError::StatusCode(StatusCode::UserActionTimeout, _))) => {
+ // TODO: Repeat the request, if it is a UserActionTimeout?
+ debug!("Device {:?} got cancelled", &self);
+ BlinkResult::Cancelled
+ }
+ // Something unexpected happened, so we assume this device is not usable and interpreting
+ // this equivalent to being cancelled.
+ e => {
+ info!("Device {:?} received unexpected answer, so we assume an error occurred and we are NOT using this device (assuming the request was cancelled): {:?}", &self, e);
+ BlinkResult::Cancelled
+ }
+ }
+ }
+ fn supports_ctap1(&self) -> bool {
+ // CAPABILITY_NMSG:
+ // If set to 1, authenticator DOES NOT implement U2FHID_MSG function
+ !self.get_device_info().cap_flags.contains(Capability::NMSG)
+ }
+
+ fn supports_ctap2(&self) -> bool {
+ self.get_device_info().cap_flags.contains(Capability::CBOR)
+ }
+
+ fn establish_shared_secret(&mut self) -> Result<(ECDHSecret, AuthenticatorInfo), HIDError> {
+ if !self.supports_ctap2() {
+ return Err(HIDError::UnsupportedCommand);
+ }
+
+ let info = if let Some(authenticator_info) = self.get_authenticator_info().cloned() {
+ authenticator_info
+ } else {
+ // We should already have it, since it is queried upon `init()`, but just to be safe
+ let info_command = GetInfo::default();
+ let info = self.send_cbor(&info_command)?;
+ debug!("infos: {:?}", info);
+
+ self.set_authenticator_info(info.clone());
+ info
+ };
+
+ // Not reusing the shared secret here, if it exists, since we might start again
+ // with a different PIN (e.g. if the last one was wrong)
+ let pin_command = GetKeyAgreement::new(&info)?;
+ let device_key_agreement = self.send_cbor(&pin_command)?;
+ let shared_secret = device_key_agreement.shared_secret()?;
+ self.set_shared_secret(shared_secret.clone());
+ Ok((shared_secret, info))
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/netbsd/device.rs b/third_party/rust/authenticator/src/transport/netbsd/device.rs
new file mode 100644
index 0000000000..cb1d6570b9
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/netbsd/device.rs
@@ -0,0 +1,227 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate libc;
+use crate::consts::{CID_BROADCAST, MAX_HID_RPT_SIZE};
+use crate::transport::hid::HIDDevice;
+use crate::transport::platform::fd::Fd;
+use crate::transport::platform::monitor::WrappedOpenDevice;
+use crate::transport::platform::uhid;
+use crate::transport::{AuthenticatorInfo, ECDHSecret, FidoDevice, HIDError};
+use crate::u2ftypes::{U2FDevice, U2FDeviceInfo};
+use crate::util::io_err;
+use std::ffi::OsString;
+use std::hash::{Hash, Hasher};
+use std::io::{self, Read, Write};
+use std::mem;
+
+#[derive(Debug)]
+pub struct Device {
+ path: OsString,
+ fd: Fd,
+ cid: [u8; 4],
+ dev_info: Option<U2FDeviceInfo>,
+ secret: Option<ECDHSecret>,
+ authenticator_info: Option<AuthenticatorInfo>,
+}
+
+impl Device {
+ fn ping(&mut self) -> io::Result<()> {
+ for i in 0..10 {
+ let mut buf = vec![0u8; 1 + MAX_HID_RPT_SIZE];
+
+ buf[0] = 0; // report number
+ buf[1] = 0xff; // CID_BROADCAST
+ buf[2] = 0xff;
+ buf[3] = 0xff;
+ buf[4] = 0xff;
+ buf[5] = 0x81; // ping
+ buf[6] = 0;
+ buf[7] = 1; // one byte
+
+ self.write(&buf[..])?;
+
+ // Wait for response
+ let mut pfd: libc::pollfd = unsafe { mem::zeroed() };
+ pfd.fd = self.fd.fileno;
+ pfd.events = libc::POLLIN;
+ let nfds = unsafe { libc::poll(&mut pfd, 1, 100) };
+ if nfds == -1 {
+ return Err(io::Error::last_os_error());
+ }
+ if nfds == 0 {
+ debug!("device timeout {}", i);
+ continue;
+ }
+
+ // Read response
+ self.read(&mut buf[..])?;
+
+ return Ok(());
+ }
+
+ Err(io_err("no response from device"))
+ }
+}
+
+impl PartialEq for Device {
+ fn eq(&self, other: &Device) -> bool {
+ self.fd == other.fd
+ }
+}
+
+impl Eq for Device {}
+
+impl Hash for Device {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.fd.hash(state);
+ }
+}
+
+impl Read for Device {
+ fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+ let bufp = buf.as_mut_ptr() as *mut libc::c_void;
+ let nread = unsafe { libc::read(self.fd.fileno, bufp, buf.len()) };
+ if nread == -1 {
+ return Err(io::Error::last_os_error());
+ }
+ Ok(nread as usize)
+ }
+}
+
+impl Write for Device {
+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ // Always skip the first byte (report number)
+ let data = &buf[1..];
+ let data_ptr = data.as_ptr() as *const libc::c_void;
+ let nwrit = unsafe { libc::write(self.fd.fileno, data_ptr, data.len()) };
+ if nwrit == -1 {
+ return Err(io::Error::last_os_error());
+ }
+ // Pretend we wrote the report number byte
+ Ok(nwrit as usize + 1)
+ }
+
+ fn flush(&mut self) -> io::Result<()> {
+ Ok(())
+ }
+}
+
+impl U2FDevice for Device {
+ fn get_cid(&self) -> &[u8; 4] {
+ &self.cid
+ }
+
+ fn set_cid(&mut self, cid: [u8; 4]) {
+ self.cid = cid;
+ }
+
+ fn in_rpt_size(&self) -> usize {
+ MAX_HID_RPT_SIZE
+ }
+
+ fn out_rpt_size(&self) -> usize {
+ MAX_HID_RPT_SIZE
+ }
+
+ fn get_property(&self, _prop_name: &str) -> io::Result<String> {
+ Err(io::Error::new(io::ErrorKind::Other, "Not implemented"))
+ }
+
+ fn get_device_info(&self) -> U2FDeviceInfo {
+ // unwrap is okay, as dev_info must have already been set, else
+ // a programmer error
+ self.dev_info.clone().unwrap()
+ }
+
+ fn set_device_info(&mut self, dev_info: U2FDeviceInfo) {
+ self.dev_info = Some(dev_info);
+ }
+}
+
+impl HIDDevice for Device {
+ type BuildParameters = WrappedOpenDevice;
+ type Id = OsString;
+
+ fn new(fido: WrappedOpenDevice) -> Result<Self, (HIDError, Self::Id)> {
+ debug!("device found: {:?}", fido);
+ let mut res = Self {
+ path: fido.os_path,
+ fd: fido.fd,
+ cid: CID_BROADCAST,
+ dev_info: None,
+ secret: None,
+ authenticator_info: None,
+ };
+ if res.is_u2f() {
+ info!("new device {:?}", res.path);
+ Ok(res)
+ } else {
+ Err((HIDError::DeviceNotSupported, res.path.clone()))
+ }
+ }
+
+ fn initialized(&self) -> bool {
+ // During successful init, the broadcast channel id gets repplaced by an actual one
+ self.cid != CID_BROADCAST
+ }
+
+ fn id(&self) -> Self::Id {
+ self.path.clone()
+ }
+
+ fn is_u2f(&mut self) -> bool {
+ if !uhid::is_u2f_device(&self.fd) {
+ return false;
+ }
+ // This step is not strictly necessary -- NetBSD puts fido
+ // devices into raw mode automatically by default, but in
+ // principle that might change, and this serves as a test to
+ // verify that we're running on a kernel with support for raw
+ // mode at all so we don't get confused issuing writes that try
+ // to set the report descriptor rather than transfer data on
+ // the output interrupt pipe as we need.
+ match uhid::hid_set_raw(&self.fd, true) {
+ Ok(_) => (),
+ Err(_) => return false,
+ }
+ if let Err(_) = self.ping() {
+ return false;
+ }
+ true
+ }
+
+ fn get_shared_secret(&self) -> Option<&ECDHSecret> {
+ self.secret.as_ref()
+ }
+
+ fn set_shared_secret(&mut self, secret: ECDHSecret) {
+ self.secret = Some(secret);
+ }
+
+ fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> {
+ self.authenticator_info.as_ref()
+ }
+
+ fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) {
+ self.authenticator_info = Some(authenticator_info);
+ }
+
+ /// This is used for cancellation of blocking read()-requests.
+ /// With this, we can clone the Device, pass it to another thread and call "cancel()" on that.
+ fn clone_device_as_write_only(&self) -> Result<Self, HIDError> {
+ // Try to open the device.
+ let fd = Fd::open(&self.path, libc::O_WRONLY)?;
+ Ok(Self {
+ path: self.path.clone(),
+ fd,
+ cid: self.cid,
+ dev_info: self.dev_info.clone(),
+ secret: self.secret.clone(),
+ authenticator_info: self.authenticator_info.clone(),
+ })
+ }
+}
+
+impl FidoDevice for Device {}
diff --git a/third_party/rust/authenticator/src/transport/netbsd/fd.rs b/third_party/rust/authenticator/src/transport/netbsd/fd.rs
new file mode 100644
index 0000000000..d45410843b
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/netbsd/fd.rs
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate libc;
+
+use std::ffi::CString;
+use std::ffi::OsStr;
+use std::hash::{Hash, Hasher};
+use std::io;
+use std::mem;
+use std::os::raw::c_int;
+use std::os::unix::{ffi::OsStrExt, io::RawFd};
+
+#[derive(Debug)]
+pub struct Fd {
+ pub fileno: RawFd,
+}
+
+impl Fd {
+ pub fn open(path: &OsStr, flags: c_int) -> io::Result<Fd> {
+ let cpath = CString::new(path.as_bytes())?;
+ let rv = unsafe { libc::open(cpath.as_ptr(), flags) };
+ if rv == -1 {
+ return Err(io::Error::last_os_error());
+ }
+ Ok(Fd { fileno: rv })
+ }
+}
+
+impl Drop for Fd {
+ fn drop(&mut self) {
+ unsafe { libc::close(self.fileno) };
+ }
+}
+
+impl PartialEq for Fd {
+ fn eq(&self, other: &Fd) -> bool {
+ let mut st: libc::stat = unsafe { mem::zeroed() };
+ let mut sto: libc::stat = unsafe { mem::zeroed() };
+ if unsafe { libc::fstat(self.fileno, &mut st) } == -1 {
+ return false;
+ }
+ if unsafe { libc::fstat(other.fileno, &mut sto) } == -1 {
+ return false;
+ }
+ (st.st_dev == sto.st_dev) & (st.st_ino == sto.st_ino)
+ }
+}
+
+impl Eq for Fd {}
+
+impl Hash for Fd {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ let mut st: libc::stat = unsafe { mem::zeroed() };
+ if unsafe { libc::fstat(self.fileno, &mut st) } == -1 {
+ return;
+ }
+ st.st_dev.hash(state);
+ st.st_ino.hash(state);
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/netbsd/mod.rs b/third_party/rust/authenticator/src/transport/netbsd/mod.rs
new file mode 100644
index 0000000000..a0eabb6e06
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/netbsd/mod.rs
@@ -0,0 +1,10 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+pub mod device;
+pub mod transaction;
+
+mod fd;
+mod monitor;
+mod uhid;
diff --git a/third_party/rust/authenticator/src/transport/netbsd/monitor.rs b/third_party/rust/authenticator/src/transport/netbsd/monitor.rs
new file mode 100644
index 0000000000..1b15733b5f
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/netbsd/monitor.rs
@@ -0,0 +1,132 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::transport::device_selector::DeviceSelectorEvent;
+use crate::transport::platform::fd::Fd;
+use runloop::RunLoop;
+use std::collections::HashMap;
+use std::error::Error;
+use std::ffi::OsString;
+use std::sync::{mpsc::Sender, Arc};
+use std::thread;
+use std::time::Duration;
+
+// XXX Should use drvctl, but it doesn't do pubsub properly yet so
+// DRVGETEVENT requires write access to /dev/drvctl. Instead, for now,
+// just poll every 500ms.
+const POLL_TIMEOUT: u64 = 500;
+
+#[derive(Debug)]
+pub struct WrappedOpenDevice {
+ pub fd: Fd,
+ pub os_path: OsString,
+}
+
+pub struct Monitor<F>
+where
+ F: Fn(
+ WrappedOpenDevice,
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Sync,
+{
+ runloops: HashMap<OsString, RunLoop>,
+ new_device_cb: Arc<F>,
+ selector_sender: Sender<DeviceSelectorEvent>,
+ status_sender: Sender<crate::StatusUpdate>,
+}
+
+impl<F> Monitor<F>
+where
+ F: Fn(
+ WrappedOpenDevice,
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Send
+ + Sync
+ + 'static,
+{
+ pub fn new(
+ new_device_cb: F,
+ selector_sender: Sender<DeviceSelectorEvent>,
+ status_sender: Sender<crate::StatusUpdate>,
+ ) -> Self {
+ Self {
+ runloops: HashMap::new(),
+ new_device_cb: Arc::new(new_device_cb),
+ selector_sender,
+ status_sender,
+ }
+ }
+
+ pub fn run(&mut self, alive: &dyn Fn() -> bool) -> Result<(), Box<dyn Error>> {
+ // Loop until we're stopped by the controlling thread, or fail.
+ while alive() {
+ for n in 0..100 {
+ let uhidpath = OsString::from(format!("/dev/uhid{}", n));
+ match Fd::open(&uhidpath, libc::O_RDWR | libc::O_CLOEXEC) {
+ Ok(uhid) => {
+ // The device is available if it can be opened.
+ let _ = self
+ .selector_sender
+ .send(DeviceSelectorEvent::DevicesAdded(vec![uhidpath.clone()]));
+ self.add_device(WrappedOpenDevice {
+ fd: uhid,
+ os_path: uhidpath,
+ });
+ }
+ Err(ref err) => match err.raw_os_error() {
+ Some(libc::EBUSY) => continue,
+ Some(libc::ENOENT) => break,
+ _ => self.remove_device(uhidpath),
+ },
+ }
+ }
+ thread::sleep(Duration::from_millis(POLL_TIMEOUT));
+ }
+
+ // Remove all tracked devices.
+ self.remove_all_devices();
+
+ Ok(())
+ }
+
+ fn add_device(&mut self, fido: WrappedOpenDevice) {
+ let f = self.new_device_cb.clone();
+ let selector_sender = self.selector_sender.clone();
+ let status_sender = self.status_sender.clone();
+ let key = fido.os_path.clone();
+ debug!("Adding device {}", key.to_string_lossy());
+
+ let runloop = RunLoop::new(move |alive| {
+ if alive() {
+ f(fido, selector_sender, status_sender, alive);
+ }
+ });
+
+ if let Ok(runloop) = runloop {
+ self.runloops.insert(key, runloop);
+ }
+ }
+
+ fn remove_device(&mut self, path: OsString) {
+ let _ = self
+ .selector_sender
+ .send(DeviceSelectorEvent::DeviceRemoved(path.clone()));
+
+ debug!("Removing device {}", path.to_string_lossy());
+ if let Some(runloop) = self.runloops.remove(&path) {
+ runloop.cancel();
+ }
+ }
+
+ fn remove_all_devices(&mut self) {
+ while !self.runloops.is_empty() {
+ let path = self.runloops.keys().next().unwrap().clone();
+ self.remove_device(path);
+ }
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/netbsd/transaction.rs b/third_party/rust/authenticator/src/transport/netbsd/transaction.rs
new file mode 100644
index 0000000000..35baa8f3c4
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/netbsd/transaction.rs
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::errors;
+use crate::statecallback::StateCallback;
+use crate::transport::device_selector::{
+ DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent,
+};
+use crate::transport::platform::monitor::Monitor;
+use runloop::RunLoop;
+use std::sync::mpsc::Sender;
+
+pub struct Transaction {
+ // Handle to the thread loop.
+ thread: RunLoop,
+ device_selector: DeviceSelector,
+}
+
+impl Transaction {
+ pub fn new<F, T>(
+ timeout: u64,
+ callback: StateCallback<crate::Result<T>>,
+ status: Sender<crate::StatusUpdate>,
+ new_device_cb: F,
+ ) -> crate::Result<Self>
+ where
+ F: Fn(
+ DeviceBuildParameters,
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Sync
+ + Send
+ + 'static,
+ T: 'static,
+ {
+ let status_sender = status.clone();
+ let device_selector = DeviceSelector::run(status);
+ let selector_sender = device_selector.clone_sender();
+ let thread = RunLoop::new_with_timeout(
+ move |alive| {
+ // Create a new device monitor.
+ let mut monitor = Monitor::new(new_device_cb, selector_sender, status_sender);
+
+ // Start polling for new devices.
+ try_or!(monitor.run(alive), |_| callback
+ .call(Err(errors::AuthenticatorError::Platform)));
+
+ // Send an error, if the callback wasn't called already.
+ callback.call(Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::NotAllowed,
+ )));
+ },
+ timeout,
+ )
+ .map_err(|_| errors::AuthenticatorError::Platform)?;
+
+ Ok(Self {
+ thread,
+ device_selector,
+ })
+ }
+
+ pub fn cancel(&mut self) {
+ info!("Transaction was cancelled.");
+ self.device_selector.stop();
+ self.thread.cancel();
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/netbsd/uhid.rs b/third_party/rust/authenticator/src/transport/netbsd/uhid.rs
new file mode 100644
index 0000000000..ea183db998
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/netbsd/uhid.rs
@@ -0,0 +1,77 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate libc;
+
+use std::io;
+use std::mem;
+use std::os::raw::c_int;
+use std::os::raw::c_uchar;
+
+use crate::transport::hidproto::has_fido_usage;
+use crate::transport::hidproto::ReportDescriptor;
+use crate::transport::platform::fd::Fd;
+use crate::util::io_err;
+
+/* sys/ioccom.h */
+
+const IOCPARM_MASK: u32 = 0x1fff;
+const IOCPARM_SHIFT: u32 = 16;
+const IOCGROUP_SHIFT: u32 = 8;
+
+//const IOC_VOID: u32 = 0x20000000;
+const IOC_OUT: u32 = 0x40000000;
+const IOC_IN: u32 = 0x80000000;
+//const IOC_INOUT: u32 = IOC_IN|IOC_OUT;
+
+macro_rules! ioctl {
+ ($dir:expr, $name:ident, $group:expr, $nr:expr, $ty:ty) => {
+ unsafe fn $name(fd: libc::c_int, val: *mut $ty) -> io::Result<libc::c_int> {
+ let ioc = ($dir as u32)
+ | ((mem::size_of::<$ty>() as u32 & IOCPARM_MASK) << IOCPARM_SHIFT)
+ | (($group as u32) << IOCGROUP_SHIFT)
+ | ($nr as u32);
+ let rv = libc::ioctl(fd, ioc as libc::c_ulong, val);
+ if rv == -1 {
+ return Err(io::Error::last_os_error());
+ }
+ Ok(rv)
+ }
+ };
+}
+
+#[allow(non_camel_case_types)]
+#[repr(C)]
+struct usb_ctl_report_desc {
+ ucrd_size: c_int,
+ ucrd_data: [c_uchar; 1024],
+}
+
+ioctl!(IOC_OUT, usb_get_report_desc, b'U', 21, usb_ctl_report_desc);
+
+fn read_report_descriptor(fd: &Fd) -> io::Result<ReportDescriptor> {
+ let mut desc = unsafe { mem::zeroed() };
+ unsafe { usb_get_report_desc(fd.fileno, &mut desc) }?;
+ if desc.ucrd_size < 0 {
+ return Err(io_err("negative report descriptor size"));
+ }
+ let size = desc.ucrd_size as usize;
+ let value = Vec::from(&desc.ucrd_data[..size]);
+ Ok(ReportDescriptor { value })
+}
+
+pub fn is_u2f_device(fd: &Fd) -> bool {
+ match read_report_descriptor(fd) {
+ Ok(desc) => has_fido_usage(desc),
+ Err(_) => false,
+ }
+}
+
+ioctl!(IOC_IN, usb_hid_set_raw_ioctl, b'h', 2, c_int);
+
+pub fn hid_set_raw(fd: &Fd, raw: bool) -> io::Result<()> {
+ let mut raw_int: c_int = if raw { 1 } else { 0 };
+ unsafe { usb_hid_set_raw_ioctl(fd.fileno, &mut raw_int) }?;
+ Ok(())
+}
diff --git a/third_party/rust/authenticator/src/transport/openbsd/device.rs b/third_party/rust/authenticator/src/transport/openbsd/device.rs
new file mode 100644
index 0000000000..aff6f9a498
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/openbsd/device.rs
@@ -0,0 +1,226 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate libc;
+use crate::consts::{CID_BROADCAST, MAX_HID_RPT_SIZE};
+use crate::transport::hid::HIDDevice;
+use crate::transport::platform::monitor::WrappedOpenDevice;
+use crate::transport::{AuthenticatorInfo, ECDHSecret, FidoDevice, HIDError};
+use crate::u2ftypes::{U2FDevice, U2FDeviceInfo};
+use crate::util::{from_unix_result, io_err};
+use std::ffi::{CString, OsString};
+use std::hash::{Hash, Hasher};
+use std::io::{self, Read, Write};
+use std::mem;
+use std::os::unix::ffi::OsStrExt;
+
+#[derive(Debug)]
+pub struct Device {
+ path: OsString,
+ fd: libc::c_int,
+ in_rpt_size: usize,
+ out_rpt_size: usize,
+ cid: [u8; 4],
+ dev_info: Option<U2FDeviceInfo>,
+ secret: Option<ECDHSecret>,
+ authenticator_info: Option<AuthenticatorInfo>,
+}
+
+impl Device {
+ fn ping(&mut self) -> io::Result<()> {
+ let capacity = 256;
+
+ for _ in 0..10 {
+ let mut data = vec![0u8; capacity];
+
+ // Send 1 byte ping
+ // self.write_all requires Device to be mut. This can't be done at the moment,
+ // and this is a workaround anyways, so writing by hand instead.
+ self.write_all(&[0, 0xff, 0xff, 0xff, 0xff, 0x81, 0, 1])?;
+
+ // Wait for response
+ let mut pfd: libc::pollfd = unsafe { mem::zeroed() };
+ pfd.fd = self.fd;
+ pfd.events = libc::POLLIN;
+ if from_unix_result(unsafe { libc::poll(&mut pfd, 1, 100) })? == 0 {
+ debug!("device {:?} timeout", self.path);
+ continue;
+ }
+
+ // Read response
+ self.read(&mut data[..])?;
+
+ return Ok(());
+ }
+
+ Err(io_err("no response from device"))
+ }
+}
+
+impl Drop for Device {
+ fn drop(&mut self) {
+ // Close the fd, ignore any errors.
+ let _ = unsafe { libc::close(self.fd) };
+ debug!("device {:?} closed", self.path);
+ }
+}
+
+impl PartialEq for Device {
+ fn eq(&self, other: &Device) -> bool {
+ self.path == other.path
+ }
+}
+
+impl Eq for Device {}
+
+impl Hash for Device {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ // The path should be the only identifying member for a device
+ // If the path is the same, its the same device
+ self.path.hash(state);
+ }
+}
+
+impl Read for Device {
+ fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+ let buf_ptr = buf.as_mut_ptr() as *mut libc::c_void;
+ let rv = unsafe { libc::read(self.fd, buf_ptr, buf.len()) };
+ from_unix_result(rv as usize)
+ }
+}
+
+impl Write for Device {
+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ // Always skip the first byte (report number)
+ let data = &buf[1..];
+ let data_ptr = data.as_ptr() as *const libc::c_void;
+ let rv = unsafe { libc::write(self.fd, data_ptr, data.len()) };
+ Ok(from_unix_result(rv as usize)? + 1)
+ }
+
+ fn flush(&mut self) -> io::Result<()> {
+ Ok(())
+ }
+}
+
+impl U2FDevice for Device {
+ fn get_cid(&self) -> &[u8; 4] {
+ &self.cid
+ }
+
+ fn set_cid(&mut self, cid: [u8; 4]) {
+ self.cid = cid;
+ }
+
+ fn in_rpt_size(&self) -> usize {
+ self.in_rpt_size
+ }
+
+ fn out_rpt_size(&self) -> usize {
+ self.out_rpt_size
+ }
+
+ fn get_property(&self, _prop_name: &str) -> io::Result<String> {
+ Err(io::Error::new(io::ErrorKind::Other, "Not implemented"))
+ }
+
+ fn get_device_info(&self) -> U2FDeviceInfo {
+ // unwrap is okay, as dev_info must have already been set, else
+ // a programmer error
+ self.dev_info.clone().unwrap()
+ }
+
+ fn set_device_info(&mut self, dev_info: U2FDeviceInfo) {
+ self.dev_info = Some(dev_info);
+ }
+}
+
+impl HIDDevice for Device {
+ type BuildParameters = WrappedOpenDevice;
+ type Id = OsString;
+
+ fn new(fido: WrappedOpenDevice) -> Result<Self, (HIDError, Self::Id)> {
+ debug!("device found: {:?}", fido);
+ let mut res = Self {
+ path: fido.os_path,
+ fd: fido.fd,
+ in_rpt_size: MAX_HID_RPT_SIZE,
+ out_rpt_size: MAX_HID_RPT_SIZE,
+ cid: CID_BROADCAST,
+ dev_info: None,
+ secret: None,
+ authenticator_info: None,
+ };
+ if res.is_u2f() {
+ info!("new device {:?}", res.path);
+ Ok(res)
+ } else {
+ Err((HIDError::DeviceNotSupported, res.path.clone()))
+ }
+ }
+
+ fn initialized(&self) -> bool {
+ // During successful init, the broadcast channel id gets repplaced by an actual one
+ self.cid != CID_BROADCAST
+ }
+
+ fn id(&self) -> Self::Id {
+ self.path.clone()
+ }
+
+ fn is_u2f(&mut self) -> bool {
+ debug!("device {:?} is U2F/FIDO", self.path);
+
+ // From OpenBSD's libfido2 in 6.6-current:
+ // "OpenBSD (as of 201910) has a bug that causes it to lose
+ // track of the DATA0/DATA1 sequence toggle across uhid device
+ // open and close. This is a terrible hack to work around it."
+ match self.ping() {
+ Ok(_) => true,
+ Err(err) => {
+ debug!("device {:?} is not responding: {}", self.path, err);
+ false
+ }
+ }
+ }
+
+ fn get_shared_secret(&self) -> Option<&ECDHSecret> {
+ self.secret.as_ref()
+ }
+
+ fn set_shared_secret(&mut self, secret: ECDHSecret) {
+ self.secret = Some(secret);
+ }
+
+ fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> {
+ self.authenticator_info.as_ref()
+ }
+
+ fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) {
+ self.authenticator_info = Some(authenticator_info);
+ }
+
+ /// This is used for cancellation of blocking read()-requests.
+ /// With this, we can clone the Device, pass it to another thread and call "cancel()" on that.
+ fn clone_device_as_write_only(&self) -> Result<Self, HIDError> {
+ // Try to open the device.
+ // This can't really error out as we already did this conversion
+ let cstr = CString::new(self.path.as_bytes()).map_err(|_| (HIDError::DeviceError))?;
+ let fd = unsafe { libc::open(cstr.as_ptr(), libc::O_WRONLY) };
+ let fd =
+ from_unix_result(fd).map_err(|e| (HIDError::IO(Some(self.path.clone().into()), e)))?;
+ Ok(Self {
+ path: self.path.clone(),
+ fd,
+ in_rpt_size: self.in_rpt_size,
+ out_rpt_size: self.out_rpt_size,
+ cid: self.cid,
+ dev_info: self.dev_info.clone(),
+ secret: self.secret.clone(),
+ authenticator_info: self.authenticator_info.clone(),
+ })
+ }
+}
+
+impl FidoDevice for Device {}
diff --git a/third_party/rust/authenticator/src/transport/openbsd/mod.rs b/third_party/rust/authenticator/src/transport/openbsd/mod.rs
new file mode 100644
index 0000000000..fa02132e67
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/openbsd/mod.rs
@@ -0,0 +1,8 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+pub mod device;
+pub mod transaction;
+
+mod monitor;
diff --git a/third_party/rust/authenticator/src/transport/openbsd/monitor.rs b/third_party/rust/authenticator/src/transport/openbsd/monitor.rs
new file mode 100644
index 0000000000..0ea5f3d0b8
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/openbsd/monitor.rs
@@ -0,0 +1,138 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::transport::device_selector::DeviceSelectorEvent;
+use crate::util::from_unix_result;
+use runloop::RunLoop;
+use std::collections::HashMap;
+use std::error::Error;
+use std::ffi::{CString, OsString};
+use std::os::unix::ffi::OsStrExt;
+use std::os::unix::io::RawFd;
+use std::path::PathBuf;
+use std::sync::{mpsc::Sender, Arc};
+use std::thread;
+use std::time::Duration;
+
+const POLL_TIMEOUT: u64 = 500;
+
+#[derive(Debug)]
+pub struct WrappedOpenDevice {
+ pub fd: RawFd,
+ pub os_path: OsString,
+}
+
+pub struct Monitor<F>
+where
+ F: Fn(
+ WrappedOpenDevice,
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Sync,
+{
+ runloops: HashMap<OsString, RunLoop>,
+ new_device_cb: Arc<F>,
+ selector_sender: Sender<DeviceSelectorEvent>,
+ status_sender: Sender<crate::StatusUpdate>,
+}
+
+impl<F> Monitor<F>
+where
+ F: Fn(
+ WrappedOpenDevice,
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Send
+ + Sync
+ + 'static,
+{
+ pub fn new(
+ new_device_cb: F,
+ selector_sender: Sender<DeviceSelectorEvent>,
+ status_sender: Sender<crate::StatusUpdate>,
+ ) -> Self {
+ Self {
+ runloops: HashMap::new(),
+ new_device_cb: Arc::new(new_device_cb),
+ selector_sender,
+ status_sender,
+ }
+ }
+
+ pub fn run(&mut self, alive: &dyn Fn() -> bool) -> Result<(), Box<dyn Error>> {
+ // Loop until we're stopped by the controlling thread, or fail.
+ while alive() {
+ // Iterate the first 10 fido(4) devices.
+ for path in (0..10)
+ .map(|unit| PathBuf::from(&format!("/dev/fido/{}", unit)))
+ .filter(|path| path.exists())
+ {
+ let os_path = path.as_os_str().to_os_string();
+ let cstr = CString::new(os_path.as_bytes())?;
+
+ // Try to open the device.
+ let fd = unsafe { libc::open(cstr.as_ptr(), libc::O_RDWR) };
+ match from_unix_result(fd) {
+ Ok(fd) => {
+ // The device is available if it can be opened.
+ let _ = self
+ .selector_sender
+ .send(DeviceSelectorEvent::DevicesAdded(vec![os_path.clone()]));
+ self.add_device(WrappedOpenDevice { fd, os_path });
+ }
+ Err(ref err) if err.raw_os_error() == Some(libc::EBUSY) => {
+ // The device is available but currently in use.
+ }
+ _ => {
+ // libc::ENODEV or any other error.
+ self.remove_device(os_path);
+ }
+ }
+ }
+
+ thread::sleep(Duration::from_millis(POLL_TIMEOUT));
+ }
+
+ // Remove all tracked devices.
+ self.remove_all_devices();
+
+ Ok(())
+ }
+
+ fn add_device(&mut self, fido: WrappedOpenDevice) {
+ if !self.runloops.contains_key(&fido.os_path) {
+ let f = self.new_device_cb.clone();
+ let key = fido.os_path.clone();
+ let selector_sender = self.selector_sender.clone();
+ let status_sender = self.status_sender.clone();
+ let runloop = RunLoop::new(move |alive| {
+ if alive() {
+ f(fido, selector_sender, status_sender, alive);
+ }
+ });
+
+ if let Ok(runloop) = runloop {
+ self.runloops.insert(key, runloop);
+ }
+ }
+ }
+
+ fn remove_device(&mut self, path: OsString) {
+ let _ = self
+ .selector_sender
+ .send(DeviceSelectorEvent::DeviceRemoved(path.clone()));
+ if let Some(runloop) = self.runloops.remove(&path) {
+ runloop.cancel();
+ }
+ }
+
+ fn remove_all_devices(&mut self) {
+ while !self.runloops.is_empty() {
+ let path = self.runloops.keys().next().unwrap().clone();
+ self.remove_device(path);
+ }
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/openbsd/transaction.rs b/third_party/rust/authenticator/src/transport/openbsd/transaction.rs
new file mode 100644
index 0000000000..35baa8f3c4
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/openbsd/transaction.rs
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::errors;
+use crate::statecallback::StateCallback;
+use crate::transport::device_selector::{
+ DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent,
+};
+use crate::transport::platform::monitor::Monitor;
+use runloop::RunLoop;
+use std::sync::mpsc::Sender;
+
+pub struct Transaction {
+ // Handle to the thread loop.
+ thread: RunLoop,
+ device_selector: DeviceSelector,
+}
+
+impl Transaction {
+ pub fn new<F, T>(
+ timeout: u64,
+ callback: StateCallback<crate::Result<T>>,
+ status: Sender<crate::StatusUpdate>,
+ new_device_cb: F,
+ ) -> crate::Result<Self>
+ where
+ F: Fn(
+ DeviceBuildParameters,
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Sync
+ + Send
+ + 'static,
+ T: 'static,
+ {
+ let status_sender = status.clone();
+ let device_selector = DeviceSelector::run(status);
+ let selector_sender = device_selector.clone_sender();
+ let thread = RunLoop::new_with_timeout(
+ move |alive| {
+ // Create a new device monitor.
+ let mut monitor = Monitor::new(new_device_cb, selector_sender, status_sender);
+
+ // Start polling for new devices.
+ try_or!(monitor.run(alive), |_| callback
+ .call(Err(errors::AuthenticatorError::Platform)));
+
+ // Send an error, if the callback wasn't called already.
+ callback.call(Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::NotAllowed,
+ )));
+ },
+ timeout,
+ )
+ .map_err(|_| errors::AuthenticatorError::Platform)?;
+
+ Ok(Self {
+ thread,
+ device_selector,
+ })
+ }
+
+ pub fn cancel(&mut self) {
+ info!("Transaction was cancelled.");
+ self.device_selector.stop();
+ self.thread.cancel();
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/stub/device.rs b/third_party/rust/authenticator/src/transport/stub/device.rs
new file mode 100644
index 0000000000..1986fdd01f
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/stub/device.rs
@@ -0,0 +1,110 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::transport::hid::HIDDevice;
+use crate::transport::FidoDevice;
+use crate::transport::{AuthenticatorInfo, ECDHSecret, HIDError};
+use crate::u2ftypes::{U2FDevice, U2FDeviceInfo};
+use std::hash::{Hash, Hasher};
+use std::io;
+use std::io::{Read, Write};
+use std::path::PathBuf;
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct Device {}
+
+impl Read for Device {
+ fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+ panic!("not implemented");
+ }
+}
+
+impl Write for Device {
+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ panic!("not implemented");
+ }
+
+ fn flush(&mut self) -> io::Result<()> {
+ panic!("not implemented");
+ }
+}
+
+impl U2FDevice for Device {
+ fn get_cid<'a>(&'a self) -> &'a [u8; 4] {
+ panic!("not implemented");
+ }
+
+ fn set_cid(&mut self, cid: [u8; 4]) {
+ panic!("not implemented");
+ }
+
+ fn in_rpt_size(&self) -> usize {
+ panic!("not implemented");
+ }
+
+ fn out_rpt_size(&self) -> usize {
+ panic!("not implemented");
+ }
+
+ fn get_property(&self, prop_name: &str) -> io::Result<String> {
+ panic!("not implemented")
+ }
+
+ fn get_device_info(&self) -> U2FDeviceInfo {
+ panic!("not implemented")
+ }
+
+ fn set_device_info(&mut self, dev_info: U2FDeviceInfo) {
+ panic!("not implemented")
+ }
+}
+
+impl HIDDevice for Device {
+ type BuildParameters = PathBuf;
+ type Id = PathBuf;
+
+ fn new(parameters: Self::BuildParameters) -> Result<Self, (HIDError, Self::Id)> {
+ unimplemented!();
+ }
+
+ fn initialized(&self) -> bool {
+ unimplemented!();
+ }
+
+ fn id(&self) -> Self::Id {
+ unimplemented!()
+ }
+
+ fn is_u2f(&mut self) -> bool {
+ unimplemented!()
+ }
+
+ fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> {
+ unimplemented!()
+ }
+
+ fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) {
+ unimplemented!()
+ }
+
+ fn set_shared_secret(&mut self, secret: ECDHSecret) {
+ unimplemented!()
+ }
+
+ fn get_shared_secret(&self) -> Option<&ECDHSecret> {
+ unimplemented!()
+ }
+
+ fn clone_device_as_write_only(&self) -> Result<Self, HIDError> {
+ unimplemented!()
+ }
+}
+
+impl Hash for Device {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ unimplemented!()
+ }
+}
+
+impl FidoDevice for Device {}
diff --git a/third_party/rust/authenticator/src/transport/stub/mod.rs b/third_party/rust/authenticator/src/transport/stub/mod.rs
new file mode 100644
index 0000000000..0fab62d495
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/stub/mod.rs
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// No-op module to permit compiling token HID support for Android, where
+// no results are returned.
+
+#![allow(unused_variables)]
+
+pub mod device;
+pub mod transaction;
diff --git a/third_party/rust/authenticator/src/transport/stub/transaction.rs b/third_party/rust/authenticator/src/transport/stub/transaction.rs
new file mode 100644
index 0000000000..21a9687ecd
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/stub/transaction.rs
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::errors;
+use crate::statecallback::StateCallback;
+use crate::transport::device_selector::{
+ DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent,
+};
+use std::path::PathBuf;
+use std::sync::mpsc::Sender;
+
+pub struct Transaction {}
+
+impl Transaction {
+ pub fn new<F, T>(
+ timeout: u64,
+ callback: StateCallback<crate::Result<T>>,
+ status: Sender<crate::StatusUpdate>,
+ new_device_cb: F,
+ ) -> crate::Result<Self>
+ where
+ F: Fn(
+ DeviceBuildParameters,
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Sync
+ + Send
+ + 'static,
+ T: 'static,
+ {
+ // Just to silence "unused"-warnings
+ let mut device_selector = DeviceSelector::run(status);
+ let _ = DeviceSelectorEvent::DevicesAdded(vec![]);
+ let _ = DeviceSelectorEvent::DeviceRemoved(PathBuf::new());
+ let _ = device_selector.clone_sender();
+ device_selector.stop();
+
+ callback.call(Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::NotSupported,
+ )));
+
+ Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::NotSupported,
+ ))
+ }
+
+ pub fn cancel(&mut self) {
+ /* No-op. */
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/windows/device.rs b/third_party/rust/authenticator/src/transport/windows/device.rs
new file mode 100644
index 0000000000..d0b0feac7d
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/windows/device.rs
@@ -0,0 +1,171 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use super::winapi::DeviceCapabilities;
+use crate::consts::{CID_BROADCAST, FIDO_USAGE_PAGE, FIDO_USAGE_U2FHID, MAX_HID_RPT_SIZE};
+use crate::transport::hid::HIDDevice;
+use crate::transport::{AuthenticatorInfo, ECDHSecret, FidoDevice, HIDError};
+use crate::u2ftypes::{U2FDevice, U2FDeviceInfo};
+use std::fs::{File, OpenOptions};
+use std::hash::{Hash, Hasher};
+use std::io::{self, Read, Write};
+use std::os::windows::io::AsRawHandle;
+
+#[derive(Debug)]
+pub struct Device {
+ path: String,
+ file: File,
+ cid: [u8; 4],
+ dev_info: Option<U2FDeviceInfo>,
+ secret: Option<ECDHSecret>,
+ authenticator_info: Option<AuthenticatorInfo>,
+}
+
+impl PartialEq for Device {
+ fn eq(&self, other: &Device) -> bool {
+ self.path == other.path
+ }
+}
+
+impl Eq for Device {}
+
+impl Hash for Device {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ // The path should be the only identifying member for a device
+ // If the path is the same, its the same device
+ self.path.hash(state);
+ }
+}
+
+impl Read for Device {
+ fn read(&mut self, bytes: &mut [u8]) -> io::Result<usize> {
+ // Windows always includes the report ID.
+ let mut input = [0u8; MAX_HID_RPT_SIZE + 1];
+ let _ = self.file.read(&mut input)?;
+ bytes.clone_from_slice(&input[1..]);
+ Ok(bytes.len() as usize)
+ }
+}
+
+impl Write for Device {
+ fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
+ self.file.write(bytes)
+ }
+
+ fn flush(&mut self) -> io::Result<()> {
+ self.file.flush()
+ }
+}
+
+impl U2FDevice for Device {
+ fn get_cid(&self) -> &[u8; 4] {
+ &self.cid
+ }
+
+ fn set_cid(&mut self, cid: [u8; 4]) {
+ self.cid = cid;
+ }
+
+ fn in_rpt_size(&self) -> usize {
+ MAX_HID_RPT_SIZE
+ }
+
+ fn out_rpt_size(&self) -> usize {
+ MAX_HID_RPT_SIZE
+ }
+
+ fn get_property(&self, _prop_name: &str) -> io::Result<String> {
+ Err(io::Error::new(io::ErrorKind::Other, "Not implemented"))
+ }
+
+ fn get_device_info(&self) -> U2FDeviceInfo {
+ // unwrap is okay, as dev_info must have already been set, else
+ // a programmer error
+ self.dev_info.clone().unwrap()
+ }
+
+ fn set_device_info(&mut self, dev_info: U2FDeviceInfo) {
+ self.dev_info = Some(dev_info);
+ }
+}
+
+impl HIDDevice for Device {
+ type BuildParameters = String;
+ type Id = String;
+
+ fn new(path: String) -> Result<Self, (HIDError, Self::Id)> {
+ debug!("Opening device {:?}", path);
+ let file = OpenOptions::new()
+ .read(true)
+ .write(true)
+ .open(&path)
+ .map_err(|e| (HIDError::IO(Some(path.clone().into()), e), path.clone()))?;
+ let mut res = Self {
+ path,
+ file,
+ cid: CID_BROADCAST,
+ dev_info: None,
+ secret: None,
+ authenticator_info: None,
+ };
+ if res.is_u2f() {
+ info!("new device {:?}", res.path);
+ Ok(res)
+ } else {
+ Err((HIDError::DeviceNotSupported, res.path.clone()))
+ }
+ }
+
+ fn initialized(&self) -> bool {
+ // During successful init, the broadcast channel id gets repplaced by an actual one
+ self.cid != CID_BROADCAST
+ }
+
+ fn id(&self) -> Self::Id {
+ self.path.clone()
+ }
+
+ fn is_u2f(&mut self) -> bool {
+ match DeviceCapabilities::new(self.file.as_raw_handle()) {
+ Ok(caps) => caps.usage() == FIDO_USAGE_U2FHID && caps.usage_page() == FIDO_USAGE_PAGE,
+ _ => false,
+ }
+ }
+
+ fn get_shared_secret(&self) -> Option<&ECDHSecret> {
+ self.secret.as_ref()
+ }
+
+ fn set_shared_secret(&mut self, secret: ECDHSecret) {
+ self.secret = Some(secret);
+ }
+
+ fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> {
+ self.authenticator_info.as_ref()
+ }
+
+ fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) {
+ self.authenticator_info = Some(authenticator_info);
+ }
+
+ /// This is used for cancellation of blocking read()-requests.
+ /// With this, we can clone the Device, pass it to another thread and call "cancel()" on that.
+ fn clone_device_as_write_only(&self) -> Result<Self, HIDError> {
+ let file = OpenOptions::new()
+ .write(true)
+ .open(&self.path)
+ .map_err(|e| (HIDError::IO(Some(self.path.clone().into()), e)))?;
+
+ Ok(Self {
+ path: self.path.clone(),
+ file,
+ cid: self.cid,
+ dev_info: self.dev_info.clone(),
+ secret: self.secret.clone(),
+ authenticator_info: self.authenticator_info.clone(),
+ })
+ }
+}
+
+impl FidoDevice for Device {}
diff --git a/third_party/rust/authenticator/src/transport/windows/mod.rs b/third_party/rust/authenticator/src/transport/windows/mod.rs
new file mode 100644
index 0000000000..09135391dd
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/windows/mod.rs
@@ -0,0 +1,9 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+pub mod device;
+pub mod transaction;
+
+mod monitor;
+mod winapi;
diff --git a/third_party/rust/authenticator/src/transport/windows/monitor.rs b/third_party/rust/authenticator/src/transport/windows/monitor.rs
new file mode 100644
index 0000000000..8bc4fc70d3
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/windows/monitor.rs
@@ -0,0 +1,115 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::transport::device_selector::DeviceSelectorEvent;
+use crate::transport::platform::winapi::DeviceInfoSet;
+use runloop::RunLoop;
+use std::collections::{HashMap, HashSet};
+use std::error::Error;
+use std::iter::FromIterator;
+use std::sync::{mpsc::Sender, Arc};
+use std::thread;
+use std::time::Duration;
+
+pub struct Monitor<F>
+where
+ F: Fn(String, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool)
+ + Sync,
+{
+ runloops: HashMap<String, RunLoop>,
+ new_device_cb: Arc<F>,
+ selector_sender: Sender<DeviceSelectorEvent>,
+ status_sender: Sender<crate::StatusUpdate>,
+}
+
+impl<F> Monitor<F>
+where
+ F: Fn(String, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool)
+ + Send
+ + Sync
+ + 'static,
+{
+ pub fn new(
+ new_device_cb: F,
+ selector_sender: Sender<DeviceSelectorEvent>,
+ status_sender: Sender<crate::StatusUpdate>,
+ ) -> Self {
+ Self {
+ runloops: HashMap::new(),
+ new_device_cb: Arc::new(new_device_cb),
+ selector_sender,
+ status_sender,
+ }
+ }
+
+ pub fn run(&mut self, alive: &dyn Fn() -> bool) -> Result<(), Box<dyn Error>> {
+ let mut stored = HashSet::new();
+
+ while alive() {
+ let device_info_set = DeviceInfoSet::new()?;
+ let devices = HashSet::from_iter(device_info_set.devices());
+
+ // Remove devices that are gone.
+ for path in stored.difference(&devices) {
+ self.remove_device(path);
+ }
+
+ let paths: Vec<_> = devices.difference(&stored).cloned().collect();
+ self.selector_sender
+ .send(DeviceSelectorEvent::DevicesAdded(paths.clone()))?;
+ // Add devices that were plugged in.
+ for path in paths {
+ self.add_device(&path);
+ }
+
+ // Remember the new set.
+ stored = devices;
+
+ // Wait a little before looking for devices again.
+ thread::sleep(Duration::from_millis(100));
+ }
+
+ // Remove all tracked devices.
+ self.remove_all_devices();
+
+ Ok(())
+ }
+
+ fn add_device(&mut self, path: &String) {
+ let f = self.new_device_cb.clone();
+ let path = path.clone();
+ let key = path.clone();
+ let selector_sender = self.selector_sender.clone();
+ let status_sender = self.status_sender.clone();
+ debug!("Adding device {}", path);
+
+ let runloop = RunLoop::new(move |alive| {
+ if alive() {
+ f(path, selector_sender, status_sender, alive);
+ }
+ });
+
+ if let Ok(runloop) = runloop {
+ self.runloops.insert(key, runloop);
+ }
+ }
+
+ fn remove_device(&mut self, path: &String) {
+ let _ = self
+ .selector_sender
+ .send(DeviceSelectorEvent::DeviceRemoved(path.clone()));
+
+ debug!("Removing device {}", path);
+ if let Some(runloop) = self.runloops.remove(path) {
+ runloop.cancel();
+ }
+ }
+
+ fn remove_all_devices(&mut self) {
+ while !self.runloops.is_empty() {
+ let path = self.runloops.keys().next().unwrap().clone();
+ self.remove_device(&path);
+ }
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/windows/transaction.rs b/third_party/rust/authenticator/src/transport/windows/transaction.rs
new file mode 100644
index 0000000000..35baa8f3c4
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/windows/transaction.rs
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::errors;
+use crate::statecallback::StateCallback;
+use crate::transport::device_selector::{
+ DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent,
+};
+use crate::transport::platform::monitor::Monitor;
+use runloop::RunLoop;
+use std::sync::mpsc::Sender;
+
+pub struct Transaction {
+ // Handle to the thread loop.
+ thread: RunLoop,
+ device_selector: DeviceSelector,
+}
+
+impl Transaction {
+ pub fn new<F, T>(
+ timeout: u64,
+ callback: StateCallback<crate::Result<T>>,
+ status: Sender<crate::StatusUpdate>,
+ new_device_cb: F,
+ ) -> crate::Result<Self>
+ where
+ F: Fn(
+ DeviceBuildParameters,
+ Sender<DeviceSelectorEvent>,
+ Sender<crate::StatusUpdate>,
+ &dyn Fn() -> bool,
+ ) + Sync
+ + Send
+ + 'static,
+ T: 'static,
+ {
+ let status_sender = status.clone();
+ let device_selector = DeviceSelector::run(status);
+ let selector_sender = device_selector.clone_sender();
+ let thread = RunLoop::new_with_timeout(
+ move |alive| {
+ // Create a new device monitor.
+ let mut monitor = Monitor::new(new_device_cb, selector_sender, status_sender);
+
+ // Start polling for new devices.
+ try_or!(monitor.run(alive), |_| callback
+ .call(Err(errors::AuthenticatorError::Platform)));
+
+ // Send an error, if the callback wasn't called already.
+ callback.call(Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::NotAllowed,
+ )));
+ },
+ timeout,
+ )
+ .map_err(|_| errors::AuthenticatorError::Platform)?;
+
+ Ok(Self {
+ thread,
+ device_selector,
+ })
+ }
+
+ pub fn cancel(&mut self) {
+ info!("Transaction was cancelled.");
+ self.device_selector.stop();
+ self.thread.cancel();
+ }
+}
diff --git a/third_party/rust/authenticator/src/transport/windows/winapi.rs b/third_party/rust/authenticator/src/transport/windows/winapi.rs
new file mode 100644
index 0000000000..9ed88138a8
--- /dev/null
+++ b/third_party/rust/authenticator/src/transport/windows/winapi.rs
@@ -0,0 +1,268 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use std::io;
+use std::mem;
+use std::ptr;
+use std::slice;
+
+use std::ffi::OsString;
+use std::os::windows::ffi::OsStringExt;
+
+use crate::util::io_err;
+
+extern crate libc;
+extern crate winapi;
+
+use winapi::shared::{guiddef, minwindef, ntdef, windef};
+use winapi::shared::{hidclass, hidpi, hidusage};
+use winapi::um::{handleapi, setupapi};
+
+#[link(name = "setupapi")]
+extern "system" {
+ fn SetupDiGetClassDevsW(
+ ClassGuid: *const guiddef::GUID,
+ Enumerator: ntdef::PCSTR,
+ hwndParent: windef::HWND,
+ flags: minwindef::DWORD,
+ ) -> setupapi::HDEVINFO;
+
+ fn SetupDiDestroyDeviceInfoList(DeviceInfoSet: setupapi::HDEVINFO) -> minwindef::BOOL;
+
+ fn SetupDiEnumDeviceInterfaces(
+ DeviceInfoSet: setupapi::HDEVINFO,
+ DeviceInfoData: setupapi::PSP_DEVINFO_DATA,
+ InterfaceClassGuid: *const guiddef::GUID,
+ MemberIndex: minwindef::DWORD,
+ DeviceInterfaceData: setupapi::PSP_DEVICE_INTERFACE_DATA,
+ ) -> minwindef::BOOL;
+
+ fn SetupDiGetDeviceInterfaceDetailW(
+ DeviceInfoSet: setupapi::HDEVINFO,
+ DeviceInterfaceData: setupapi::PSP_DEVICE_INTERFACE_DATA,
+ DeviceInterfaceDetailData: setupapi::PSP_DEVICE_INTERFACE_DETAIL_DATA_W,
+ DeviceInterfaceDetailDataSize: minwindef::DWORD,
+ RequiredSize: minwindef::PDWORD,
+ DeviceInfoData: setupapi::PSP_DEVINFO_DATA,
+ ) -> minwindef::BOOL;
+}
+
+#[link(name = "hid")]
+extern "system" {
+ fn HidD_GetPreparsedData(
+ HidDeviceObject: ntdef::HANDLE,
+ PreparsedData: *mut hidpi::PHIDP_PREPARSED_DATA,
+ ) -> ntdef::BOOLEAN;
+
+ fn HidD_FreePreparsedData(PreparsedData: hidpi::PHIDP_PREPARSED_DATA) -> ntdef::BOOLEAN;
+
+ fn HidP_GetCaps(
+ PreparsedData: hidpi::PHIDP_PREPARSED_DATA,
+ Capabilities: hidpi::PHIDP_CAPS,
+ ) -> ntdef::NTSTATUS;
+}
+
+fn from_wide_ptr(ptr: *const u16, len: usize) -> String {
+ assert!(!ptr.is_null() && len % 2 == 0);
+ let slice = unsafe { slice::from_raw_parts(ptr, len / 2) };
+ OsString::from_wide(slice).to_string_lossy().into_owned()
+}
+
+pub struct DeviceInfoSet {
+ set: setupapi::HDEVINFO,
+}
+
+impl DeviceInfoSet {
+ pub fn new() -> io::Result<Self> {
+ let flags = setupapi::DIGCF_PRESENT | setupapi::DIGCF_DEVICEINTERFACE;
+ let set = unsafe {
+ SetupDiGetClassDevsW(
+ &hidclass::GUID_DEVINTERFACE_HID,
+ ptr::null_mut(),
+ ptr::null_mut(),
+ flags,
+ )
+ };
+ if set == handleapi::INVALID_HANDLE_VALUE {
+ return Err(io_err("SetupDiGetClassDevsW failed!"));
+ }
+
+ Ok(Self { set })
+ }
+
+ pub fn get(&self) -> setupapi::HDEVINFO {
+ self.set
+ }
+
+ pub fn devices(&self) -> DeviceInfoSetIter {
+ DeviceInfoSetIter::new(self)
+ }
+}
+
+impl Drop for DeviceInfoSet {
+ fn drop(&mut self) {
+ let _ = unsafe { SetupDiDestroyDeviceInfoList(self.set) };
+ }
+}
+
+pub struct DeviceInfoSetIter<'a> {
+ set: &'a DeviceInfoSet,
+ index: minwindef::DWORD,
+}
+
+impl<'a> DeviceInfoSetIter<'a> {
+ fn new(set: &'a DeviceInfoSet) -> Self {
+ Self { set, index: 0 }
+ }
+}
+
+impl<'a> Iterator for DeviceInfoSetIter<'a> {
+ type Item = String;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let mut device_interface_data =
+ mem::MaybeUninit::<setupapi::SP_DEVICE_INTERFACE_DATA>::zeroed();
+ unsafe {
+ (*device_interface_data.as_mut_ptr()).cbSize =
+ mem::size_of::<setupapi::SP_DEVICE_INTERFACE_DATA>() as minwindef::UINT;
+ }
+
+ let rv = unsafe {
+ SetupDiEnumDeviceInterfaces(
+ self.set.get(),
+ ptr::null_mut(),
+ &hidclass::GUID_DEVINTERFACE_HID,
+ self.index,
+ device_interface_data.as_mut_ptr(),
+ )
+ };
+ if rv == 0 {
+ return None; // We're past the last device index.
+ }
+
+ // Determine the size required to hold a detail struct.
+ let mut required_size = 0;
+ unsafe {
+ SetupDiGetDeviceInterfaceDetailW(
+ self.set.get(),
+ device_interface_data.as_mut_ptr(),
+ ptr::null_mut(),
+ required_size,
+ &mut required_size,
+ ptr::null_mut(),
+ )
+ };
+ if required_size == 0 {
+ return None; // An error occurred.
+ }
+
+ let detail = DeviceInterfaceDetailData::new(required_size as usize);
+ if detail.is_none() {
+ return None; // malloc() failed.
+ }
+
+ let detail = detail.unwrap();
+ let rv = unsafe {
+ SetupDiGetDeviceInterfaceDetailW(
+ self.set.get(),
+ device_interface_data.as_mut_ptr(),
+ detail.get(),
+ required_size,
+ ptr::null_mut(),
+ ptr::null_mut(),
+ )
+ };
+ if rv == 0 {
+ return None; // An error occurred.
+ }
+
+ self.index += 1;
+ Some(detail.path())
+ }
+}
+
+struct DeviceInterfaceDetailData {
+ data: setupapi::PSP_DEVICE_INTERFACE_DETAIL_DATA_W,
+ path_len: usize,
+}
+
+impl DeviceInterfaceDetailData {
+ fn new(size: usize) -> Option<Self> {
+ let mut cb_size = mem::size_of::<setupapi::SP_DEVICE_INTERFACE_DETAIL_DATA_W>();
+ if cfg!(target_pointer_width = "32") {
+ cb_size = 4 + 2; // 4-byte uint + default TCHAR size. size_of is inaccurate.
+ }
+
+ if size < cb_size {
+ warn!("DeviceInterfaceDetailData is too small. {}", size);
+ return None;
+ }
+
+ let data = unsafe { libc::malloc(size) as setupapi::PSP_DEVICE_INTERFACE_DETAIL_DATA_W };
+ if data.is_null() {
+ return None;
+ }
+
+ // Set total size of the structure.
+ unsafe { (*data).cbSize = cb_size as minwindef::UINT };
+
+ // Compute offset of `SP_DEVICE_INTERFACE_DETAIL_DATA_W.DevicePath`.
+ let offset = memoffset::offset_of!(setupapi::SP_DEVICE_INTERFACE_DETAIL_DATA_W, DevicePath);
+
+ Some(Self {
+ data,
+ path_len: size - offset,
+ })
+ }
+
+ fn get(&self) -> setupapi::PSP_DEVICE_INTERFACE_DETAIL_DATA_W {
+ self.data
+ }
+
+ fn path(&self) -> String {
+ unsafe { from_wide_ptr((*self.data).DevicePath.as_ptr(), self.path_len - 2) }
+ }
+}
+
+impl Drop for DeviceInterfaceDetailData {
+ fn drop(&mut self) {
+ unsafe { libc::free(self.data as *mut libc::c_void) };
+ }
+}
+
+pub struct DeviceCapabilities {
+ caps: hidpi::HIDP_CAPS,
+}
+
+impl DeviceCapabilities {
+ pub fn new(handle: ntdef::HANDLE) -> io::Result<Self> {
+ let mut preparsed_data = ptr::null_mut();
+ let rv = unsafe { HidD_GetPreparsedData(handle, &mut preparsed_data) };
+ if rv == 0 || preparsed_data.is_null() {
+ return Err(io_err("HidD_GetPreparsedData failed!"));
+ }
+
+ let mut caps = mem::MaybeUninit::<hidpi::HIDP_CAPS>::uninit();
+ unsafe {
+ let rv = HidP_GetCaps(preparsed_data, caps.as_mut_ptr());
+ HidD_FreePreparsedData(preparsed_data);
+
+ if rv != hidpi::HIDP_STATUS_SUCCESS {
+ return Err(io_err("HidP_GetCaps failed!"));
+ }
+
+ Ok(Self {
+ caps: caps.assume_init(),
+ })
+ }
+ }
+
+ pub fn usage(&self) -> hidusage::USAGE {
+ self.caps.Usage
+ }
+
+ pub fn usage_page(&self) -> hidusage::USAGE {
+ self.caps.UsagePage
+ }
+}
diff --git a/third_party/rust/authenticator/src/u2fhid-capi.h b/third_party/rust/authenticator/src/u2fhid-capi.h
new file mode 100644
index 0000000000..b097cd5217
--- /dev/null
+++ b/third_party/rust/authenticator/src/u2fhid-capi.h
@@ -0,0 +1,119 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __U2FHID_CAPI
+#define __U2FHID_CAPI
+#include <stdlib.h>
+#include "nsString.h"
+
+extern "C" {
+
+const uint8_t U2F_RESBUF_ID_REGISTRATION = 0;
+const uint8_t U2F_RESBUF_ID_KEYHANDLE = 1;
+const uint8_t U2F_RESBUF_ID_SIGNATURE = 2;
+const uint8_t U2F_RESBUF_ID_APPID = 3;
+const uint8_t U2F_RESBUF_ID_VENDOR_NAME = 4;
+const uint8_t U2F_RESBUF_ID_DEVICE_NAME = 5;
+const uint8_t U2F_RESBUF_ID_FIRMWARE_MAJOR = 6;
+const uint8_t U2F_RESBUF_ID_FIRMWARE_MINOR = 7;
+const uint8_t U2F_RESBUF_ID_FIRMWARE_BUILD = 8;
+
+const uint64_t U2F_FLAG_REQUIRE_RESIDENT_KEY = 1;
+const uint64_t U2F_FLAG_REQUIRE_USER_VERIFICATION = 2;
+const uint64_t U2F_FLAG_REQUIRE_PLATFORM_ATTACHMENT = 4;
+
+const uint8_t U2F_AUTHENTICATOR_TRANSPORT_USB = 1;
+const uint8_t U2F_AUTHENTICATOR_TRANSPORT_NFC = 2;
+const uint8_t U2F_AUTHENTICATOR_TRANSPORT_BLE = 4;
+const uint8_t CTAP_AUTHENTICATOR_TRANSPORT_INTERNAL = 8;
+
+const uint8_t U2F_OK = 0;
+const uint8_t U2F_ERROR_UKNOWN = 1;
+const uint8_t U2F_ERROR_NOT_SUPPORTED = 2;
+const uint8_t U2F_ERROR_INVALID_STATE = 3;
+const uint8_t U2F_ERROR_CONSTRAINT = 4;
+const uint8_t U2F_ERROR_NOT_ALLOWED = 5;
+const uint8_t CTAP_ERROR_PIN_REQUIRED = 6;
+const uint8_t CTAP_ERROR_PIN_INVALID = 7;
+const uint8_t CTAP_ERROR_PIN_AUTH_BLOCKED = 8;
+const uint8_t CTAP_ERROR_PIN_BLOCKED = 9;
+
+// NOTE: Preconditions
+// * All rust_u2f_mgr* pointers must refer to pointers which are returned
+// by rust_u2f_mgr_new, and must be freed with rust_u2f_mgr_free.
+// * All rust_u2f_khs* pointers must refer to pointers which are returned
+// by rust_u2f_khs_new, and must be freed with rust_u2f_khs_free.
+// * All rust_u2f_res* pointers must refer to pointers passed to the
+// register() and sign() callbacks. They can be null on failure.
+
+// The `rust_u2f_mgr` opaque type is equivalent to the rust type `U2FManager`
+// TODO(MS): Once CTAP2 support is added, this should probably be renamed.
+struct rust_ctap_manager;
+
+// The `rust_u2f_app_ids` opaque type is equivalent to the rust type `U2FAppIds`
+struct rust_u2f_app_ids;
+
+// The `rust_u2f_key_handles` opaque type is equivalent to the rust type
+// `U2FKeyHandles`
+struct rust_u2f_key_handles;
+
+// The `rust_u2f_res` opaque type is equivalent to the rust type `U2FResult`
+struct rust_u2f_result;
+
+// The callback passed to register() and sign().
+typedef void (*rust_u2f_callback)(uint64_t, rust_u2f_result*);
+
+/// U2FManager functions.
+
+rust_ctap_manager* rust_u2f_mgr_new();
+/* unsafe */ void rust_u2f_mgr_free(rust_ctap_manager* mgr);
+
+uint64_t rust_u2f_mgr_register(rust_ctap_manager* mgr, uint64_t flags,
+ uint64_t timeout, rust_u2f_callback,
+ const uint8_t* challenge_ptr,
+ size_t challenge_len,
+ const uint8_t* application_ptr,
+ size_t application_len,
+ const rust_u2f_key_handles* khs);
+
+uint64_t rust_u2f_mgr_sign(rust_ctap_manager* mgr, uint64_t flags,
+ uint64_t timeout, rust_u2f_callback,
+ const uint8_t* challenge_ptr, size_t challenge_len,
+ const rust_u2f_app_ids* app_ids,
+ const rust_u2f_key_handles* khs);
+
+void rust_u2f_mgr_cancel(rust_ctap_manager* mgr);
+
+/// U2FAppIds functions.
+
+rust_u2f_app_ids* rust_u2f_app_ids_new();
+void rust_u2f_app_ids_add(rust_u2f_app_ids* ids, const uint8_t* id,
+ size_t id_len);
+/* unsafe */ void rust_u2f_app_ids_free(rust_u2f_app_ids* ids);
+
+/// U2FKeyHandles functions.
+
+rust_u2f_key_handles* rust_u2f_khs_new();
+void rust_u2f_khs_add(rust_u2f_key_handles* khs, const uint8_t* key_handle,
+ size_t key_handle_len, uint8_t transports);
+/* unsafe */ void rust_u2f_khs_free(rust_u2f_key_handles* khs);
+
+/// U2FResult functions.
+
+// Returns 0 for success, or the U2F_ERROR error code >= 1.
+uint8_t rust_u2f_result_error(const rust_u2f_result* res);
+
+// Call this before `[..]_copy()` to allocate enough space.
+bool rust_u2f_resbuf_length(const rust_u2f_result* res, uint8_t bid,
+ size_t* len);
+bool rust_u2f_resbuf_contains(const rust_u2f_result* res, uint8_t bid);
+bool rust_u2f_resbuf_copy(const rust_u2f_result* res, uint8_t bid,
+ uint8_t* dst);
+/* unsafe */ void rust_u2f_res_free(rust_u2f_result* res);
+
+}
+
+#endif // __U2FHID_CAPI
diff --git a/third_party/rust/authenticator/src/u2fprotocol.rs b/third_party/rust/authenticator/src/u2fprotocol.rs
new file mode 100644
index 0000000000..08d7844351
--- /dev/null
+++ b/third_party/rust/authenticator/src/u2fprotocol.rs
@@ -0,0 +1,398 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#![cfg_attr(feature = "cargo-clippy", allow(clippy::needless_lifetimes))]
+
+extern crate std;
+
+use rand::{thread_rng, RngCore};
+use std::ffi::CString;
+use std::io;
+use std::io::{Read, Write};
+
+use crate::consts::*;
+use crate::u2ftypes::*;
+use crate::util::io_err;
+
+////////////////////////////////////////////////////////////////////////
+// Device Commands
+////////////////////////////////////////////////////////////////////////
+
+pub fn u2f_init_device<T>(dev: &mut T) -> bool
+where
+ T: U2FDevice + Read + Write,
+{
+ let mut nonce = [0u8; 8];
+ thread_rng().fill_bytes(&mut nonce);
+
+ // Initialize the device and check its version.
+ init_device(dev, &nonce).is_ok() && is_v2_device(dev).unwrap_or(false)
+}
+
+pub fn u2f_register<T>(dev: &mut T, challenge: &[u8], application: &[u8]) -> io::Result<Vec<u8>>
+where
+ T: U2FDevice + Read + Write,
+{
+ if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "Invalid parameter sizes",
+ ));
+ }
+
+ let mut register_data = Vec::with_capacity(2 * PARAMETER_SIZE);
+ register_data.extend(challenge);
+ register_data.extend(application);
+
+ let flags = U2F_REQUEST_USER_PRESENCE;
+ let (resp, status) = send_ctap1(dev, U2F_REGISTER, flags, &register_data)?;
+ status_word_to_result(status, resp)
+}
+
+pub fn u2f_sign<T>(
+ dev: &mut T,
+ challenge: &[u8],
+ application: &[u8],
+ key_handle: &[u8],
+) -> io::Result<Vec<u8>>
+where
+ T: U2FDevice + Read + Write,
+{
+ if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "Invalid parameter sizes",
+ ));
+ }
+
+ if key_handle.len() > 256 {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "Key handle too large",
+ ));
+ }
+
+ let mut sign_data = Vec::with_capacity(2 * PARAMETER_SIZE + 1 + key_handle.len());
+ sign_data.extend(challenge);
+ sign_data.extend(application);
+ sign_data.push(key_handle.len() as u8);
+ sign_data.extend(key_handle);
+
+ let flags = U2F_REQUEST_USER_PRESENCE;
+ let (resp, status) = send_ctap1(dev, U2F_AUTHENTICATE, flags, &sign_data)?;
+ status_word_to_result(status, resp)
+}
+
+pub fn u2f_is_keyhandle_valid<T>(
+ dev: &mut T,
+ challenge: &[u8],
+ application: &[u8],
+ key_handle: &[u8],
+) -> io::Result<bool>
+where
+ T: U2FDevice + Read + Write,
+{
+ if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "Invalid parameter sizes",
+ ));
+ }
+
+ if key_handle.len() > 256 {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "Key handle too large",
+ ));
+ }
+
+ let mut sign_data = Vec::with_capacity(2 * PARAMETER_SIZE + 1 + key_handle.len());
+ sign_data.extend(challenge);
+ sign_data.extend(application);
+ sign_data.push(key_handle.len() as u8);
+ sign_data.extend(key_handle);
+
+ let flags = U2F_CHECK_IS_REGISTERED;
+ let (_, status) = send_ctap1(dev, U2F_AUTHENTICATE, flags, &sign_data)?;
+ Ok(status == SW_CONDITIONS_NOT_SATISFIED)
+}
+
+////////////////////////////////////////////////////////////////////////
+// Internal Device Commands
+////////////////////////////////////////////////////////////////////////
+
+fn init_device<T>(dev: &mut T, nonce: &[u8]) -> io::Result<()>
+where
+ T: U2FDevice + Read + Write,
+{
+ assert_eq!(nonce.len(), INIT_NONCE_SIZE);
+ // Send Init to broadcast address to create a new channel
+ let raw = sendrecv(dev, HIDCmd::Init, nonce)?;
+ let rsp = U2FHIDInitResp::read(&raw, nonce)?;
+ // Get the new Channel ID
+ dev.set_cid(rsp.cid);
+
+ let vendor = dev
+ .get_property("Manufacturer")
+ .unwrap_or_else(|_| String::from("Unknown Vendor"));
+ let product = dev
+ .get_property("Product")
+ .unwrap_or_else(|_| String::from("Unknown Device"));
+
+ dev.set_device_info(U2FDeviceInfo {
+ vendor_name: vendor.as_bytes().to_vec(),
+ device_name: product.as_bytes().to_vec(),
+ version_interface: rsp.version_interface,
+ version_major: rsp.version_major,
+ version_minor: rsp.version_minor,
+ version_build: rsp.version_build,
+ cap_flags: rsp.cap_flags,
+ });
+
+ Ok(())
+}
+
+fn is_v2_device<T>(dev: &mut T) -> io::Result<bool>
+where
+ T: U2FDevice + Read + Write,
+{
+ let (data, status) = send_ctap1(dev, U2F_VERSION, 0x00, &[])?;
+ let actual = CString::new(data)?;
+ let expected = CString::new("U2F_V2")?;
+ status_word_to_result(status, actual == expected)
+}
+
+////////////////////////////////////////////////////////////////////////
+// Error Handling
+////////////////////////////////////////////////////////////////////////
+
+fn status_word_to_result<T>(status: [u8; 2], val: T) -> io::Result<T> {
+ use self::io::ErrorKind::{InvalidData, InvalidInput};
+
+ match status {
+ SW_NO_ERROR => Ok(val),
+ SW_WRONG_DATA => Err(io::Error::new(InvalidData, "wrong data")),
+ SW_WRONG_LENGTH => Err(io::Error::new(InvalidInput, "wrong length")),
+ SW_CONDITIONS_NOT_SATISFIED => Err(io_err("conditions not satisfied")),
+ _ => Err(io_err(&format!("failed with status {:?}", status))),
+ }
+}
+
+////////////////////////////////////////////////////////////////////////
+// Device Communication Functions
+////////////////////////////////////////////////////////////////////////
+
+pub fn sendrecv<T>(dev: &mut T, cmd: HIDCmd, send: &[u8]) -> io::Result<Vec<u8>>
+where
+ T: U2FDevice + Read + Write,
+{
+ // Send initialization packet.
+ let mut count = U2FHIDInit::write(dev, cmd.into(), send)?;
+
+ // Send continuation packets.
+ let mut sequence = 0u8;
+ while count < send.len() {
+ count += U2FHIDCont::write(dev, sequence, &send[count..])?;
+ sequence += 1;
+ }
+
+ // Now we read. This happens in 2 chunks: The initial packet, which has the
+ // size we expect overall, then continuation packets, which will fill in
+ // data until we have everything.
+ let (_, mut data) = U2FHIDInit::read(dev)?;
+
+ let mut sequence = 0u8;
+ while data.len() < data.capacity() {
+ let max = data.capacity() - data.len();
+ data.extend_from_slice(&U2FHIDCont::read(dev, sequence, max)?);
+ sequence += 1;
+ }
+
+ Ok(data)
+}
+
+fn send_ctap1<T>(dev: &mut T, cmd: u8, p1: u8, send: &[u8]) -> io::Result<(Vec<u8>, [u8; 2])>
+where
+ T: U2FDevice + Read + Write,
+{
+ let apdu = CTAP1RequestAPDU::serialize(cmd, p1, send)?;
+ let mut data = sendrecv(dev, HIDCmd::Msg, &apdu)?;
+
+ if data.len() < 2 {
+ return Err(io_err("unexpected response"));
+ }
+
+ let split_at = data.len() - 2;
+ let status = data.split_off(split_at);
+ Ok((data, [status[0], status[1]]))
+}
+
+////////////////////////////////////////////////////////////////////////
+// Tests
+////////////////////////////////////////////////////////////////////////
+
+#[cfg(test)]
+pub(crate) mod tests {
+ use super::{init_device, is_v2_device, send_ctap1, sendrecv, U2FDevice};
+ use crate::consts::{Capability, HIDCmd, CID_BROADCAST, SW_NO_ERROR};
+ use crate::transport::device_selector::Device;
+ use crate::transport::hid::HIDDevice;
+ use crate::u2ftypes::U2FDeviceInfo;
+ use rand::{thread_rng, RngCore};
+
+ #[test]
+ fn test_init_device() {
+ let mut device = Device::new("u2fprotocol").unwrap();
+ let nonce = vec![0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01];
+
+ // channel id
+ let mut cid = [0u8; 4];
+ thread_rng().fill_bytes(&mut cid);
+
+ // init packet
+ let mut msg = CID_BROADCAST.to_vec();
+ msg.extend(vec![HIDCmd::Init.into(), 0x00, 0x08]); // cmd + bcnt
+ msg.extend_from_slice(&nonce);
+ device.add_write(&msg, 0);
+
+ // init_resp packet
+ let mut msg = CID_BROADCAST.to_vec();
+ msg.extend(vec![HIDCmd::Init.into(), 0x00, 0x11]); // cmd + bcnt
+ msg.extend_from_slice(&nonce);
+ msg.extend_from_slice(&cid); // new channel id
+ msg.extend(vec![0x02, 0x04, 0x01, 0x08, 0x01]); // versions + flags
+ device.add_read(&msg, 0);
+
+ init_device(&mut device, &nonce).unwrap();
+ assert_eq!(device.get_cid(), &cid);
+
+ let dev_info = device.get_device_info();
+ assert_eq!(dev_info.version_interface, 0x02);
+ assert_eq!(dev_info.version_major, 0x04);
+ assert_eq!(dev_info.version_minor, 0x01);
+ assert_eq!(dev_info.version_build, 0x08);
+ assert_eq!(dev_info.cap_flags, Capability::WINK); // 0x01
+ }
+
+ #[test]
+ fn test_get_version() {
+ let mut device = Device::new("u2fprotocol").unwrap();
+ // channel id
+ let mut cid = [0u8; 4];
+ thread_rng().fill_bytes(&mut cid);
+
+ device.set_cid(cid.clone());
+
+ let info = U2FDeviceInfo {
+ vendor_name: Vec::new(),
+ device_name: Vec::new(),
+ version_interface: 0x02,
+ version_major: 0x04,
+ version_minor: 0x01,
+ version_build: 0x08,
+ cap_flags: Capability::WINK,
+ };
+ device.set_device_info(info);
+
+ // ctap1.0 U2F_VERSION request
+ let mut msg = cid.to_vec();
+ msg.extend(&[HIDCmd::Msg.into(), 0x0, 0x7]); // cmd + bcnt
+ msg.extend(&[0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0]);
+ device.add_write(&msg, 0);
+
+ // fido response
+ let mut msg = cid.to_vec();
+ msg.extend(&[HIDCmd::Msg.into(), 0x0, 0x08]); // cmd + bcnt
+ msg.extend(&[0x55, 0x32, 0x46, 0x5f, 0x56, 0x32]); // 'U2F_V2'
+ msg.extend(&SW_NO_ERROR);
+ device.add_read(&msg, 0);
+
+ let res = is_v2_device(&mut device).expect("Failed to get version");
+ assert!(res);
+ }
+
+ #[test]
+ fn test_sendrecv_multiple() {
+ let mut device = Device::new("u2fprotocol").unwrap();
+ let cid = [0x01, 0x02, 0x03, 0x04];
+ device.set_cid(cid);
+
+ // init packet
+ let mut msg = cid.to_vec();
+ msg.extend(vec![HIDCmd::Ping.into(), 0x00, 0xe4]); // cmd + length = 228
+ // write msg, append [1u8; 57], 171 bytes remain
+ device.add_write(&msg, 1);
+ device.add_read(&msg, 1);
+
+ // cont packet
+ let mut msg = cid.to_vec();
+ msg.push(0x00); // seq = 0
+ // write msg, append [1u8; 59], 112 bytes remaining
+ device.add_write(&msg, 1);
+ device.add_read(&msg, 1);
+
+ // cont packet
+ let mut msg = cid.to_vec();
+ msg.push(0x01); // seq = 1
+ // write msg, append [1u8; 59], 53 bytes remaining
+ device.add_write(&msg, 1);
+ device.add_read(&msg, 1);
+
+ // cont packet
+ let mut msg = cid.to_vec();
+ msg.push(0x02); // seq = 2
+ msg.extend_from_slice(&[1u8; 53]);
+ // write msg, append remaining 53 bytes.
+ device.add_write(&msg, 0);
+ device.add_read(&msg, 0);
+
+ let data = [1u8; 228];
+ let d = sendrecv(&mut device, HIDCmd::Ping, &data).unwrap();
+ assert_eq!(d.len(), 228);
+ assert_eq!(d, &data[..]);
+ }
+
+ #[test]
+ fn test_sendapdu() {
+ let cid = [0x01, 0x02, 0x03, 0x04];
+ let data = [0x01, 0x02, 0x03, 0x04, 0x05];
+ let mut device = Device::new("u2fprotocol").unwrap();
+ device.set_cid(cid);
+
+ let mut msg = cid.to_vec();
+ // sendrecv header
+ msg.extend(vec![HIDCmd::Msg.into(), 0x00, 0x0e]); // len = 14
+ // apdu header
+ msg.extend(vec![
+ 0x00,
+ HIDCmd::Ping.into(),
+ 0xaa,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x05,
+ ]);
+ // apdu data
+ msg.extend_from_slice(&data);
+ device.add_write(&msg, 0);
+
+ // Send data back
+ let mut msg = cid.to_vec();
+ msg.extend(vec![HIDCmd::Msg.into(), 0x00, 0x07]);
+ msg.extend_from_slice(&data);
+ msg.extend_from_slice(&SW_NO_ERROR);
+ device.add_read(&msg, 0);
+
+ let (result, status) = send_ctap1(&mut device, HIDCmd::Ping.into(), 0xaa, &data).unwrap();
+ assert_eq!(result, &data);
+ assert_eq!(status, SW_NO_ERROR);
+ }
+
+ #[test]
+ fn test_get_property() {
+ let device = Device::new("u2fprotocol").unwrap();
+
+ assert_eq!(device.get_property("a").unwrap(), "a not implemented");
+ }
+}
diff --git a/third_party/rust/authenticator/src/u2ftypes.rs b/third_party/rust/authenticator/src/u2ftypes.rs
new file mode 100644
index 0000000000..782c698c17
--- /dev/null
+++ b/third_party/rust/authenticator/src/u2ftypes.rs
@@ -0,0 +1,363 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::consts::*;
+use crate::util::io_err;
+use serde::Serialize;
+use std::{cmp, fmt, io, str};
+
+pub fn to_hex(data: &[u8], joiner: &str) -> String {
+ let parts: Vec<String> = data.iter().map(|byte| format!("{:02x}", byte)).collect();
+ parts.join(joiner)
+}
+
+pub fn trace_hex(data: &[u8]) {
+ if log_enabled!(log::Level::Trace) {
+ trace!("USB send: {}", to_hex(data, ""));
+ }
+}
+
+// Trait for representing U2F HID Devices. Requires getters/setters for the
+// channel ID, created during device initialization.
+pub trait U2FDevice {
+ fn get_cid(&self) -> &[u8; 4];
+ fn set_cid(&mut self, cid: [u8; 4]);
+
+ fn in_rpt_size(&self) -> usize;
+ fn in_init_data_size(&self) -> usize {
+ self.in_rpt_size() - INIT_HEADER_SIZE
+ }
+ fn in_cont_data_size(&self) -> usize {
+ self.in_rpt_size() - CONT_HEADER_SIZE
+ }
+
+ fn out_rpt_size(&self) -> usize;
+ fn out_init_data_size(&self) -> usize {
+ self.out_rpt_size() - INIT_HEADER_SIZE
+ }
+ fn out_cont_data_size(&self) -> usize {
+ self.out_rpt_size() - CONT_HEADER_SIZE
+ }
+
+ fn get_property(&self, prop_name: &str) -> io::Result<String>;
+ fn get_device_info(&self) -> U2FDeviceInfo;
+ fn set_device_info(&mut self, dev_info: U2FDeviceInfo);
+}
+
+// Init structure for U2F Communications. Tells the receiver what channel
+// communication is happening on, what command is running, and how much data to
+// expect to receive over all.
+//
+// Spec at https://fidoalliance.org/specs/fido-u2f-v1.
+// 0-nfc-bt-amendment-20150514/fido-u2f-hid-protocol.html#message--and-packet-structure
+pub struct U2FHIDInit {}
+
+impl U2FHIDInit {
+ pub fn read<T>(dev: &mut T) -> io::Result<(HIDCmd, Vec<u8>)>
+ where
+ T: U2FDevice + io::Read,
+ {
+ let mut frame = vec![0u8; dev.in_rpt_size()];
+ let mut count = dev.read(&mut frame)?;
+
+ while dev.get_cid() != &frame[..4] {
+ count = dev.read(&mut frame)?;
+ }
+
+ if count != dev.in_rpt_size() {
+ return Err(io_err("invalid init packet"));
+ }
+
+ let cmd = HIDCmd::from(frame[4] | TYPE_INIT);
+
+ let cap = (frame[5] as usize) << 8 | (frame[6] as usize);
+ let mut data = Vec::with_capacity(cap);
+
+ let len = cmp::min(cap, dev.in_init_data_size());
+ data.extend_from_slice(&frame[7..7 + len]);
+
+ Ok((cmd, data))
+ }
+
+ pub fn write<T>(dev: &mut T, cmd: u8, data: &[u8]) -> io::Result<usize>
+ where
+ T: U2FDevice + io::Write,
+ {
+ if data.len() > 0xffff {
+ return Err(io_err("payload length > 2^16"));
+ }
+
+ let mut frame = vec![0u8; dev.out_rpt_size() + 1];
+ frame[1..5].copy_from_slice(dev.get_cid());
+ frame[5] = cmd;
+ frame[6] = (data.len() >> 8) as u8;
+ frame[7] = data.len() as u8;
+
+ let count = cmp::min(data.len(), dev.out_init_data_size());
+ frame[8..8 + count].copy_from_slice(&data[..count]);
+ trace_hex(&frame);
+
+ if dev.write(&frame)? != frame.len() {
+ return Err(io_err("device write failed"));
+ }
+
+ Ok(count)
+ }
+}
+
+// Continuation structure for U2F Communications. After an Init structure is
+// sent, continuation structures are used to transmit all extra data that
+// wouldn't fit in the initial packet. The sequence number increases with every
+// packet, until all data is received.
+//
+// https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-hid-protocol.
+// html#message--and-packet-structure
+pub struct U2FHIDCont {}
+
+impl U2FHIDCont {
+ pub fn read<T>(dev: &mut T, seq: u8, max: usize) -> io::Result<Vec<u8>>
+ where
+ T: U2FDevice + io::Read,
+ {
+ let mut frame = vec![0u8; dev.in_rpt_size()];
+ let mut count = dev.read(&mut frame)?;
+
+ while dev.get_cid() != &frame[..4] {
+ count = dev.read(&mut frame)?;
+ }
+
+ if count != dev.in_rpt_size() {
+ return Err(io_err("invalid cont packet"));
+ }
+
+ if seq != frame[4] {
+ return Err(io_err("invalid sequence number"));
+ }
+
+ let max = cmp::min(max, dev.in_cont_data_size());
+ Ok(frame[5..5 + max].to_vec())
+ }
+
+ pub fn write<T>(dev: &mut T, seq: u8, data: &[u8]) -> io::Result<usize>
+ where
+ T: U2FDevice + io::Write,
+ {
+ let mut frame = vec![0u8; dev.out_rpt_size() + 1];
+ frame[1..5].copy_from_slice(dev.get_cid());
+ frame[5] = seq;
+
+ let count = cmp::min(data.len(), dev.out_cont_data_size());
+ frame[6..6 + count].copy_from_slice(&data[..count]);
+ trace_hex(&frame);
+
+ if dev.write(&frame)? != frame.len() {
+ return Err(io_err("device write failed"));
+ }
+
+ Ok(count)
+ }
+}
+
+// Reply sent after initialization command. Contains information about U2F USB
+// Key versioning, as well as the communication channel to be used for all
+// further requests.
+//
+// https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-hid-protocol.
+// html#u2fhid_init
+pub struct U2FHIDInitResp {
+ pub cid: [u8; 4],
+ pub version_interface: u8,
+ pub version_major: u8,
+ pub version_minor: u8,
+ pub version_build: u8,
+ pub cap_flags: Capability,
+}
+
+impl U2FHIDInitResp {
+ pub fn read(data: &[u8], nonce: &[u8]) -> io::Result<U2FHIDInitResp> {
+ assert_eq!(nonce.len(), INIT_NONCE_SIZE);
+
+ if data.len() != INIT_NONCE_SIZE + 9 {
+ return Err(io_err("invalid init response"));
+ }
+
+ if nonce != &data[..INIT_NONCE_SIZE] {
+ return Err(io_err("invalid nonce"));
+ }
+
+ let rsp = U2FHIDInitResp {
+ cid: [
+ data[INIT_NONCE_SIZE],
+ data[INIT_NONCE_SIZE + 1],
+ data[INIT_NONCE_SIZE + 2],
+ data[INIT_NONCE_SIZE + 3],
+ ],
+ version_interface: data[INIT_NONCE_SIZE + 4],
+ version_major: data[INIT_NONCE_SIZE + 5],
+ version_minor: data[INIT_NONCE_SIZE + 6],
+ version_build: data[INIT_NONCE_SIZE + 7],
+ cap_flags: Capability::from_bits_truncate(data[INIT_NONCE_SIZE + 8]),
+ };
+
+ Ok(rsp)
+ }
+}
+
+/// CTAP1 (FIDO v1.x / U2F / "APDU-like") request framing format, used for
+/// communication with authenticators over *all* transports.
+///
+/// This implementation follows the [FIDO v1.2 spec][fido12rawf], but only
+/// implements extended APDUs (supported by USB HID, NFC and BLE transports).
+///
+/// # Technical details
+///
+/// FIDO v1.0 U2F framing [claims][fido10rawf] to be based on
+/// [ISO/IEC 7816-4:2005][iso7816] (smart card) APDUs, but has several
+/// differences, errors and omissions which make it incompatible.
+///
+/// FIDO v1.1 and v1.2 fixed *most* of these issues, but as a result is *not*
+/// fully compatible with the FIDO v1.0 specification:
+///
+/// * FIDO v1.0 *only* defines extended APDUs, though
+/// [v1.0 NFC implementors][fido10nfc] need to also handle short APDUs.
+///
+/// FIDO v1.1 and later define *both* short and extended APDUs, but defers to
+/// transport-level guidance about which to use (where extended APDU support
+/// is mandatory for all transports, and short APDU support is only mandatory
+/// for NFC transports).
+///
+/// * FIDO v1.0 doesn't special-case N<sub>c</sub> (command data length) = 0
+/// (ie: L<sub>c</sub> is *always* present).
+///
+/// * FIDO v1.0 declares extended L<sub>c</sub> as a 24-bit integer, rather than
+/// 16-bit with padding byte.
+///
+/// * FIDO v1.0 omits L<sub>e</sub> bytes entirely,
+/// [except for short APDUs over NFC][fido10nfc].
+///
+/// Unfortunately, FIDO v2.x gives ambiguous compatibility guidance:
+///
+/// * [The FIDO v2.0 spec describes framing][fido20u2f] in
+/// [FIDO v1.0 U2F Raw Message Format][fido10rawf], [cites][fido20u2fcite] the
+/// FIDO v1.0 format by *name*, but actually links to the
+/// [FIDO v1.2 format][fido12rawf].
+///
+/// * [The FIDO v2.1 spec also describes framing][fido21u2f] in
+/// [FIDO v1.0 U2F Raw Message Format][fido10rawf], but [cites][fido21u2fcite]
+/// the [FIDO v1.2 U2F Raw Message Format][fido12rawf] as a reference by name
+/// and URL.
+///
+/// [fido10nfc]: https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-nfc-protocol.html#framing
+/// [fido10raw]: https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-raw-message-formats.html
+/// [fido10rawf]: https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-raw-message-formats.html#u2f-message-framing
+/// [fido12rawf]: https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#u2f-message-framing
+/// [fido20u2f]: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#u2f-framing
+/// [fido20u2fcite]: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#biblio-u2frawmsgs
+/// [fido21u2f]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#u2f-framing
+/// [fido21u2fcite]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#biblio-u2frawmsgs
+/// [iso7816]: https://www.iso.org/standard/36134.html
+pub struct CTAP1RequestAPDU {}
+
+impl CTAP1RequestAPDU {
+ /// Serializes a CTAP command into
+ /// [FIDO v1.2 U2F Raw Message Format][fido12raw]. See
+ /// [the struct documentation][Self] for implementation notes.
+ ///
+ /// # Arguments
+ ///
+ /// * `ins`: U2F command code, as documented in
+ /// [FIDO v1.2 U2F Raw Format][fido12cmd].
+ /// * `p1`: Command parameter 1 / control byte.
+ /// * `data`: Request data, as documented in
+ /// [FIDO v1.2 Raw Message Formats][fido12raw], of up to 65535 bytes.
+ ///
+ /// [fido12cmd]: https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#command-and-parameter-values
+ /// [fido12raw]: https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html
+ pub fn serialize(ins: u8, p1: u8, data: &[u8]) -> io::Result<Vec<u8>> {
+ if data.len() > 0xffff {
+ return Err(io_err("payload length > 2^16"));
+ }
+ // Size of header + data.
+ let data_size = if data.is_empty() { 0 } else { 2 + data.len() };
+ let mut bytes = vec![0u8; U2FAPDUHEADER_SIZE + data_size];
+
+ // bytes[0] (CLA): Always 0 in FIDO v1.x.
+ bytes[1] = ins;
+ bytes[2] = p1;
+ // bytes[3] (P2): Always 0 in FIDO v1.x.
+
+ // bytes[4] (Lc1/Le1): Always 0 for extended APDUs.
+ if !data.is_empty() {
+ bytes[5] = (data.len() >> 8) as u8; // Lc2
+ bytes[6] = data.len() as u8; // Lc3
+
+ bytes[7..7 + data.len()].copy_from_slice(data);
+ }
+
+ // Last two bytes (Le): Always 0 for Ne = 65536
+ Ok(bytes)
+ }
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct U2FDeviceInfo {
+ pub vendor_name: Vec<u8>,
+ pub device_name: Vec<u8>,
+ pub version_interface: u8,
+ pub version_major: u8,
+ pub version_minor: u8,
+ pub version_build: u8,
+ pub cap_flags: Capability,
+}
+
+impl fmt::Display for U2FDeviceInfo {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "Vendor: {}, Device: {}, Interface: {}, Firmware: v{}.{}.{}, Capabilities: {}",
+ str::from_utf8(&self.vendor_name).unwrap(),
+ str::from_utf8(&self.device_name).unwrap(),
+ &self.version_interface,
+ &self.version_major,
+ &self.version_minor,
+ &self.version_build,
+ to_hex(&[self.cap_flags.bits()], ":"),
+ )
+ }
+}
+
+////////////////////////////////////////////////////////////////////////
+// Tests
+////////////////////////////////////////////////////////////////////////
+
+#[cfg(test)]
+pub(crate) mod tests {
+ use super::CTAP1RequestAPDU;
+
+ #[test]
+ fn test_ctap1_serialize() {
+ // Command with no data, Lc should be omitted.
+ assert_eq!(
+ vec![0, 1, 2, 0, 0, 0, 0],
+ CTAP1RequestAPDU::serialize(1, 2, &[]).unwrap()
+ );
+
+ // Command with data, Lc should be included.
+ assert_eq!(
+ vec![0, 1, 2, 0, 0, 0, 1, 42, 0, 0],
+ CTAP1RequestAPDU::serialize(1, 2, &[42]).unwrap()
+ );
+
+ // Command with 300 bytes data, longer Lc.
+ let d = [0xFF; 300];
+ let mut expected = vec![0, 1, 2, 0, 0, 0x1, 0x2c];
+ expected.extend_from_slice(&d);
+ expected.extend_from_slice(&[0, 0]); // Lc
+ assert_eq!(expected, CTAP1RequestAPDU::serialize(1, 2, &d).unwrap());
+
+ // Command with 64k of data should error
+ let big = [0xFF; 65536];
+ assert!(CTAP1RequestAPDU::serialize(1, 2, &big).is_err());
+ }
+}
diff --git a/third_party/rust/authenticator/src/util.rs b/third_party/rust/authenticator/src/util.rs
new file mode 100644
index 0000000000..221d718e3b
--- /dev/null
+++ b/third_party/rust/authenticator/src/util.rs
@@ -0,0 +1,75 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate libc;
+
+use std::io;
+
+macro_rules! try_or {
+ ($val:expr, $or:expr) => {
+ match $val {
+ Ok(v) => v,
+ Err(e) => {
+ return $or(e);
+ }
+ }
+ };
+}
+
+pub trait Signed {
+ fn is_negative(&self) -> bool;
+}
+
+impl Signed for i32 {
+ fn is_negative(&self) -> bool {
+ *self < 0
+ }
+}
+
+impl Signed for usize {
+ fn is_negative(&self) -> bool {
+ (*self as isize) < 0
+ }
+}
+
+#[cfg(any(target_os = "linux"))]
+pub fn from_unix_result<T: Signed>(rv: T) -> io::Result<T> {
+ if rv.is_negative() {
+ let errno = unsafe { *libc::__errno_location() };
+ Err(io::Error::from_raw_os_error(errno))
+ } else {
+ Ok(rv)
+ }
+}
+
+#[cfg(any(target_os = "freebsd"))]
+pub fn from_unix_result<T: Signed>(rv: T) -> io::Result<T> {
+ if rv.is_negative() {
+ let errno = unsafe { *libc::__error() };
+ Err(io::Error::from_raw_os_error(errno))
+ } else {
+ Ok(rv)
+ }
+}
+
+#[cfg(any(target_os = "openbsd"))]
+pub fn from_unix_result<T: Signed>(rv: T) -> io::Result<T> {
+ if rv.is_negative() {
+ Err(io::Error::last_os_error())
+ } else {
+ Ok(rv)
+ }
+}
+
+pub fn io_err(msg: &str) -> io::Error {
+ io::Error::new(io::ErrorKind::Other, msg)
+}
+
+#[cfg(test)]
+pub fn decode_hex(s: &str) -> Vec<u8> {
+ (0..s.len())
+ .step_by(2)
+ .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
+ .collect()
+}
diff --git a/third_party/rust/authenticator/src/virtualdevices/mod.rs b/third_party/rust/authenticator/src/virtualdevices/mod.rs
new file mode 100644
index 0000000000..5c0a9d39fc
--- /dev/null
+++ b/third_party/rust/authenticator/src/virtualdevices/mod.rs
@@ -0,0 +1,8 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#[cfg(feature = "webdriver")]
+pub mod webdriver;
+
+pub mod software_u2f;
diff --git a/third_party/rust/authenticator/src/virtualdevices/software_u2f.rs b/third_party/rust/authenticator/src/virtualdevices/software_u2f.rs
new file mode 100644
index 0000000000..cda4ca82fc
--- /dev/null
+++ b/third_party/rust/authenticator/src/virtualdevices/software_u2f.rs
@@ -0,0 +1,65 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+use crate::consts::Capability;
+use crate::{RegisterResult, SignResult};
+
+pub struct SoftwareU2FToken {}
+
+// This is simply for platforms that aren't using the U2F Token, usually for builds
+// without --feature webdriver
+#[allow(dead_code)]
+
+impl SoftwareU2FToken {
+ pub fn new() -> SoftwareU2FToken {
+ Self {}
+ }
+
+ pub fn register(
+ &self,
+ _flags: crate::RegisterFlags,
+ _timeout: u64,
+ _challenge: Vec<u8>,
+ _application: crate::AppId,
+ _key_handles: Vec<crate::KeyHandle>,
+ ) -> crate::Result<crate::RegisterResult> {
+ Ok(RegisterResult::CTAP1(vec![0u8; 16], self.dev_info()))
+ }
+
+ /// The implementation of this method must return quickly and should
+ /// report its status via the status and callback methods
+ pub fn sign(
+ &self,
+ _flags: crate::SignFlags,
+ _timeout: u64,
+ _challenge: Vec<u8>,
+ _app_ids: Vec<crate::AppId>,
+ _key_handles: Vec<crate::KeyHandle>,
+ ) -> crate::Result<crate::SignResult> {
+ Ok(SignResult::CTAP1(
+ vec![0u8; 0],
+ vec![0u8; 0],
+ vec![0u8; 0],
+ self.dev_info(),
+ ))
+ }
+
+ pub fn dev_info(&self) -> crate::u2ftypes::U2FDeviceInfo {
+ crate::u2ftypes::U2FDeviceInfo {
+ vendor_name: b"Mozilla".to_vec(),
+ device_name: b"Authenticator Webdriver Token".to_vec(),
+ version_interface: 0,
+ version_major: 1,
+ version_minor: 2,
+ version_build: 3,
+ cap_flags: Capability::empty(),
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////
+// Tests
+////////////////////////////////////////////////////////////////////////
+
+#[cfg(test)]
+mod tests {}
diff --git a/third_party/rust/authenticator/src/virtualdevices/webdriver/mod.rs b/third_party/rust/authenticator/src/virtualdevices/webdriver/mod.rs
new file mode 100644
index 0000000000..b1ef27d813
--- /dev/null
+++ b/third_party/rust/authenticator/src/virtualdevices/webdriver/mod.rs
@@ -0,0 +1,9 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+mod testtoken;
+mod virtualmanager;
+mod web_api;
+
+pub use virtualmanager::VirtualManager;
diff --git a/third_party/rust/authenticator/src/virtualdevices/webdriver/testtoken.rs b/third_party/rust/authenticator/src/virtualdevices/webdriver/testtoken.rs
new file mode 100644
index 0000000000..9bf60bbaf5
--- /dev/null
+++ b/third_party/rust/authenticator/src/virtualdevices/webdriver/testtoken.rs
@@ -0,0 +1,140 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::errors;
+use crate::virtualdevices::software_u2f::SoftwareU2FToken;
+use crate::{RegisterFlags, RegisterResult, SignFlags, SignResult};
+
+pub enum TestWireProtocol {
+ CTAP1,
+ CTAP2,
+}
+
+impl TestWireProtocol {
+ pub fn to_webdriver_string(&self) -> String {
+ match self {
+ TestWireProtocol::CTAP1 => "ctap1/u2f".to_string(),
+ TestWireProtocol::CTAP2 => "ctap2".to_string(),
+ }
+ }
+}
+
+pub struct TestTokenCredential {
+ pub credential: Vec<u8>,
+ pub privkey: Vec<u8>,
+ pub user_handle: Vec<u8>,
+ pub sign_count: u64,
+ pub is_resident_credential: bool,
+ pub rp_id: String,
+}
+
+pub struct TestToken {
+ pub id: u64,
+ pub protocol: TestWireProtocol,
+ pub transport: String,
+ pub is_user_consenting: bool,
+ pub has_user_verification: bool,
+ pub is_user_verified: bool,
+ pub has_resident_key: bool,
+ pub u2f_impl: Option<SoftwareU2FToken>,
+ pub credentials: Vec<TestTokenCredential>,
+}
+
+impl TestToken {
+ pub fn new(
+ id: u64,
+ protocol: TestWireProtocol,
+ transport: String,
+ is_user_consenting: bool,
+ has_user_verification: bool,
+ is_user_verified: bool,
+ has_resident_key: bool,
+ ) -> TestToken {
+ match protocol {
+ TestWireProtocol::CTAP1 => Self {
+ id,
+ protocol,
+ transport,
+ is_user_consenting,
+ has_user_verification,
+ is_user_verified,
+ has_resident_key,
+ u2f_impl: Some(SoftwareU2FToken::new()),
+ credentials: Vec::new(),
+ },
+ _ => unreachable!(),
+ }
+ }
+
+ pub fn insert_credential(
+ &mut self,
+ credential: &[u8],
+ privkey: &[u8],
+ rp_id: String,
+ is_resident_credential: bool,
+ user_handle: &[u8],
+ sign_count: u64,
+ ) {
+ let c = TestTokenCredential {
+ credential: credential.to_vec(),
+ privkey: privkey.to_vec(),
+ rp_id,
+ is_resident_credential,
+ user_handle: user_handle.to_vec(),
+ sign_count,
+ };
+
+ match self
+ .credentials
+ .binary_search_by_key(&credential, |probe| &probe.credential)
+ {
+ Ok(_) => {}
+ Err(idx) => self.credentials.insert(idx, c),
+ }
+ }
+
+ pub fn delete_credential(&mut self, credential: &[u8]) -> bool {
+ debug!("Asking to delete credential",);
+ if let Ok(idx) = self
+ .credentials
+ .binary_search_by_key(&credential, |probe| &probe.credential)
+ {
+ debug!("Asking to delete credential from idx {}", idx);
+ self.credentials.remove(idx);
+ return true;
+ }
+
+ false
+ }
+
+ pub fn register(&self) -> crate::Result<RegisterResult> {
+ if self.u2f_impl.is_some() {
+ return self.u2f_impl.as_ref().unwrap().register(
+ RegisterFlags::empty(),
+ 10_000,
+ vec![0; 32],
+ vec![0; 32],
+ vec![],
+ );
+ }
+ Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::Unknown,
+ ))
+ }
+
+ pub fn sign(&self) -> crate::Result<SignResult> {
+ if self.u2f_impl.is_some() {
+ return self.u2f_impl.as_ref().unwrap().sign(
+ SignFlags::empty(),
+ 10_000,
+ vec![0; 32],
+ vec![vec![0; 32]],
+ vec![],
+ );
+ }
+ Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::Unknown,
+ ))
+ }
+}
diff --git a/third_party/rust/authenticator/src/virtualdevices/webdriver/virtualmanager.rs b/third_party/rust/authenticator/src/virtualdevices/webdriver/virtualmanager.rs
new file mode 100644
index 0000000000..31e5d09e3a
--- /dev/null
+++ b/third_party/rust/authenticator/src/virtualdevices/webdriver/virtualmanager.rs
@@ -0,0 +1,151 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use runloop::RunLoop;
+use std::net::{IpAddr, Ipv4Addr, SocketAddr};
+use std::ops::Deref;
+use std::sync::mpsc::Sender;
+use std::sync::{Arc, Mutex};
+use std::vec;
+use std::{io, string, thread};
+
+use crate::authenticatorservice::{AuthenticatorTransport, RegisterArgs, SignArgs};
+use crate::errors;
+use crate::statecallback::StateCallback;
+use crate::virtualdevices::webdriver::{testtoken, web_api};
+
+pub struct VirtualManagerState {
+ pub authenticator_counter: u64,
+ pub tokens: vec::Vec<testtoken::TestToken>,
+}
+
+impl VirtualManagerState {
+ pub fn new() -> Arc<Mutex<VirtualManagerState>> {
+ Arc::new(Mutex::new(VirtualManagerState {
+ authenticator_counter: 0,
+ tokens: vec![],
+ }))
+ }
+}
+
+pub struct VirtualManager {
+ addr: SocketAddr,
+ state: Arc<Mutex<VirtualManagerState>>,
+ rloop: Option<RunLoop>,
+}
+
+impl VirtualManager {
+ pub fn new() -> io::Result<Self> {
+ let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080);
+ let state = VirtualManagerState::new();
+ let stateclone = state.clone();
+
+ let builder = thread::Builder::new().name("WebDriver Command Server".into());
+ builder.spawn(move || {
+ web_api::serve(stateclone, addr);
+ })?;
+
+ Ok(Self {
+ addr,
+ state,
+ rloop: None,
+ })
+ }
+
+ pub fn url(&self) -> string::String {
+ format!("http://{}/webauthn/authenticator", &self.addr)
+ }
+}
+
+impl AuthenticatorTransport for VirtualManager {
+ fn register(
+ &mut self,
+ timeout: u64,
+ _ctap_args: RegisterArgs,
+ _status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::RegisterResult>>,
+ ) -> crate::Result<()> {
+ if self.rloop.is_some() {
+ error!("WebDriver state error, prior operation never cancelled.");
+ return Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::Unknown,
+ ));
+ }
+
+ let state = self.state.clone();
+ let rloop = try_or!(
+ RunLoop::new_with_timeout(
+ move |alive| {
+ while alive() {
+ let state_obj = state.lock().unwrap();
+
+ for token in state_obj.tokens.deref() {
+ if token.is_user_consenting {
+ let register_result = token.register();
+ thread::spawn(move || {
+ callback.call(register_result);
+ });
+ return;
+ }
+ }
+ }
+ },
+ timeout
+ ),
+ |_| Err(errors::AuthenticatorError::Platform)
+ );
+
+ self.rloop = Some(rloop);
+ Ok(())
+ }
+
+ fn sign(
+ &mut self,
+ timeout: u64,
+ _ctap_args: SignArgs,
+ _status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::SignResult>>,
+ ) -> crate::Result<()> {
+ if self.rloop.is_some() {
+ error!("WebDriver state error, prior operation never cancelled.");
+ return Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::Unknown,
+ ));
+ }
+
+ let state = self.state.clone();
+ let rloop = try_or!(
+ RunLoop::new_with_timeout(
+ move |alive| {
+ while alive() {
+ let state_obj = state.lock().unwrap();
+
+ for token in state_obj.tokens.deref() {
+ if token.is_user_consenting {
+ let sign_result = token.sign();
+ thread::spawn(move || {
+ callback.call(sign_result);
+ });
+ return;
+ }
+ }
+ }
+ },
+ timeout
+ ),
+ |_| Err(errors::AuthenticatorError::Platform)
+ );
+
+ self.rloop = Some(rloop);
+ Ok(())
+ }
+
+ fn cancel(&mut self) -> crate::Result<()> {
+ if let Some(r) = self.rloop.take() {
+ debug!("WebDriver operation cancelled.");
+ r.cancel();
+ }
+ Ok(())
+ }
+}
diff --git a/third_party/rust/authenticator/src/virtualdevices/webdriver/web_api.rs b/third_party/rust/authenticator/src/virtualdevices/webdriver/web_api.rs
new file mode 100644
index 0000000000..07bfc9f612
--- /dev/null
+++ b/third_party/rust/authenticator/src/virtualdevices/webdriver/web_api.rs
@@ -0,0 +1,964 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use serde::{Deserialize, Serialize};
+use std::net::SocketAddr;
+use std::string;
+use std::sync::{Arc, Mutex};
+use warp::Filter;
+
+use crate::virtualdevices::webdriver::{testtoken, virtualmanager::VirtualManagerState};
+
+fn default_as_false() -> bool {
+ false
+}
+fn default_as_true() -> bool {
+ false
+}
+
+#[derive(Serialize, Deserialize, Clone, PartialEq)]
+pub struct AuthenticatorConfiguration {
+ protocol: string::String,
+ transport: string::String,
+ #[serde(rename = "hasResidentKey")]
+ #[serde(default = "default_as_false")]
+ has_resident_key: bool,
+ #[serde(rename = "hasUserVerification")]
+ #[serde(default = "default_as_false")]
+ has_user_verification: bool,
+ #[serde(rename = "isUserConsenting")]
+ #[serde(default = "default_as_true")]
+ is_user_consenting: bool,
+ #[serde(rename = "isUserVerified")]
+ #[serde(default = "default_as_false")]
+ is_user_verified: bool,
+}
+
+#[derive(Serialize, Deserialize, Clone, PartialEq)]
+pub struct CredentialParameters {
+ #[serde(rename = "credentialId")]
+ credential_id: String,
+ #[serde(rename = "isResidentCredential")]
+ is_resident_credential: bool,
+ #[serde(rename = "rpId")]
+ rp_id: String,
+ #[serde(rename = "privateKey")]
+ private_key: String,
+ #[serde(rename = "userHandle")]
+ #[serde(default)]
+ user_handle: String,
+ #[serde(rename = "signCount")]
+ sign_count: u64,
+}
+
+#[derive(Serialize, Deserialize, Clone, PartialEq)]
+pub struct UserVerificationParameters {
+ #[serde(rename = "isUserVerified")]
+ is_user_verified: bool,
+}
+
+impl CredentialParameters {
+ fn new_from_test_token_credential(tc: &testtoken::TestTokenCredential) -> CredentialParameters {
+ let credential_id = base64::encode_config(&tc.credential, base64::URL_SAFE);
+
+ let private_key = base64::encode_config(&tc.privkey, base64::URL_SAFE);
+
+ let user_handle = base64::encode_config(&tc.user_handle, base64::URL_SAFE);
+
+ CredentialParameters {
+ credential_id,
+ is_resident_credential: tc.is_resident_credential,
+ rp_id: tc.rp_id.clone(),
+ private_key,
+ user_handle,
+ sign_count: tc.sign_count,
+ }
+ }
+}
+
+fn with_state(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = (Arc<Mutex<VirtualManagerState>>,), Error = std::convert::Infallible> + Clone
+{
+ warp::any().map(move || state.clone())
+}
+
+fn authenticator_add(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
+ warp::path!("webauthn" / "authenticator")
+ .and(warp::post())
+ .and(warp::body::json())
+ .and(with_state(state))
+ .and_then(handlers::authenticator_add)
+}
+
+fn authenticator_delete(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
+ warp::path!("webauthn" / "authenticator" / u64)
+ .and(warp::delete())
+ .and(with_state(state))
+ .and_then(handlers::authenticator_delete)
+}
+
+fn authenticator_set_uv(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
+ warp::path!("webauthn" / "authenticator" / u64 / "uv")
+ .and(warp::post())
+ .and(warp::body::json())
+ .and(with_state(state))
+ .and_then(handlers::authenticator_set_uv)
+}
+
+// This is not part of the specification, but it's useful for debugging
+fn authenticator_get(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
+ warp::path!("webauthn" / "authenticator" / u64)
+ .and(warp::get())
+ .and(with_state(state))
+ .and_then(handlers::authenticator_get)
+}
+
+fn authenticator_credential_add(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
+ warp::path!("webauthn" / "authenticator" / u64 / "credential")
+ .and(warp::post())
+ .and(warp::body::json())
+ .and(with_state(state))
+ .and_then(handlers::authenticator_credential_add)
+}
+
+fn authenticator_credential_delete(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
+ warp::path!("webauthn" / "authenticator" / u64 / "credentials" / String)
+ .and(warp::delete())
+ .and(with_state(state))
+ .and_then(handlers::authenticator_credential_delete)
+}
+
+fn authenticator_credentials_get(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
+ warp::path!("webauthn" / "authenticator" / u64 / "credentials")
+ .and(warp::get())
+ .and(with_state(state))
+ .and_then(handlers::authenticator_credentials_get)
+}
+
+fn authenticator_credentials_clear(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
+ warp::path!("webauthn" / "authenticator" / u64 / "credentials")
+ .and(warp::delete())
+ .and(with_state(state))
+ .and_then(handlers::authenticator_credentials_clear)
+}
+
+mod handlers {
+ use super::{CredentialParameters, UserVerificationParameters};
+ use crate::virtualdevices::webdriver::{
+ testtoken, virtualmanager::VirtualManagerState, web_api::AuthenticatorConfiguration,
+ };
+ use serde::Serialize;
+ use std::convert::Infallible;
+ use std::ops::DerefMut;
+ use std::sync::{Arc, Mutex};
+ use std::vec;
+ use warp::http::{uri, StatusCode};
+
+ #[derive(Serialize)]
+ struct JsonSuccess {}
+
+ impl JsonSuccess {
+ pub fn blank() -> JsonSuccess {
+ JsonSuccess {}
+ }
+ }
+
+ #[derive(Serialize)]
+ struct JsonError {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ line: Option<u32>,
+ error: String,
+ details: String,
+ }
+
+ impl JsonError {
+ pub fn new(error: &str, line: u32, details: &str) -> JsonError {
+ JsonError {
+ details: details.to_string(),
+ error: error.to_string(),
+ line: Some(line),
+ }
+ }
+ pub fn from_status_code(code: StatusCode) -> JsonError {
+ JsonError {
+ details: code.canonical_reason().unwrap().to_string(),
+ line: None,
+ error: "".to_string(),
+ }
+ }
+ pub fn from_error(error: &str) -> JsonError {
+ JsonError {
+ details: "".to_string(),
+ error: error.to_string(),
+ line: None,
+ }
+ }
+ }
+
+ macro_rules! reply_error {
+ ($status:expr) => {
+ warp::reply::with_status(
+ warp::reply::json(&JsonError::from_status_code($status)),
+ $status,
+ )
+ };
+ }
+
+ macro_rules! try_json {
+ ($val:expr, $status:expr) => {
+ match $val {
+ Ok(v) => v,
+ Err(e) => {
+ return Ok(warp::reply::with_status(
+ warp::reply::json(&JsonError::new(
+ $status.canonical_reason().unwrap(),
+ line!(),
+ &e.to_string(),
+ )),
+ $status,
+ ));
+ }
+ }
+ };
+ }
+
+ pub fn validate_rp_id(rp_id: &str) -> crate::Result<()> {
+ if let Ok(uri) = rp_id.parse::<uri::Uri>().map_err(|_| {
+ crate::errors::AuthenticatorError::U2FToken(crate::errors::U2FTokenError::Unknown)
+ }) {
+ if uri.scheme().is_none()
+ && uri.path_and_query().is_none()
+ && uri.port().is_none()
+ && uri.host().is_some()
+ && uri.authority().unwrap() == uri.host().unwrap()
+ // Don't try too hard to ensure it's a valid domain, just
+ // ensure there's a label delim in there somewhere
+ && uri.host().unwrap().find('.').is_some()
+ {
+ return Ok(());
+ }
+ }
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::Unknown,
+ ))
+ }
+
+ pub async fn authenticator_add(
+ auth: AuthenticatorConfiguration,
+ state: Arc<Mutex<VirtualManagerState>>,
+ ) -> Result<impl warp::Reply, Infallible> {
+ let protocol = match auth.protocol.as_str() {
+ "ctap1/u2f" => testtoken::TestWireProtocol::CTAP1,
+ "ctap2" => testtoken::TestWireProtocol::CTAP2,
+ _ => {
+ return Ok(warp::reply::with_status(
+ warp::reply::json(&JsonError::from_error(
+ format!("unknown protocol: {}", auth.protocol).as_str(),
+ )),
+ StatusCode::BAD_REQUEST,
+ ))
+ }
+ };
+
+ let mut state_lock = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR);
+ let mut state_obj = state_lock.deref_mut();
+ state_obj.authenticator_counter += 1;
+
+ let tt = testtoken::TestToken::new(
+ state_obj.authenticator_counter,
+ protocol,
+ auth.transport,
+ auth.is_user_consenting,
+ auth.has_user_verification,
+ auth.is_user_verified,
+ auth.has_resident_key,
+ );
+
+ match state_obj
+ .tokens
+ .binary_search_by_key(&state_obj.authenticator_counter, |probe| probe.id)
+ {
+ Ok(_) => panic!("unexpected repeat of authenticator_id"),
+ Err(idx) => state_obj.tokens.insert(idx, tt),
+ }
+
+ #[derive(Serialize)]
+ struct AddResult {
+ #[serde(rename = "authenticatorId")]
+ authenticator_id: u64,
+ }
+
+ Ok(warp::reply::with_status(
+ warp::reply::json(&AddResult {
+ authenticator_id: state_obj.authenticator_counter,
+ }),
+ StatusCode::CREATED,
+ ))
+ }
+
+ pub async fn authenticator_delete(
+ id: u64,
+ state: Arc<Mutex<VirtualManagerState>>,
+ ) -> Result<impl warp::Reply, Infallible> {
+ let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR);
+ match state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) {
+ Ok(idx) => state_obj.tokens.remove(idx),
+ Err(_) => {
+ return Ok(reply_error!(StatusCode::NOT_FOUND));
+ }
+ };
+
+ Ok(warp::reply::with_status(
+ warp::reply::json(&JsonSuccess::blank()),
+ StatusCode::OK,
+ ))
+ }
+
+ pub async fn authenticator_get(
+ id: u64,
+ state: Arc<Mutex<VirtualManagerState>>,
+ ) -> Result<impl warp::Reply, Infallible> {
+ let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR);
+ if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) {
+ let tt = &mut state_obj.tokens[idx];
+
+ let data = AuthenticatorConfiguration {
+ protocol: tt.protocol.to_webdriver_string(),
+ transport: tt.transport.clone(),
+ has_resident_key: tt.has_resident_key,
+ has_user_verification: tt.has_user_verification,
+ is_user_consenting: tt.is_user_consenting,
+ is_user_verified: tt.is_user_verified,
+ };
+
+ return Ok(warp::reply::with_status(
+ warp::reply::json(&data),
+ StatusCode::OK,
+ ));
+ }
+
+ Ok(reply_error!(StatusCode::NOT_FOUND))
+ }
+
+ pub async fn authenticator_set_uv(
+ id: u64,
+ uv: UserVerificationParameters,
+ state: Arc<Mutex<VirtualManagerState>>,
+ ) -> Result<impl warp::Reply, Infallible> {
+ let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR);
+
+ if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) {
+ let tt = &mut state_obj.tokens[idx];
+ tt.is_user_verified = uv.is_user_verified;
+ return Ok(warp::reply::with_status(
+ warp::reply::json(&JsonSuccess::blank()),
+ StatusCode::OK,
+ ));
+ }
+
+ Ok(reply_error!(StatusCode::NOT_FOUND))
+ }
+
+ pub async fn authenticator_credential_add(
+ id: u64,
+ auth: CredentialParameters,
+ state: Arc<Mutex<VirtualManagerState>>,
+ ) -> Result<impl warp::Reply, Infallible> {
+ let credential = try_json!(
+ base64::decode_config(&auth.credential_id, base64::URL_SAFE),
+ StatusCode::BAD_REQUEST
+ );
+
+ let privkey = try_json!(
+ base64::decode_config(&auth.private_key, base64::URL_SAFE),
+ StatusCode::BAD_REQUEST
+ );
+
+ let userhandle = try_json!(
+ base64::decode_config(&auth.user_handle, base64::URL_SAFE),
+ StatusCode::BAD_REQUEST
+ );
+
+ try_json!(validate_rp_id(&auth.rp_id), StatusCode::BAD_REQUEST);
+
+ let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR);
+ if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) {
+ let tt = &mut state_obj.tokens[idx];
+
+ tt.insert_credential(
+ &credential,
+ &privkey,
+ auth.rp_id,
+ auth.is_resident_credential,
+ &userhandle,
+ auth.sign_count,
+ );
+
+ return Ok(warp::reply::with_status(
+ warp::reply::json(&JsonSuccess::blank()),
+ StatusCode::CREATED,
+ ));
+ }
+
+ Ok(reply_error!(StatusCode::NOT_FOUND))
+ }
+
+ pub async fn authenticator_credential_delete(
+ id: u64,
+ credential_id: String,
+ state: Arc<Mutex<VirtualManagerState>>,
+ ) -> Result<impl warp::Reply, Infallible> {
+ let credential = try_json!(
+ base64::decode_config(&credential_id, base64::URL_SAFE),
+ StatusCode::BAD_REQUEST
+ );
+
+ let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR);
+
+ debug!("Asking to delete {}", &credential_id);
+
+ if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) {
+ let tt = &mut state_obj.tokens[idx];
+ debug!("Asking to delete from token {}", tt.id);
+ if tt.delete_credential(&credential) {
+ return Ok(warp::reply::with_status(
+ warp::reply::json(&JsonSuccess::blank()),
+ StatusCode::OK,
+ ));
+ }
+ }
+
+ Ok(reply_error!(StatusCode::NOT_FOUND))
+ }
+
+ pub async fn authenticator_credentials_get(
+ id: u64,
+ state: Arc<Mutex<VirtualManagerState>>,
+ ) -> Result<impl warp::Reply, Infallible> {
+ let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR);
+
+ if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) {
+ let tt = &mut state_obj.tokens[idx];
+ let mut creds: vec::Vec<CredentialParameters> = vec![];
+ for ttc in &tt.credentials {
+ creds.push(CredentialParameters::new_from_test_token_credential(ttc));
+ }
+
+ return Ok(warp::reply::with_status(
+ warp::reply::json(&creds),
+ StatusCode::OK,
+ ));
+ }
+
+ Ok(reply_error!(StatusCode::NOT_FOUND))
+ }
+
+ pub async fn authenticator_credentials_clear(
+ id: u64,
+ state: Arc<Mutex<VirtualManagerState>>,
+ ) -> Result<impl warp::Reply, Infallible> {
+ let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR);
+ if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) {
+ let tt = &mut state_obj.tokens[idx];
+
+ tt.credentials.clear();
+
+ return Ok(warp::reply::with_status(
+ warp::reply::json(&JsonSuccess::blank()),
+ StatusCode::OK,
+ ));
+ }
+
+ Ok(reply_error!(StatusCode::NOT_FOUND))
+ }
+}
+
+#[tokio::main]
+pub async fn serve(state: Arc<Mutex<VirtualManagerState>>, addr: SocketAddr) {
+ let routes = authenticator_add(state.clone())
+ .or(authenticator_delete(state.clone()))
+ .or(authenticator_get(state.clone()))
+ .or(authenticator_set_uv(state.clone()))
+ .or(authenticator_credential_add(state.clone()))
+ .or(authenticator_credential_delete(state.clone()))
+ .or(authenticator_credentials_get(state.clone()))
+ .or(authenticator_credentials_clear(state.clone()));
+
+ warp::serve(routes).run(addr).await;
+}
+
+#[cfg(test)]
+mod tests {
+ use super::handlers::validate_rp_id;
+ use super::testtoken::*;
+ use super::*;
+ use crate::virtualdevices::webdriver::virtualmanager::VirtualManagerState;
+ use std::sync::{Arc, Mutex};
+ use warp::http::StatusCode;
+
+ fn init() {
+ let _ = env_logger::builder().is_test(true).try_init();
+ }
+
+ #[test]
+ fn test_validate_rp_id() {
+ init();
+
+ assert_matches!(
+ validate_rp_id(&String::from("http://example.com")),
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::Unknown,
+ ))
+ );
+ assert_matches!(
+ validate_rp_id(&String::from("https://example.com")),
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::Unknown,
+ ))
+ );
+ assert_matches!(
+ validate_rp_id(&String::from("example.com:443")),
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::Unknown,
+ ))
+ );
+ assert_matches!(
+ validate_rp_id(&String::from("example.com/path")),
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::Unknown,
+ ))
+ );
+ assert_matches!(
+ validate_rp_id(&String::from("example.com:443/path")),
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::Unknown,
+ ))
+ );
+ assert_matches!(
+ validate_rp_id(&String::from("user:pass@example.com")),
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::Unknown,
+ ))
+ );
+ assert_matches!(
+ validate_rp_id(&String::from("com")),
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::Unknown,
+ ))
+ );
+ assert_matches!(validate_rp_id(&String::from("example.com")), Ok(()));
+ }
+
+ fn mk_state_with_token_list(ids: &[u64]) -> Arc<Mutex<VirtualManagerState>> {
+ let state = VirtualManagerState::new();
+
+ {
+ let mut state_obj = state.lock().unwrap();
+ for id in ids {
+ state_obj.tokens.push(TestToken::new(
+ *id,
+ TestWireProtocol::CTAP1,
+ "internal".to_string(),
+ true,
+ true,
+ true,
+ true,
+ ));
+ }
+
+ state_obj.tokens.sort_by_key(|probe| probe.id)
+ }
+
+ state
+ }
+
+ fn assert_success_rsp_blank(body: &warp::hyper::body::Bytes) {
+ assert_eq!(String::from_utf8_lossy(&body), r#"{}"#)
+ }
+
+ fn assert_creds_equals_test_token_params(
+ a: &[CredentialParameters],
+ b: &[TestTokenCredential],
+ ) {
+ assert_eq!(a.len(), b.len());
+
+ for (i, j) in a.iter().zip(b.iter()) {
+ assert_eq!(
+ i.credential_id,
+ base64::encode_config(&j.credential, base64::URL_SAFE)
+ );
+ assert_eq!(
+ i.user_handle,
+ base64::encode_config(&j.user_handle, base64::URL_SAFE)
+ );
+ assert_eq!(
+ i.private_key,
+ base64::encode_config(&j.privkey, base64::URL_SAFE)
+ );
+ assert_eq!(i.rp_id, j.rp_id);
+ assert_eq!(i.sign_count, j.sign_count);
+ assert_eq!(i.is_resident_credential, j.is_resident_credential);
+ }
+ }
+
+ #[tokio::test]
+ async fn test_authenticator_add() {
+ init();
+ let filter = authenticator_add(mk_state_with_token_list(&[]));
+
+ {
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator")
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ }
+
+ let valid_add = AuthenticatorConfiguration {
+ protocol: "ctap1/u2f".to_string(),
+ transport: "usb".to_string(),
+ has_resident_key: false,
+ has_user_verification: false,
+ is_user_consenting: false,
+ is_user_verified: false,
+ };
+
+ {
+ let mut invalid = valid_add.clone();
+ invalid.protocol = "unknown".to_string();
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator")
+ .json(&invalid)
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ assert!(String::from_utf8_lossy(&res.body())
+ .contains(&String::from("unknown protocol: unknown")));
+ }
+
+ {
+ let mut unknown = valid_add.clone();
+ unknown.transport = "unknown".to_string();
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator")
+ .json(&unknown)
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_success());
+ assert_eq!(
+ String::from_utf8_lossy(&res.body()),
+ r#"{"authenticatorId":1}"#
+ )
+ }
+
+ {
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator")
+ .json(&valid_add)
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_success());
+ assert_eq!(
+ String::from_utf8_lossy(&res.body()),
+ r#"{"authenticatorId":2}"#
+ )
+ }
+ }
+
+ #[tokio::test]
+ async fn test_authenticator_delete() {
+ init();
+ let filter = authenticator_delete(mk_state_with_token_list(&[32]));
+
+ {
+ let res = warp::test::request()
+ .method("DELETE")
+ .path("/webauthn/authenticator/3")
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ }
+
+ {
+ let res = warp::test::request()
+ .method("DELETE")
+ .path("/webauthn/authenticator/32")
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_success());
+ assert_success_rsp_blank(res.body());
+ }
+
+ {
+ let res = warp::test::request()
+ .method("DELETE")
+ .path("/webauthn/authenticator/42")
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ }
+ }
+
+ #[tokio::test]
+ async fn test_authenticator_change_uv() {
+ init();
+ let state = mk_state_with_token_list(&[1]);
+ let filter = authenticator_set_uv(state.clone());
+
+ {
+ let state_obj = state.lock().unwrap();
+ assert_eq!(true, state_obj.tokens[0].is_user_verified);
+ }
+
+ {
+ // Empty POST is bad
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/uv")
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ }
+
+ {
+ // Unexpected POST structure is bad
+ #[derive(Serialize)]
+ struct Unexpected {
+ id: u64,
+ }
+ let unexpected = Unexpected { id: 4 };
+
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/uv")
+ .json(&unexpected)
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ }
+
+ {
+ let param_false = UserVerificationParameters {
+ is_user_verified: false,
+ };
+
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/uv")
+ .json(&param_false)
+ .reply(&filter)
+ .await;
+ assert_eq!(res.status(), 200);
+ assert_success_rsp_blank(res.body());
+
+ let state_obj = state.lock().unwrap();
+ assert_eq!(false, state_obj.tokens[0].is_user_verified);
+ }
+
+ {
+ let param_false = UserVerificationParameters {
+ is_user_verified: true,
+ };
+
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/uv")
+ .json(&param_false)
+ .reply(&filter)
+ .await;
+ assert_eq!(res.status(), 200);
+ assert_success_rsp_blank(res.body());
+
+ let state_obj = state.lock().unwrap();
+ assert_eq!(true, state_obj.tokens[0].is_user_verified);
+ }
+ }
+
+ #[tokio::test]
+ async fn test_authenticator_credentials() {
+ init();
+ let state = mk_state_with_token_list(&[1]);
+ let filter = authenticator_credential_add(state.clone())
+ .or(authenticator_credential_delete(state.clone()))
+ .or(authenticator_credentials_get(state.clone()))
+ .or(authenticator_credentials_clear(state.clone()));
+
+ let valid_add_credential = CredentialParameters {
+ credential_id: r"c3VwZXIgcmVhZGVy".to_string(),
+ is_resident_credential: true,
+ rp_id: "valid.rpid".to_string(),
+ private_key: base64::encode_config(b"hello internet~", base64::URL_SAFE),
+ user_handle: base64::encode_config(b"hello internet~", base64::URL_SAFE),
+ sign_count: 0,
+ };
+
+ {
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/credential")
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ }
+
+ {
+ let mut invalid = valid_add_credential.clone();
+ invalid.credential_id = "!@#$ invalid base64".to_string();
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/credential")
+ .json(&invalid)
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ }
+
+ {
+ let mut invalid = valid_add_credential.clone();
+ invalid.rp_id = "example".to_string();
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/credential")
+ .json(&invalid)
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ }
+
+ {
+ let mut invalid = valid_add_credential.clone();
+ invalid.rp_id = "https://example.com".to_string();
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/credential")
+ .json(&invalid)
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+
+ let state_obj = state.lock().unwrap();
+ assert_eq!(0, state_obj.tokens[0].credentials.len());
+ }
+
+ {
+ let mut no_user_handle = valid_add_credential.clone();
+ no_user_handle.user_handle = "".to_string();
+ no_user_handle.credential_id = "YQo=".to_string();
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/credential")
+ .json(&no_user_handle)
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_success());
+ assert_success_rsp_blank(res.body());
+
+ let state_obj = state.lock().unwrap();
+ assert_eq!(1, state_obj.tokens[0].credentials.len());
+ let c = &state_obj.tokens[0].credentials[0];
+ assert!(c.user_handle.is_empty());
+ }
+
+ {
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/credential")
+ .json(&valid_add_credential)
+ .reply(&filter)
+ .await;
+ assert_eq!(res.status(), StatusCode::CREATED);
+ assert_success_rsp_blank(res.body());
+
+ let state_obj = state.lock().unwrap();
+ assert_eq!(2, state_obj.tokens[0].credentials.len());
+ let c = &state_obj.tokens[0].credentials[1];
+ assert!(!c.user_handle.is_empty());
+ }
+
+ {
+ // Duplicate, should still be two credentials
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/credential")
+ .json(&valid_add_credential)
+ .reply(&filter)
+ .await;
+ assert_eq!(res.status(), StatusCode::CREATED);
+ assert_success_rsp_blank(res.body());
+
+ let state_obj = state.lock().unwrap();
+ assert_eq!(2, state_obj.tokens[0].credentials.len());
+ }
+
+ {
+ let res = warp::test::request()
+ .method("GET")
+ .path("/webauthn/authenticator/1/credentials")
+ .reply(&filter)
+ .await;
+ assert_eq!(res.status(), 200);
+ let (_, body) = res.into_parts();
+ let cred = serde_json::de::from_slice::<Vec<CredentialParameters>>(&body).unwrap();
+
+ let state_obj = state.lock().unwrap();
+ assert_creds_equals_test_token_params(&cred, &state_obj.tokens[0].credentials);
+ }
+
+ {
+ let res = warp::test::request()
+ .method("DELETE")
+ .path("/webauthn/authenticator/1/credentials/YmxhbmsK")
+ .reply(&filter)
+ .await;
+ assert_eq!(res.status(), 404);
+ }
+
+ {
+ let res = warp::test::request()
+ .method("DELETE")
+ .path("/webauthn/authenticator/1/credentials/c3VwZXIgcmVhZGVy")
+ .reply(&filter)
+ .await;
+ assert_eq!(res.status(), 200);
+ assert_success_rsp_blank(res.body());
+
+ let state_obj = state.lock().unwrap();
+ assert_eq!(1, state_obj.tokens[0].credentials.len());
+ }
+
+ {
+ let res = warp::test::request()
+ .method("DELETE")
+ .path("/webauthn/authenticator/1/credentials")
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_success());
+ assert_success_rsp_blank(res.body());
+
+ let state_obj = state.lock().unwrap();
+ assert_eq!(0, state_obj.tokens[0].credentials.len());
+ }
+ }
+}