Bug 1830944 - vendor authenticator-rs v0.4.0-alpha14. r=keeler,supply-chain-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D176938
This commit is contained in:
John Schanck 2023-05-03 16:37:45 +00:00
parent 7e261edb97
commit 92fb6d7ad4
44 changed files with 1074 additions and 833 deletions

4
Cargo.lock generated
View file

@ -320,9 +320,9 @@ dependencies = [
[[package]]
name = "authenticator"
version = "0.4.0-alpha.13"
version = "0.4.0-alpha.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7571904a2c563ed99aac46a7d990186f76a8568f0fd1a769a78214dd385946bf"
checksum = "9f3aaf9a8213bf1a4cfafca1a38befe0873a2a4d8013136d2dd12227e14bb2ba"
dependencies = [
"base64 0.13.999",
"bitflags 1.3.2",

View file

@ -7609,6 +7609,14 @@ var WebAuthnPromptHelper = {
"deviceBlocked",
"webauthn.deviceBlockedPrompt"
);
} else if (data.action == "pin-not-set") {
this.show_info(
mgr,
data.origin,
data.tid,
"pinNotSet",
"webauthn.pinNotSetPrompt"
);
}
},

View file

@ -462,6 +462,8 @@ webauthn.selectDevicePrompt=Multiple devices found for %S. Please select one.
webauthn.deviceBlockedPrompt=User verification failed on %S. There are no attempts left and your device has been locked, because the wrong PIN was provided too many times. The device needs a reset.
# LOCALIZATION NOTE (webauthn.pinAuthBlockedPrompt): %S is hostname
webauthn.pinAuthBlockedPrompt=User verification failed on %S. There were too many failed attempts in a row and PIN authentication has been temporarily blocked. Your device needs a power cycle (unplug and re-insert).
# LOCALIZATION NOTE (webauthn.pinNotSetPrompt): %S is hostname
webauthn.pinNotSetPrompt=User verification failed on %S. You may need to set a PIN on your device.
webauthn.cancel=Cancel
webauthn.cancel.accesskey=c
webauthn.proceed=Proceed

View file

@ -5,7 +5,7 @@ edition = "2021"
authors = ["Martin Sirringhaus", "John Schanck"]
[dependencies]
authenticator = { version = "0.4.0-alpha.13", features = ["gecko"] }
authenticator = { version = "0.4.0-alpha.14", features = ["gecko"] }
log = "0.4"
moz_task = { path = "../../../xpcom/rust/moz_task" }
nserror = { path = "../../../xpcom/rust/nserror" }

View file

@ -9,13 +9,11 @@ extern crate log;
extern crate xpcom;
use authenticator::{
authenticatorservice::{
AuthenticatorService, GetAssertionOptions, MakeCredentialsOptions,
RegisterArgs, SignArgs,
},
authenticatorservice::{AuthenticatorService, RegisterArgs, SignArgs},
ctap2::attestation::AttestationStatement,
ctap2::server::{
PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, User,
PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty,
ResidentKeyRequirement, User, UserVerificationRequirement,
},
errors::{AuthenticatorError, PinError, U2FTokenError},
statecallback::StateCallback,
@ -85,6 +83,7 @@ fn authrs_to_nserror(e: &AuthenticatorError) -> nsresult {
AuthenticatorError::PinError(PinError::InvalidPin(_)) => NS_ERROR_DOM_OPERATION_ERR,
AuthenticatorError::PinError(PinError::PinAuthBlocked) => NS_ERROR_DOM_OPERATION_ERR,
AuthenticatorError::PinError(PinError::PinBlocked) => NS_ERROR_DOM_OPERATION_ERR,
AuthenticatorError::PinError(PinError::PinNotSet) => NS_ERROR_DOM_OPERATION_ERR,
_ => NS_ERROR_DOM_UNKNOWN_ERR,
}
}
@ -376,9 +375,20 @@ fn status_callback(
make_pin_error_prompt("device-blocked", tid, origin, browsing_context_id);
controller.send_prompt(tid, &notification_str);
}
Ok(StatusUpdate::PinUvError(StatusPinUv::PinNotSet)) => {
let notification_str =
make_pin_error_prompt("pin-not-set", tid, origin, browsing_context_id);
controller.send_prompt(tid, &notification_str);
}
Ok(StatusUpdate::PinUvError(e)) => {
warn!("Unexpected error: {:?}", e)
}
Ok(StatusUpdate::InteractiveManagement((_, dev_info, auth_info))) => {
debug!(
"STATUS: interactive management: {}, {:?}",
dev_info, auth_info
);
}
Err(RecvError) => {
debug!("STATUS: end");
return;
@ -482,22 +492,26 @@ impl AuthrsTransport {
let mut resident_key = nsString::new();
unsafe { args.GetResidentKey(&mut *resident_key) }.to_result()?;
let resident_key = if resident_key.eq("required") {
Some(true)
let resident_key_req = if resident_key.eq("required") {
ResidentKeyRequirement::Required
} else if resident_key.eq("preferred") {
ResidentKeyRequirement::Preferred
} else if resident_key.eq("discouraged") {
Some(false)
ResidentKeyRequirement::Discouraged
} else {
None
return Err(NS_ERROR_FAILURE);
};
let mut user_verification = nsString::new();
unsafe { args.GetUserVerification(&mut *user_verification) }.to_result()?;
let user_verification = if user_verification.eq("required") {
Some(true)
let user_verification_req = if user_verification.eq("required") {
UserVerificationRequirement::Required
} else if user_verification.eq("preferred") {
UserVerificationRequirement::Preferred
} else if user_verification.eq("discouraged") {
Some(false)
UserVerificationRequirement::Discouraged
} else {
None
return Err(NS_ERROR_FAILURE);
};
let mut attestation_conveyance_preference = nsString::new();
@ -529,10 +543,8 @@ impl AuthrsTransport {
},
pub_cred_params,
exclude_list,
options: MakeCredentialsOptions {
resident_key,
user_verification,
},
user_verification_req,
resident_key_req,
extensions: Default::default(),
pin: None,
use_ctap1_fallback: static_prefs::pref!("security.webauthn.ctap2") == false,
@ -625,12 +637,14 @@ impl AuthrsTransport {
let mut user_verification = nsString::new();
unsafe { args.GetUserVerification(&mut *user_verification) }.to_result()?;
let user_verification = if user_verification.eq("required") {
Some(true)
let user_verification_req = if user_verification.eq("required") {
UserVerificationRequirement::Required
} else if user_verification.eq("preferred") {
UserVerificationRequirement::Preferred
} else if user_verification.eq("discouraged") {
Some(false)
UserVerificationRequirement::Discouraged
} else {
None
return Err(NS_ERROR_FAILURE);
};
let mut alternate_rp_id = None;
@ -695,10 +709,8 @@ impl AuthrsTransport {
relying_party_id: relying_party_id.to_string(),
origin: origin.to_string(),
allow_list,
options: GetAssertionOptions {
user_presence: Some(true),
user_verification,
},
user_verification_req,
user_presence_req: true,
extensions: Default::default(),
pin: None,
alternate_rp_id,

View file

@ -8,6 +8,13 @@ user-id = 1258
user-login = "padenot"
user-name = "Paul Adenot"
[[publisher.authenticator]]
version = "0.4.0-alpha.14"
when = "2023-05-02"
user-id = 175410
user-login = "jschanck"
user-name = "John Schanck"
[[publisher.bhttp]]
version = "0.3.1"
when = "2023-02-23"

File diff suppressed because one or more lines are too long

View file

@ -39,7 +39,7 @@ dependencies = [
[[package]]
name = "authenticator"
version = "0.4.0-alpha.13"
version = "0.4.0-alpha.14"
dependencies = [
"assert_matches",
"base64",

View file

@ -12,7 +12,7 @@
[package]
edition = "2018"
name = "authenticator"
version = "0.4.0-alpha.13"
version = "0.4.0-alpha.14"
authors = [
"J.C. Jones <jc@mozilla.com>",
"Tim Taubert <ttaubert@mozilla.com>",

View file

@ -4,12 +4,12 @@
use authenticator::{
authenticatorservice::{
AuthenticatorService, GetAssertionExtensions, GetAssertionOptions,
HmacSecretExtension, MakeCredentialsExtensions, MakeCredentialsOptions, RegisterArgs,
SignArgs,
AuthenticatorService, GetAssertionExtensions, HmacSecretExtension,
MakeCredentialsExtensions, RegisterArgs, SignArgs,
},
ctap2::server::{
PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, Transport, User,
PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty,
ResidentKeyRequirement, Transport, User, UserVerificationRequirement,
},
statecallback::StateCallback,
COSEAlgorithm, Pin, RegisterResult, SignResult, StatusPinUv, StatusUpdate,
@ -40,6 +40,7 @@ fn main() {
);
opts.optflag("s", "hmac_secret", "With hmac-secret");
opts.optflag("h", "help", "print this help menu");
opts.optflag("f", "fallback", "Use CTAP1 fallback implementation");
let matches = match opts.parse(&args[1..]) {
Ok(m) => m,
Err(f) => panic!("{}", f.to_string()),
@ -49,13 +50,15 @@ fn main() {
return;
}
let mut manager = AuthenticatorService::new()
.expect("The auth service should initialize safely");
let mut manager =
AuthenticatorService::new().expect("The auth service should initialize safely");
if !matches.opt_present("no-u2f-usb-hid") {
manager.add_u2f_usb_hid_platform_transports();
}
let fallback = matches.opt_present("fallback");
let timeout_ms = match matches.opt_get_default::<u64>("timeout", 25) {
Ok(timeout_s) => {
println!("Using {}s as the timeout", &timeout_s);
@ -78,12 +81,12 @@ fn main() {
challenge.update(challenge_str.as_bytes());
let chall_bytes: [u8; 32] = challenge.finalize().into();
// TODO(MS): Needs to be added to RegisterArgsCtap2
// let flags = RegisterFlags::empty();
let (status_tx, status_rx) = channel::<StatusUpdate>();
thread::spawn(move || loop {
match status_rx.recv() {
Ok(StatusUpdate::InteractiveManagement(..)) => {
panic!("STATUS: This can't happen when doing non-interactive usage");
}
Ok(StatusUpdate::DeviceAvailable { dev_info }) => {
println!("STATUS: device available: {dev_info}")
}
@ -153,7 +156,7 @@ fn main() {
display_name: None,
};
let origin = "https://example.com".to_string();
let ctap_args = RegisterArgs{
let ctap_args = RegisterArgs {
client_data_hash: chall_bytes,
relying_party: RelyingParty {
id: "example.com".to_string(),
@ -178,10 +181,8 @@ fn main() {
],
transports: vec![Transport::USB, Transport::NFC],
}],
options: MakeCredentialsOptions {
resident_key: None,
user_verification: None,
},
user_verification_req: UserVerificationRequirement::Preferred,
resident_key_req: ResidentKeyRequirement::Discouraged,
extensions: MakeCredentialsExtensions {
hmac_secret: if matches.opt_present("hmac_secret") {
Some(true)
@ -191,7 +192,7 @@ fn main() {
..Default::default()
},
pin: None,
use_ctap1_fallback: false,
use_ctap1_fallback: fallback,
};
let attestation_object;
@ -201,8 +202,7 @@ fn main() {
register_tx.send(rv).unwrap();
}));
if let Err(e) = manager.register(timeout_ms, ctap_args, status_tx.clone(), callback)
{
if let Err(e) = manager.register(timeout_ms, ctap_args, status_tx.clone(), callback) {
panic!("Couldn't register: {:?}", e);
};
@ -242,7 +242,8 @@ fn main() {
origin,
relying_party_id: "example.com".to_string(),
allow_list,
options: GetAssertionOptions::default(),
user_verification_req: UserVerificationRequirement::Preferred,
user_presence_req: true,
extensions: GetAssertionExtensions {
hmac_secret: if matches.opt_present("hmac_secret") {
Some(HmacSecretExtension::new(
@ -259,7 +260,7 @@ fn main() {
},
pin: None,
alternate_rp_id: None,
use_ctap1_fallback: false,
use_ctap1_fallback: fallback,
};
loop {

View file

@ -3,12 +3,10 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use authenticator::{
authenticatorservice::{
AuthenticatorService, GetAssertionOptions, MakeCredentialsOptions,
RegisterArgs, SignArgs,
},
authenticatorservice::{AuthenticatorService, RegisterArgs, SignArgs},
ctap2::server::{
PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, Transport, User,
PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty,
ResidentKeyRequirement, Transport, User, UserVerificationRequirement,
},
statecallback::StateCallback,
COSEAlgorithm, Pin, RegisterResult, SignResult, StatusPinUv, StatusUpdate,
@ -49,6 +47,9 @@ fn register_user(manager: &mut AuthenticatorService, username: &str, timeout_ms:
let (status_tx, status_rx) = channel::<StatusUpdate>();
thread::spawn(move || loop {
match status_rx.recv() {
Ok(StatusUpdate::InteractiveManagement(..)) => {
panic!("STATUS: This can't happen when doing non-interactive usage");
}
Ok(StatusUpdate::DeviceAvailable { dev_info }) => {
println!("STATUS: device available: {dev_info}")
}
@ -139,10 +140,8 @@ fn register_user(manager: &mut AuthenticatorService, username: &str, timeout_ms:
id: vec![],
transports: vec![Transport::USB, Transport::NFC],
}],
options: MakeCredentialsOptions {
resident_key: Some(true),
user_verification: Some(true),
},
user_verification_req: UserVerificationRequirement::Required,
resident_key_req: ResidentKeyRequirement::Required,
extensions: Default::default(),
pin: None,
use_ctap1_fallback: false,
@ -201,8 +200,8 @@ fn main() {
return;
}
let mut manager = AuthenticatorService::new()
.expect("The auth service should initialize safely");
let mut manager =
AuthenticatorService::new().expect("The auth service should initialize safely");
if !matches.opt_present("no-u2f-usb-hid") {
manager.add_u2f_usb_hid_platform_transports();
@ -241,6 +240,9 @@ fn main() {
let (status_tx, status_rx) = channel::<StatusUpdate>();
thread::spawn(move || loop {
match status_rx.recv() {
Ok(StatusUpdate::InteractiveManagement(..)) => {
panic!("STATUS: This can't happen when doing non-interactive usage");
}
Ok(StatusUpdate::DeviceAvailable { dev_info }) => {
println!("STATUS: device available: {dev_info}")
}
@ -311,7 +313,8 @@ fn main() {
origin,
relying_party_id: "example.com".to_string(),
allow_list,
options: GetAssertionOptions::default(),
user_verification_req: UserVerificationRequirement::Required,
user_presence_req: true,
extensions: Default::default(),
pin: None,
alternate_rp_id: None,

View file

@ -0,0 +1,201 @@
/* 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 authenticator::{
authenticatorservice::AuthenticatorService, errors::AuthenticatorError,
statecallback::StateCallback, InteractiveRequest, Pin, ResetResult, StatusUpdate,
};
use getopts::Options;
use log::debug;
use std::{env, io, thread};
use std::{
io::Write,
sync::mpsc::{channel, Receiver, RecvError},
};
fn print_usage(program: &str, opts: Options) {
let brief = format!("Usage: {program} [options]");
print!("{}", opts.usage(&brief));
}
fn interactive_status_callback(status_rx: Receiver<StatusUpdate>) {
let mut num_of_devices = 0;
loop {
match status_rx.recv() {
Ok(StatusUpdate::InteractiveManagement((tx, dev_info, auth_info))) => {
debug!(
"STATUS: interactive management: {:#}, {:#?}",
dev_info, auth_info
);
println!("Device info {:#}", dev_info);
let mut change_pin = false;
if let Some(info) = auth_info {
println!("Authenticator Info {:#?}", info);
println!();
println!("What do you wish to do?");
let mut choices = vec!["0", "1"];
println!("(0) Quit");
println!("(1) Reset token");
match info.options.client_pin {
None => {}
Some(true) => {
println!("(2) Change PIN");
choices.push("2");
change_pin = true;
}
Some(false) => {
println!("(2) Set PIN");
choices.push("2");
}
}
let mut input = String::new();
while !choices.contains(&input.trim()) {
input.clear();
print!("Your choice: ");
io::stdout()
.lock()
.flush()
.expect("Failed to flush stdout!");
io::stdin()
.read_line(&mut input)
.expect("error: unable to read user input");
}
input = input.trim().to_string();
match input.as_str() {
"0" => {
return;
}
"1" => {
tx.send(InteractiveRequest::Reset)
.expect("Failed to send Reset request.");
}
"2" => {
let raw_new_pin = rpassword::prompt_password_stderr("Enter new PIN: ")
.expect("Failed to read PIN");
let new_pin = Pin::new(&raw_new_pin);
if change_pin {
let raw_curr_pin =
rpassword::prompt_password_stderr("Enter current PIN: ")
.expect("Failed to read PIN");
let curr_pin = Pin::new(&raw_curr_pin);
tx.send(InteractiveRequest::ChangePIN(curr_pin, new_pin))
.expect("Failed to send PIN-change request");
} else {
tx.send(InteractiveRequest::SetPIN(new_pin))
.expect("Failed to send PIN-set request");
}
return;
}
_ => {
panic!("Can't happen");
}
}
} else {
println!("Device only supports CTAP1 and can't be managed.");
}
}
Ok(StatusUpdate::DeviceAvailable { dev_info }) => {
num_of_devices += 1;
debug!(
"STATUS: New device #{} available: {}",
num_of_devices, dev_info
);
}
Ok(StatusUpdate::DeviceUnavailable { dev_info }) => {
num_of_devices -= 1;
if num_of_devices <= 0 {
println!("No more devices left. Please plug in a device!");
}
debug!("STATUS: Device became unavailable: {}", dev_info)
}
Ok(StatusUpdate::Success { dev_info }) => {
println!("STATUS: success using device: {}", dev_info);
}
Ok(StatusUpdate::SelectDeviceNotice) => {
println!("STATUS: Please select a device by touching one of them.");
}
Ok(StatusUpdate::DeviceSelected(_dev_info)) => {}
Ok(StatusUpdate::PinUvError(..)) => {
println!("STATUS: Pin Error!");
}
Err(RecvError) => {
println!("STATUS: end");
return;
}
}
}
}
fn main() {
env_logger::init();
let args: Vec<String> = env::args().collect();
let program = args[0].clone();
let mut opts = Options::new();
opts.optflag("x", "no-u2f-usb-hid", "do not enable u2f-usb-hid platforms");
opts.optflag("h", "help", "print this help menu").optopt(
"t",
"timeout",
"timeout in seconds",
"SEC",
);
opts.optflag("h", "help", "print this help menu");
let matches = match opts.parse(&args[1..]) {
Ok(m) => m,
Err(f) => panic!("{}", f.to_string()),
};
if matches.opt_present("help") {
print_usage(&program, opts);
return;
}
let mut manager =
AuthenticatorService::new().expect("The auth service should initialize safely");
if !matches.opt_present("no-u2f-usb-hid") {
manager.add_u2f_usb_hid_platform_transports();
}
let timeout_ms = match matches.opt_get_default::<u64>("timeout", 120) {
Ok(timeout_s) => {
println!("Using {}s as the timeout", &timeout_s);
timeout_s * 1_000
}
Err(e) => {
println!("{e}");
print_usage(&program, opts);
return;
}
};
let (status_tx, status_rx) = channel::<StatusUpdate>();
thread::spawn(move || interactive_status_callback(status_rx));
let (manage_tx, manage_rx) = channel();
let state_callback =
StateCallback::<Result<ResetResult, AuthenticatorError>>::new(Box::new(move |rv| {
manage_tx.send(rv).unwrap();
}));
match manager.manage(timeout_ms, status_tx, state_callback) {
Ok(_) => {
debug!("Started management")
}
Err(e) => {
println!("Error! Failed to start interactive management: {:?}", e)
}
}
let manage_result = manage_rx
.recv()
.expect("Problem receiving, unable to continue");
match manage_result {
Ok(_) => println!("Success!"),
Err(e) => println!("Error! {:?}", e),
};
println!("Done");
}

View file

@ -70,6 +70,9 @@ fn main() {
let (status_tx, status_rx) = channel::<StatusUpdate>();
thread::spawn(move || loop {
match status_rx.recv() {
Ok(StatusUpdate::InteractiveManagement(..)) => {
panic!("STATUS: This can't happen when doing non-interactive usage");
}
Ok(StatusUpdate::DeviceAvailable { dev_info }) => {
println!("STATUS: device available: {dev_info}")
}

View file

@ -3,12 +3,11 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use authenticator::{
authenticatorservice::{
AuthenticatorService, MakeCredentialsOptions, RegisterArgs,
},
authenticatorservice::{AuthenticatorService, RegisterArgs},
ctap2::commands::StatusCode,
ctap2::server::{
PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, Transport, User,
PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty,
ResidentKeyRequirement, Transport, User, UserVerificationRequirement,
},
errors::{AuthenticatorError, CommandError, HIDError},
statecallback::StateCallback,
@ -48,8 +47,8 @@ fn main() {
return;
}
let mut manager = AuthenticatorService::new()
.expect("The auth service should initialize safely");
let mut manager =
AuthenticatorService::new().expect("The auth service should initialize safely");
manager.add_u2f_usb_hid_platform_transports();
@ -75,12 +74,12 @@ fn main() {
challenge.update(challenge_str.as_bytes());
let chall_bytes = challenge.finalize().into();
// TODO(MS): Needs to be added to RegisterArgsCtap2
// let flags = RegisterFlags::empty();
let (status_tx, status_rx) = channel::<StatusUpdate>();
thread::spawn(move || loop {
match status_rx.recv() {
Ok(StatusUpdate::InteractiveManagement(..)) => {
panic!("STATUS: This can't happen when doing non-interactive usage");
}
Ok(StatusUpdate::DeviceAvailable { dev_info }) => {
println!("STATUS: device available: {dev_info}")
}
@ -168,10 +167,8 @@ fn main() {
},
],
exclude_list: vec![],
options: MakeCredentialsOptions {
resident_key: None,
user_verification: None,
},
user_verification_req: UserVerificationRequirement::Preferred,
resident_key_req: ResidentKeyRequirement::Discouraged,
extensions: Default::default(),
pin: None,
use_ctap1_fallback: false,
@ -183,12 +180,8 @@ fn main() {
register_tx.send(rv).unwrap();
}));
if let Err(e) = manager.register(
timeout_ms,
ctap_args.clone(),
status_tx.clone(),
callback,
) {
if let Err(e) = manager.register(timeout_ms, ctap_args.clone(), status_tx.clone(), callback)
{
panic!("Couldn't register: {:?}", e);
};

View file

@ -3,14 +3,11 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
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,
};
pub use crate::ctap2::commands::get_assertion::{GetAssertionExtensions, HmacSecretExtension};
pub use crate::ctap2::commands::make_credentials::MakeCredentialsExtensions;
use crate::ctap2::server::{
PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, User,
PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty,
ResidentKeyRequirement, User, UserVerificationRequirement,
};
use crate::errors::*;
use crate::manager::Manager;
@ -25,7 +22,8 @@ pub struct RegisterArgs {
pub user: User,
pub pub_cred_params: Vec<PublicKeyCredentialParameters>,
pub exclude_list: Vec<PublicKeyCredentialDescriptor>,
pub options: MakeCredentialsOptions,
pub user_verification_req: UserVerificationRequirement,
pub resident_key_req: ResidentKeyRequirement,
pub extensions: MakeCredentialsExtensions,
pub pin: Option<Pin>,
pub use_ctap1_fallback: bool,
@ -37,7 +35,8 @@ pub struct SignArgs {
pub origin: String,
pub relying_party_id: String,
pub allow_list: Vec<PublicKeyCredentialDescriptor>,
pub options: GetAssertionOptions,
pub user_verification_req: UserVerificationRequirement,
pub user_presence_req: bool,
pub extensions: GetAssertionExtensions,
pub pin: Option<Pin>,
pub alternate_rp_id: Option<String>,
@ -85,6 +84,12 @@ pub trait AuthenticatorTransport {
status: Sender<crate::StatusUpdate>,
callback: StateCallback<crate::Result<crate::ResetResult>>,
) -> crate::Result<()>;
fn manage(
&mut self,
timeout: u64,
status: Sender<crate::StatusUpdate>,
callback: StateCallback<crate::Result<crate::ResetResult>>,
) -> crate::Result<()>;
}
pub struct AuthenticatorService {
@ -288,6 +293,39 @@ impl AuthenticatorService {
Ok(())
}
pub fn manage(
&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!(
"Manage called with {} transports, iterable is {}",
self.transports.len(),
iterable_transports.len()
);
for (idx, transport_mutex) in iterable_transports.iter().enumerate() {
let mut transports_to_cancel = iterable_transports.clone();
transports_to_cancel.remove(idx);
debug!("reset transports_to_cancel {}", transports_to_cancel.len());
transport_mutex.lock().unwrap().manage(
timeout,
status.clone(),
clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel),
)?;
}
Ok(())
}
}
////////////////////////////////////////////////////////////////////////
@ -298,7 +336,9 @@ impl AuthenticatorService {
mod tests {
use super::{AuthenticatorService, AuthenticatorTransport, Pin, RegisterArgs, SignArgs};
use crate::consts::{Capability, PARAMETER_SIZE};
use crate::ctap2::server::{RelyingParty, User};
use crate::ctap2::server::{
RelyingParty, ResidentKeyRequirement, User, UserVerificationRequirement,
};
use crate::statecallback::StateCallback;
use crate::{RegisterResult, SignResult, StatusUpdate};
use std::sync::atomic::{AtomicBool, Ordering};
@ -401,6 +441,15 @@ mod tests {
) -> crate::Result<()> {
unimplemented!();
}
fn manage(
&mut self,
_timeout: u64,
_status: Sender<crate::StatusUpdate>,
_callback: StateCallback<crate::Result<crate::ResetResult>>,
) -> crate::Result<()> {
unimplemented!();
}
}
fn mk_challenge() -> [u8; PARAMETER_SIZE] {
@ -432,7 +481,8 @@ mod tests {
},
pub_cred_params: vec![],
exclude_list: vec![],
options: Default::default(),
user_verification_req: UserVerificationRequirement::Preferred,
resident_key_req: ResidentKeyRequirement::Preferred,
extensions: Default::default(),
pin: None,
use_ctap1_fallback: false,
@ -453,7 +503,8 @@ mod tests {
origin: "example.com".to_string(),
relying_party_id: "example.com".to_string(),
allow_list: vec![],
options: Default::default(),
user_verification_req: UserVerificationRequirement::Preferred,
user_presence_req: true,
extensions: Default::default(),
pin: None,
alternate_rp_id: None,
@ -511,7 +562,8 @@ mod tests {
},
pub_cred_params: vec![],
exclude_list: vec![],
options: Default::default(),
user_verification_req: UserVerificationRequirement::Preferred,
resident_key_req: ResidentKeyRequirement::Preferred,
extensions: Default::default(),
pin: None,
use_ctap1_fallback: false,
@ -555,7 +607,8 @@ mod tests {
origin: "example.com".to_string(),
relying_party_id: "example.com".to_string(),
allow_list: vec![],
options: Default::default(),
user_verification_req: UserVerificationRequirement::Preferred,
user_presence_req: true,
extensions: Default::default(),
pin: None,
alternate_rp_id: None,
@ -609,7 +662,8 @@ mod tests {
},
pub_cred_params: vec![],
exclude_list: vec![],
options: Default::default(),
user_verification_req: UserVerificationRequirement::Preferred,
resident_key_req: ResidentKeyRequirement::Preferred,
extensions: Default::default(),
pin: None,
use_ctap1_fallback: false,

View file

@ -362,6 +362,15 @@ pub struct PinUvAuthParam {
pin_auth: Vec<u8>,
}
impl PinUvAuthParam {
pub(crate) fn create_empty() -> Self {
Self {
pin_protocol: PinUvAuthProtocol(Box::new(PinUvAuth1 {})),
pin_auth: vec![],
}
}
}
impl Serialize for PinUvAuthParam {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where

View file

@ -679,7 +679,7 @@ impl AsRef<[u8]> for EncryptedPinToken {
}
}
#[derive(Clone)]
#[derive(Clone, Serialize, Deserialize)]
pub struct Pin(String);
impl fmt::Debug for Pin {

View file

@ -12,7 +12,9 @@ use crate::ctap2::client_data::ClientDataHash;
use crate::ctap2::commands::client_pin::Pin;
use crate::ctap2::commands::get_next_assertion::GetNextAssertion;
use crate::ctap2::commands::make_credentials::UserVerification;
use crate::ctap2::server::{PublicKeyCredentialDescriptor, RelyingPartyWrapper, RpIdHash, User};
use crate::ctap2::server::{
PublicKeyCredentialDescriptor, RelyingPartyWrapper, RpIdHash, User, UserVerificationRequirement,
};
use crate::errors::AuthenticatorError;
use crate::transport::errors::{ApduErrorStatus, HIDError};
use crate::transport::FidoDevice;
@ -218,7 +220,6 @@ pub struct GetAssertion {
// This is used to implement the FIDO AppID extension.
pub(crate) alternate_rp_id: Option<String>,
pub(crate) use_ctap1_fallback: bool,
}
impl GetAssertion {
@ -230,9 +231,8 @@ impl GetAssertion {
extensions: GetAssertionExtensions,
pin: Option<Pin>,
alternate_rp_id: Option<String>,
use_ctap1_fallback: bool,
) -> Result<Self, HIDError> {
Ok(Self {
) -> Self {
Self {
client_data_hash,
rp,
allow_list,
@ -241,8 +241,7 @@ impl GetAssertion {
pin,
pin_uv_auth_param: None,
alternate_rp_id,
use_ctap1_fallback,
})
}
}
}
@ -275,10 +274,6 @@ impl PinUvAuthCommand for GetAssertion {
self.options.user_verification = uv;
}
fn get_uv_option(&mut self) -> Option<bool> {
self.options.user_verification
}
fn get_rp_id(&self) -> Option<&String> {
match &self.rp {
// CTAP1 case: We only have the hash, not the entire RpID
@ -287,24 +282,18 @@ impl PinUvAuthCommand for GetAssertion {
}
}
fn can_skip_user_verification(&mut self, info: &AuthenticatorInfo) -> bool {
fn can_skip_user_verification(
&mut self,
info: &AuthenticatorInfo,
uv_req: UserVerificationRequirement,
) -> bool {
let supports_uv = info.options.user_verification == Some(true);
let pin_configured = info.options.client_pin == Some(true);
let device_protected = supports_uv || pin_configured;
let uv_preferred_or_required = self.get_uv_option() != Some(false);
let uv_discouraged = uv_req == UserVerificationRequirement::Discouraged;
let always_uv = info.options.always_uv == Some(true);
if always_uv || (device_protected && uv_preferred_or_required) {
// If the token is protected AND the RP doesn't specifically discourage UV, we have to use it
self.set_uv_option(Some(true));
false
} else {
// "[..] the Relying Party does not wish to require user verification (e.g., by setting options.userVerification
// to "discouraged" in the WebAuthn API), the platform invokes the authenticatorGetAssertion operation using
// the marshalled input parameters along with an absent "uv" option key."
self.set_uv_option(None);
true
}
!always_uv && (!device_protected || uv_discouraged)
}
}
@ -730,9 +719,7 @@ pub mod test {
Default::default(),
None,
None,
false,
)
.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);
@ -933,9 +920,7 @@ pub mod test {
Default::default(),
None,
None,
false,
)
.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];
@ -1022,9 +1007,7 @@ pub mod test {
Default::default(),
None,
None,
false,
)
.expect("Failed to create GetAssertion");
);
let mut device = Device::new("commands/get_assertion").unwrap(); // not really used (all functions ignore it)
// channel id

View file

@ -62,17 +62,17 @@ fn true_val() -> bool {
true
}
#[derive(Debug, Deserialize, Clone, Eq, PartialEq)]
pub(crate) struct AuthenticatorOptions {
#[derive(Debug, Deserialize, Clone, Eq, PartialEq, Serialize)]
pub struct AuthenticatorOptions {
/// Indicates that the device is attached to the client and therefore cant
/// be removed and used on another client.
#[serde(rename = "plat", default)]
pub(crate) platform_device: bool,
pub platform_device: bool,
/// Indicates that the device is capable of storing keys on the device
/// itself and therefore can satisfy the authenticatorGetAssertion request
/// with allowList parameter not specified or empty.
#[serde(rename = "rk", default)]
pub(crate) resident_key: bool,
pub resident_key: bool,
/// Client PIN:
/// If present and set to true, it indicates that the device is capable of
@ -83,11 +83,11 @@ pub(crate) struct AuthenticatorOptions {
/// PIN from the client.
/// Client PIN is one of the ways to do user verification.
#[serde(rename = "clientPin")]
pub(crate) client_pin: Option<bool>,
pub 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,
pub user_presence: bool,
/// Indicates that the device is capable of verifying the user within
/// itself. For example, devices with UI, biometrics fall into this
@ -107,7 +107,7 @@ pub(crate) struct AuthenticatorOptions {
// 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>,
pub user_verification: Option<bool>,
// ----------------------------------------------------
// CTAP 2.1 options
@ -124,7 +124,7 @@ pub(crate) struct AuthenticatorOptions {
/// getPinUvAuthTokenUsingPinWithPermissions and getPinUvAuthTokenUsingUvWithPermissions
/// subcommands.
#[serde(rename = "pinUvAuthToken")]
pub(crate) pin_uv_auth_token: Option<bool>,
pub pin_uv_auth_token: Option<bool>,
/// If this noMcGaPermissionsWithClientPin is:
/// present and set to true
@ -141,7 +141,7 @@ pub(crate) struct AuthenticatorOptions {
/// Note: noMcGaPermissionsWithClientPin MUST only be present if the
/// clientPin option ID is present.
#[serde(rename = "noMcGaPermissionsWithClientPin")]
pub(crate) no_mc_ga_permissions_with_client_pin: Option<bool>,
pub no_mc_ga_permissions_with_client_pin: Option<bool>,
/// If largeBlobs is:
/// present and set to true
@ -149,7 +149,7 @@ pub(crate) struct AuthenticatorOptions {
/// present and set to false, or absent.
/// The authenticatorLargeBlobs command is NOT supported.
#[serde(rename = "largeBlobs")]
pub(crate) large_blobs: Option<bool>,
pub large_blobs: Option<bool>,
/// Enterprise Attestation feature support:
/// If ep is:
@ -162,7 +162,7 @@ pub(crate) struct AuthenticatorOptions {
/// Absent
/// The Enterprise Attestation feature is NOT supported.
#[serde(rename = "ep")]
pub(crate) ep: Option<bool>,
pub ep: Option<bool>,
/// If bioEnroll is:
/// present and set to true
@ -174,7 +174,7 @@ pub(crate) struct AuthenticatorOptions {
/// absent
/// the authenticatorBioEnrollment commands are NOT supported.
#[serde(rename = "bioEnroll")]
pub(crate) bio_enroll: Option<bool>,
pub bio_enroll: Option<bool>,
/// "FIDO_2_1_PRE" Prototype Credential management support:
/// If userVerificationMgmtPreview is:
@ -187,7 +187,7 @@ pub(crate) struct AuthenticatorOptions {
/// absent
/// the Prototype authenticatorBioEnrollment (0x41) commands are not supported.
#[serde(rename = "userVerificationMgmtPreview")]
pub(crate) user_verification_mgmt_preview: Option<bool>,
pub user_verification_mgmt_preview: Option<bool>,
/// getPinUvAuthTokenUsingUvWithPermissions support for requesting the be permission:
/// This option ID MUST only be present if bioEnroll is also present.
@ -199,7 +199,7 @@ pub(crate) struct AuthenticatorOptions {
/// requesting the be permission when invoking getPinUvAuthTokenUsingUvWithPermissions
/// is NOT supported.
#[serde(rename = "uvBioEnroll")]
pub(crate) uv_bio_enroll: Option<bool>,
pub uv_bio_enroll: Option<bool>,
/// authenticatorConfig command support:
/// If authnrCfg is:
@ -208,7 +208,7 @@ pub(crate) struct AuthenticatorOptions {
/// present and set to false, or absent.
/// the authenticatorConfig command is NOT supported.
#[serde(rename = "authnrCfg")]
pub(crate) authnr_cfg: Option<bool>,
pub authnr_cfg: Option<bool>,
/// getPinUvAuthTokenUsingUvWithPermissions support for requesting the acfg permission:
/// This option ID MUST only be present if authnrCfg is also present.
@ -220,7 +220,7 @@ pub(crate) struct AuthenticatorOptions {
/// requesting the acfg permission when invoking getPinUvAuthTokenUsingUvWithPermissions
/// is NOT supported.
#[serde(rename = "uvAcfg")]
pub(crate) uv_acfg: Option<bool>,
pub uv_acfg: Option<bool>,
/// Credential management support:
/// If credMgmt is:
@ -229,7 +229,7 @@ pub(crate) struct AuthenticatorOptions {
/// present and set to false, or absent.
/// the authenticatorCredentialManagement command is NOT supported.
#[serde(rename = "credMgmt")]
pub(crate) cred_mgmt: Option<bool>,
pub cred_mgmt: Option<bool>,
/// "FIDO_2_1_PRE" Prototype Credential management support:
/// If credentialMgmtPreview is:
@ -238,7 +238,7 @@ pub(crate) struct AuthenticatorOptions {
/// present and set to false, or absent.
/// the Prototype authenticatorCredentialManagement (0x41) command is NOT supported.
#[serde(rename = "credentialMgmtPreview")]
pub(crate) credential_mgmt_preview: Option<bool>,
pub credential_mgmt_preview: Option<bool>,
/// Support for the Set Minimum PIN Length feature.
/// If setMinPINLength is:
@ -248,7 +248,7 @@ pub(crate) struct AuthenticatorOptions {
/// the setMinPINLength subcommand is NOT supported.
/// Note: setMinPINLength MUST only be present if the clientPin option ID is present.
#[serde(rename = "setMinPINLength")]
pub(crate) set_min_pin_length: Option<bool>,
pub set_min_pin_length: Option<bool>,
/// Support for making non-discoverable credentials without requiring User Verification.
/// If makeCredUvNotRqd is:
@ -261,7 +261,7 @@ pub(crate) struct AuthenticatorOptions {
/// for the authenticatorMakeCredential command.
/// Authenticators SHOULD include this option with the value true.
#[serde(rename = "makeCredUvNotRqd")]
pub(crate) make_cred_uv_not_rqd: Option<bool>,
pub make_cred_uv_not_rqd: Option<bool>,
/// Support for the Always Require User Verification feature:
/// If alwaysUv is
@ -274,7 +274,7 @@ pub(crate) struct AuthenticatorOptions {
/// Note: If the alwaysUv option ID is present and true the authenticator MUST set the value
/// of makeCredUvNotRqd to false.
#[serde(rename = "alwaysUv")]
pub(crate) always_uv: Option<bool>,
pub always_uv: Option<bool>,
}
impl Default for AuthenticatorOptions {
@ -312,30 +312,30 @@ pub enum AuthenticatorVersion {
FIDO_2_1,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
pub struct AuthenticatorInfo {
pub(crate) versions: Vec<AuthenticatorVersion>,
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>,
pub versions: Vec<AuthenticatorVersion>,
pub extensions: Vec<String>,
pub aaguid: AAGuid,
pub options: AuthenticatorOptions,
pub max_msg_size: Option<usize>,
pub 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>>,
pub max_credential_count_in_list: Option<usize>,
pub max_credential_id_length: Option<usize>,
pub transports: Option<Vec<String>>,
pub algorithms: Option<Vec<PublicKeyCredentialParameters>>,
pub max_ser_large_blob_array: Option<u64>,
pub force_pin_change: Option<bool>,
pub min_pin_length: Option<u64>,
pub firmware_version: Option<u64>,
pub max_cred_blob_length: Option<u64>,
pub max_rpids_for_set_min_pin_length: Option<u64>,
pub preferred_platform_uv_attempts: Option<u64>,
pub uv_modality: Option<u64>,
pub certifications: Option<BTreeMap<String, u64>>,
pub remaining_discoverable_credentials: Option<u64>,
pub vendor_prototype_config_commands: Option<Vec<u64>>,
}
impl AuthenticatorInfo {

View file

@ -17,7 +17,7 @@ use crate::ctap2::commands::client_pin::Pin;
use crate::ctap2::commands::get_assertion::CheckKeyHandle;
use crate::ctap2::server::{
PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty,
RelyingPartyWrapper, RpIdHash, User,
RelyingPartyWrapper, RpIdHash, User, UserVerificationRequirement,
};
use crate::errors::AuthenticatorError;
use crate::transport::{
@ -174,7 +174,6 @@ pub struct MakeCredentials {
pub(crate) pin: Option<Pin>,
pub(crate) pin_uv_auth_param: Option<PinUvAuthParam>,
pub(crate) enterprise_attestation: Option<u64>,
pub(crate) use_ctap1_fallback: bool,
}
impl MakeCredentials {
@ -188,9 +187,8 @@ impl MakeCredentials {
options: MakeCredentialsOptions,
extensions: MakeCredentialsExtensions,
pin: Option<Pin>,
use_ctap1_fallback: bool,
) -> Result<Self, HIDError> {
Ok(Self {
) -> Self {
Self {
client_data_hash,
rp,
user,
@ -201,8 +199,7 @@ impl MakeCredentials {
pin,
pin_uv_auth_param: None,
enterprise_attestation: None,
use_ctap1_fallback,
})
}
}
}
@ -235,10 +232,6 @@ impl PinUvAuthCommand for MakeCredentials {
self.options.user_verification = uv;
}
fn get_uv_option(&mut self) -> Option<bool> {
self.options.user_verification
}
fn get_rp_id(&self) -> Option<&String> {
match &self.rp {
// CTAP1 case: We only have the hash, not the entire RpID
@ -247,7 +240,11 @@ impl PinUvAuthCommand for MakeCredentials {
}
}
fn can_skip_user_verification(&mut self, info: &AuthenticatorInfo) -> bool {
fn can_skip_user_verification(
&mut self,
info: &AuthenticatorInfo,
uv_req: UserVerificationRequirement,
) -> bool {
// TODO(MS): Handle here the case where we NEED a UV, the device supports PINs, but hasn't set a PIN.
// For this, the user has to be prompted to set a PIN first (see https://github.com/mozilla/authenticator-rs/issues/223)
@ -260,27 +257,13 @@ impl PinUvAuthCommand for MakeCredentials {
// For CTAP2.0, UV is always required when doing MakeCredential
let always_uv = info.options.always_uv == Some(true)
|| info.max_supported_version() == AuthenticatorVersion::FIDO_2_0;
let uv_discouraged = self.get_uv_option() == Some(false);
let uv_discouraged = uv_req == UserVerificationRequirement::Discouraged;
// CTAP 2.1 authenticators can allow MakeCredential without PinUvAuth,
// but that is only relevant, if RP also discourages UV.
let can_make_cred_without_uv = make_cred_uv_not_required && uv_discouraged;
if always_uv || (device_protected && !can_make_cred_without_uv) {
// If the token is protected, we have to require UV anyways
self.set_uv_option(Some(true));
false
} else {
// "[..] the Relying Party wants to create a non-discoverable credential and not require user verification
// (e.g., by setting options.authenticatorSelection.userVerification to "discouraged" in the WebAuthn API),
// the platform invokes the authenticatorMakeCredential operation using the marshalled input parameters along
// with the "uv" option key set to false and terminate these steps."
// Note: This is basically a no-op right now, since we use `get_uv_option() == Some(false)`, to determine if
// the RP is discouraging UV. But we may change that part of the API in the future, so better to be
// explicit here.
self.set_uv_option(Some(false));
true
}
!always_uv && (!device_protected || can_make_cred_without_uv)
}
}
@ -353,14 +336,6 @@ impl RequestCtap1 for MakeCredentials {
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()
@ -469,7 +444,7 @@ impl RequestCtap2 for MakeCredentials {
}
pub(crate) fn dummy_make_credentials_cmd() -> Result<MakeCredentials, HIDError> {
MakeCredentials::new(
let mut req = MakeCredentials::new(
CollectedClientData {
webauthn_type: WebauthnType::Create,
challenge: Challenge::new(vec![0, 1, 2, 3, 4]),
@ -477,8 +452,7 @@ pub(crate) fn dummy_make_credentials_cmd() -> Result<MakeCredentials, HIDError>
cross_origin: false,
token_binding: None,
}
.hash()
.expect("failed to serialize client data"),
.hash()?,
RelyingPartyWrapper::Data(RelyingParty {
id: String::from("make.me.blink"),
..Default::default()
@ -495,8 +469,12 @@ pub(crate) fn dummy_make_credentials_cmd() -> Result<MakeCredentials, HIDError>
MakeCredentialsOptions::default(),
MakeCredentialsExtensions::default(),
None,
false,
)
);
// Using a zero-length pinAuth will trigger the device to blink.
// For CTAP1, this gets ignored anyways and we do a 'normal' register
// command, which also just blinks.
req.pin_uv_auth_param = Some(PinUvAuthParam::create_empty());
Ok(req)
}
#[cfg(test)]
@ -648,9 +626,7 @@ pub mod test {
},
Default::default(),
None,
false,
)
.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
@ -708,9 +684,7 @@ pub mod test {
},
Default::default(),
None,
false,
)
.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

View file

@ -2,6 +2,7 @@ use crate::crypto::{CryptoError, PinUvAuthToken};
use crate::ctap2::commands::client_pin::{GetPinRetries, GetUvRetries, Pin, PinError};
use crate::ctap2::commands::get_info::AuthenticatorInfo;
use crate::ctap2::server::UserVerificationRequirement;
use crate::errors::AuthenticatorError;
use crate::transport::errors::{ApduErrorStatus, HIDError};
use crate::transport::FidoDevice;
@ -121,9 +122,12 @@ pub(crate) trait PinUvAuthCommand: RequestCtap2 {
pin_uv_auth_token: Option<PinUvAuthToken>,
) -> Result<(), AuthenticatorError>;
fn set_uv_option(&mut self, uv: Option<bool>);
fn get_uv_option(&mut self) -> Option<bool>;
fn get_rp_id(&self) -> Option<&String>;
fn can_skip_user_verification(&mut self, info: &AuthenticatorInfo) -> bool;
fn can_skip_user_verification(
&mut self,
info: &AuthenticatorInfo,
uv_req: UserVerificationRequirement,
) -> bool;
}
pub(crate) fn repackage_pin_errors<D: FidoDevice>(

View file

@ -300,6 +300,20 @@ impl From<&KeyHandle> for PublicKeyCredentialDescriptor {
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ResidentKeyRequirement {
Discouraged,
Preferred,
Required,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum UserVerificationRequirement {
Discouraged,
Preferred,
Required,
}
#[cfg(test)]
mod test {
use super::{

View file

@ -13,8 +13,12 @@ use std::sync::mpsc;
#[derive(Debug)]
pub enum UnsupportedOption {
MaxPinLength,
EmptyAllowList,
HmacSecret,
MaxPinLength,
PubCredParams,
ResidentKey,
UserVerification,
}
#[derive(Debug)]

View file

@ -40,6 +40,7 @@ pub use ctap2::attestation::AttestationObject;
pub use ctap2::client_data::CollectedClientData;
pub use ctap2::commands::client_pin::{Pin, PinError};
pub use ctap2::commands::get_assertion::Assertion;
pub use ctap2::commands::get_info::AuthenticatorInfo;
pub use ctap2::AssertionObject;
pub mod errors;

View file

@ -4,11 +4,6 @@
use crate::authenticatorservice::AuthenticatorTransport;
use crate::authenticatorservice::{RegisterArgs, SignArgs};
use crate::ctap2::client_data::ClientDataHash;
use crate::ctap2::commands::get_assertion::GetAssertion;
use crate::ctap2::commands::make_credentials::MakeCredentials;
use crate::ctap2::server::{RelyingParty, RelyingPartyWrapper};
use crate::errors::*;
use crate::statecallback::StateCallback;
use crate::statemachine::StateMachine;
@ -21,13 +16,13 @@ use std::time::Duration;
enum QueueAction {
Register {
timeout: u64,
make_credentials: MakeCredentials,
register_args: RegisterArgs,
status: Sender<crate::StatusUpdate>,
callback: StateCallback<crate::Result<crate::RegisterResult>>,
},
Sign {
timeout: u64,
get_assertion: GetAssertion,
sign_args: SignArgs,
status: Sender<crate::StatusUpdate>,
callback: StateCallback<crate::Result<crate::SignResult>>,
},
@ -43,6 +38,11 @@ enum QueueAction {
status: Sender<crate::StatusUpdate>,
callback: StateCallback<crate::Result<crate::ResetResult>>,
},
InteractiveManagement {
timeout: u64,
status: Sender<crate::StatusUpdate>,
callback: StateCallback<crate::Result<crate::ResetResult>>,
},
}
pub struct Manager {
@ -62,22 +62,22 @@ impl Manager {
match rx.recv_timeout(Duration::from_millis(50)) {
Ok(QueueAction::Register {
timeout,
make_credentials,
register_args,
status,
callback,
}) => {
// This must not block, otherwise we can't cancel.
sm.register(timeout, make_credentials, status, callback);
sm.register(timeout, register_args, status, callback);
}
Ok(QueueAction::Sign {
timeout,
get_assertion,
sign_args,
status,
callback,
}) => {
// This must not block, otherwise we can't cancel.
sm.sign(timeout, get_assertion, status, callback);
sm.sign(timeout, sign_args, status, callback);
}
Ok(QueueAction::Cancel) => {
@ -105,6 +105,15 @@ impl Manager {
sm.set_pin(timeout, new_pin, status, callback);
}
Ok(QueueAction::InteractiveManagement {
timeout,
status,
callback,
}) => {
// Manage token interactively
sm.manage(timeout, status, callback);
}
Err(RecvTimeoutError::Disconnected) => {
break;
}
@ -131,26 +140,13 @@ impl AuthenticatorTransport for Manager {
fn register(
&mut self,
timeout: u64,
args: RegisterArgs,
register_args: RegisterArgs,
status: Sender<crate::StatusUpdate>,
callback: StateCallback<crate::Result<crate::RegisterResult>>,
) -> Result<(), AuthenticatorError> {
let make_credentials = MakeCredentials::new(
ClientDataHash(args.client_data_hash),
RelyingPartyWrapper::Data(args.relying_party),
Some(args.user),
args.pub_cred_params,
args.exclude_list,
args.options,
args.extensions,
args.pin,
args.use_ctap1_fallback,
// pin_auth will be filled in Statemachine, once we have a device
)?;
let action = QueueAction::Register {
timeout,
make_credentials,
register_args,
status,
callback,
};
@ -160,28 +156,13 @@ impl AuthenticatorTransport for Manager {
fn sign(
&mut self,
timeout: u64,
args: SignArgs,
sign_args: SignArgs,
status: Sender<crate::StatusUpdate>,
callback: StateCallback<crate::Result<crate::SignResult>>,
) -> crate::Result<()> {
let get_assertion = GetAssertion::new(
ClientDataHash(args.client_data_hash),
RelyingPartyWrapper::Data(RelyingParty {
id: args.relying_party_id,
name: None,
icon: None,
}),
args.allow_list,
args.options,
args.extensions,
args.pin,
args.alternate_rp_id,
args.use_ctap1_fallback,
)?;
let action = QueueAction::Sign {
timeout,
get_assertion,
sign_args,
status,
callback,
};
@ -221,4 +202,17 @@ impl AuthenticatorTransport for Manager {
callback,
})?)
}
fn manage(
&mut self,
timeout: u64,
status: Sender<crate::StatusUpdate>,
callback: StateCallback<crate::Result<crate::ResetResult>>,
) -> Result<(), AuthenticatorError> {
Ok(self.tx.send(QueueAction::InteractiveManagement {
timeout,
status,
callback,
})?)
}
}

View file

@ -2,19 +2,26 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use crate::authenticatorservice::{RegisterArgs, SignArgs};
use crate::consts::PARAMETER_SIZE;
use crate::crypto::COSEAlgorithm;
use crate::ctap2::client_data::ClientDataHash;
use crate::ctap2::commands::client_pin::{
ChangeExistingPin, Pin, PinError, PinUvAuthTokenPermission, SetNewPin,
};
use crate::ctap2::commands::get_assertion::{GetAssertion, GetAssertionResult};
use crate::ctap2::commands::make_credentials::{MakeCredentials, MakeCredentialsResult};
use crate::ctap2::commands::get_assertion::{
GetAssertion, GetAssertionOptions, GetAssertionResult,
};
use crate::ctap2::commands::make_credentials::{
MakeCredentials, MakeCredentialsOptions, MakeCredentialsResult,
};
use crate::ctap2::commands::reset::Reset;
use crate::ctap2::commands::{
repackage_pin_errors, CommandError, PinUvAuthCommand, PinUvAuthResult, Request, StatusCode,
};
use crate::ctap2::server::{
PublicKeyCredentialDescriptor, RelyingParty, RelyingPartyWrapper, RpIdHash,
PublicKeyCredentialDescriptor, RelyingParty, RelyingPartyWrapper, ResidentKeyRequirement,
RpIdHash, UserVerificationRequirement,
};
use crate::errors::{self, AuthenticatorError, UnsupportedOption};
use crate::statecallback::StateCallback;
@ -26,10 +33,10 @@ 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, AuthenticatorTransports, KeyHandle, RegisterFlags, RegisterResult, SignFlags,
SignResult, StatusPinUv, StatusUpdate,
send_status, AuthenticatorTransports, InteractiveRequest, KeyHandle, RegisterFlags,
RegisterResult, SignFlags, SignResult, StatusPinUv, StatusUpdate,
};
use std::sync::mpsc::{channel, Sender};
use std::sync::mpsc::{channel, RecvError, RecvTimeoutError, Sender};
use std::thread;
use std::time::Duration;
@ -75,6 +82,7 @@ impl StateMachine {
fn init_and_select(
info: DeviceBuildParameters,
selector: &Sender<DeviceSelectorEvent>,
status: &Sender<crate::StatusUpdate>,
ctap2_only: bool,
keep_alive: &dyn Fn() -> bool,
) -> Option<Device> {
@ -101,22 +109,16 @@ impl StateMachine {
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)))
.send(DeviceSelectorEvent::ImAToken((dev.id(), tx)))
.ok()?;
send_status(
status,
crate::StatusUpdate::DeviceAvailable {
dev_info: dev.get_device_info(),
},
);
// We can be cancelled from the user (through keep_alive()) or from the device selector
// (through a DeviceCommand::Cancel on rx). We'll combine those signals into a single
@ -125,29 +127,50 @@ impl StateMachine {
// Blocking recv. DeviceSelector will tell us what to do
match rx.recv() {
Ok(DeviceCommand::Blink) => match dev.block_and_blink(&keep_blinking) {
BlinkResult::DeviceSelected => {
// User selected us. Let DeviceSelector know, so it can cancel all other
// outstanding open blink-requests.
selector
.send(DeviceSelectorEvent::SelectedToken(dev.id()))
.ok()?;
Ok(DeviceCommand::Blink) => {
// Inform the user that there are multiple devices available.
// NOTE: We'll send this once per device, so the recipient should be prepared
// to receive this message multiple times.
send_status(status, crate::StatusUpdate::SelectDeviceNotice);
match dev.block_and_blink(&keep_blinking) {
BlinkResult::DeviceSelected => {
// User selected us. Let DeviceSelector know, so it can cancel all other
// outstanding open blink-requests.
selector
.send(DeviceSelectorEvent::SelectedToken(dev.id()))
.ok()?;
send_status(
status,
crate::StatusUpdate::DeviceSelected(dev.get_device_info()),
);
}
BlinkResult::Cancelled => {
info!("Device {:?} was not selected", dev.id());
return None;
}
}
BlinkResult::Cancelled => {
info!("Device {:?} was not selected", dev.id());
return None;
}
},
}
Ok(DeviceCommand::Cancel) => {
info!("Device {:?} was not selected", dev.id());
return None;
}
Ok(DeviceCommand::Removed) => {
info!("Device {:?} was removed", dev.id());
send_status(
status,
crate::StatusUpdate::DeviceUnavailable {
dev_info: dev.get_device_info(),
},
);
return None;
}
Ok(DeviceCommand::Continue) => {
// Just continue
send_status(
status,
crate::StatusUpdate::DeviceSelected(dev.get_device_info()),
);
}
Err(_) => {
warn!("Error when trying to receive messages from DeviceSelector! Exiting.");
@ -178,9 +201,9 @@ impl StateMachine {
}
match rx.recv() {
Ok(pin) => Ok(pin),
Err(_) => {
Err(RecvError) => {
// recv() can only fail, if the other side is dropping the Sender.
error!("Callback dropped the channel. Aborting.");
info!("Callback dropped the channel. Aborting.");
callback.call(Err(AuthenticatorError::CancelledByUser));
Err(())
}
@ -195,7 +218,23 @@ impl StateMachine {
dev: &mut Device,
permission: PinUvAuthTokenPermission,
skip_uv: bool,
uv_req: UserVerificationRequirement,
) -> Result<PinUvAuthResult, AuthenticatorError> {
// CTAP 2.1 is very specific that the request should either include pinUvAuthParam
// OR uv=true, but not both at the same time. We now have to decide which (if either)
// to send. We may omit both values. Will never send an explicit uv=false, because
// a) this is the default, and
// b) some CTAP 2.0 authenticators return UnsupportedOption when uv=false.
// We ensure both pinUvAuthParam and uv are not set to start.
cmd.set_pin_uv_auth_param(None)?;
cmd.set_uv_option(None);
// CTAP1/U2F-only devices do not support user verification, so we skip it
if !dev.supports_ctap2() {
return Ok(PinUvAuthResult::DeviceIsCtap1);
}
let info = dev
.get_authenticator_info()
.ok_or(AuthenticatorError::HIDError(HIDError::DeviceNotInitialized))?;
@ -213,16 +252,30 @@ impl StateMachine {
// Check if the combination of device-protection and request-options
// are allowing for 'discouraged', meaning no auth required.
if cmd.can_skip_user_verification(info) {
if cmd.can_skip_user_verification(info, uv_req) {
return Ok(PinUvAuthResult::NoAuthRequired);
}
// Device does not support any auth-method
if !pin_configured && !supports_uv {
// We'll send it to the device anyways, and let it error out (or magically work)
// Device does not support any (remaining) auth-method
if (skip_uv || !supports_uv) && !supports_pin {
if supports_uv && uv_req == UserVerificationRequirement::Required {
// We should always set the uv option in the Required case, but the CTAP 2.1 spec
// says 'Platforms MUST NOT include the "uv" option key if the authenticator does
// not support built-in user verification.' This is to work around some CTAP 2.0
// authenticators which incorrectly error out with CTAP2_ERR_UNSUPPORTED_OPTION
// when the "uv" option is set. The RP that requested UV will (hopefully) reject our
// response in the !supports_uv case.
cmd.set_uv_option(Some(true));
}
return Ok(PinUvAuthResult::NoAuthTypeSupported);
}
// Device supports PINs, but a PIN is not configured. Signal that we
// can complete the operation if the user sets a PIN first.
if (skip_uv || !supports_uv) && !pin_configured {
return Err(AuthenticatorError::PinError(PinError::PinNotSet));
}
let (res, pin_auth_token) = if info.options.pin_uv_auth_token == Some(true) {
if !skip_uv && supports_uv {
// CTAP 2.1 - UV
@ -232,8 +285,12 @@ impl StateMachine {
PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions,
pin_auth_token,
)
} else if supports_pin && pin_configured {
} else {
// CTAP 2.1 - PIN
// We did not take the `!skip_uv && supports_uv` branch, so we have
// `(skip_uv || !supports_uv)`. Moreover we did not exit early in the
// `(skip_uv || !supports_uv) && !pin_configured` case. So we have
// `pin_configured`.
let pin_auth_token = dev.get_pin_uv_auth_token_using_pin_with_permissions(
cmd.pin(),
permission,
@ -243,8 +300,6 @@ impl StateMachine {
PinUvAuthResult::SuccessGetPinUvAuthTokenUsingPinWithPermissions,
pin_auth_token,
)
} else {
return Ok(PinUvAuthResult::NoAuthTypeSupported);
}
} else {
// CTAP 2.0 fallback
@ -257,6 +312,8 @@ impl StateMachine {
if info.supports_hmac_secret() {
let _shared_secret = dev.establish_shared_secret()?;
}
// CTAP 2.1, Section 6.1.1, Step 1.1.2.1.2.
cmd.set_uv_option(Some(true));
return Ok(PinUvAuthResult::UsingInternalUv);
}
@ -266,10 +323,6 @@ impl StateMachine {
let pin_auth_token = pin_auth_token.map_err(|e| repackage_pin_errors(dev, e))?;
cmd.set_pin_uv_auth_param(Some(pin_auth_token))?;
// 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
cmd.set_uv_option(None);
Ok(res)
}
@ -286,22 +339,15 @@ impl StateMachine {
dev: &mut Device,
mut skip_uv: bool,
permission: PinUvAuthTokenPermission,
uv_req: UserVerificationRequirement,
status: &Sender<StatusUpdate>,
callback: &StateCallback<crate::Result<U>>,
alive: &dyn Fn() -> bool,
) -> Result<PinUvAuthResult, ()> {
// Starting from a blank slate.
cmd.set_pin_uv_auth_param(None).map_err(|_| ())?;
// CTAP1/U2F-only devices do not support PinUvAuth, so we skip it
if !dev.supports_ctap2() {
return Ok(PinUvAuthResult::DeviceIsCtap1);
}
while alive() {
debug!("-----------------------------------------------------------------");
debug!("Getting pinUvAuthParam");
match Self::get_pin_uv_auth_param(cmd, dev, permission, skip_uv) {
match Self::get_pin_uv_auth_param(cmd, dev, permission, skip_uv, uv_req) {
Ok(r) => {
return Ok(r);
}
@ -378,21 +424,23 @@ impl StateMachine {
pub fn register(
&mut self,
timeout: u64,
params: MakeCredentials,
args: RegisterArgs,
status: Sender<crate::StatusUpdate>,
callback: StateCallback<crate::Result<crate::RegisterResult>>,
) {
if params.use_ctap1_fallback {
if args.use_ctap1_fallback {
/* Firefox uses this when security.webauthn.ctap2 is false. */
let mut flags = RegisterFlags::empty();
if params.options.resident_key == Some(true) {
if args.resident_key_req == ResidentKeyRequirement::Required {
flags |= RegisterFlags::REQUIRE_RESIDENT_KEY;
}
if params.options.user_verification == Some(true) {
if args.user_verification_req == UserVerificationRequirement::Required {
flags |= RegisterFlags::REQUIRE_USER_VERIFICATION;
}
let application = params.rp.hash().0.to_vec();
let key_handles = params
let rp = RelyingPartyWrapper::Data(args.relying_party);
let application = rp.hash().as_ref().to_vec();
let key_handles = args
.exclude_list
.iter()
.map(|cred_desc| KeyHandle {
@ -400,7 +448,7 @@ impl StateMachine {
transports: AuthenticatorTransports::empty(),
})
.collect();
let challenge = params.client_data_hash;
let challenge = ClientDataHash(args.client_data_hash);
self.legacy_register(
flags,
@ -422,7 +470,7 @@ impl StateMachine {
cbc.clone(),
status,
move |info, selector, status, alive| {
let mut dev = match Self::init_and_select(info, &selector, false, alive) {
let mut dev = match Self::init_and_select(info, &selector, &status, false, alive) {
None => {
return;
}
@ -430,33 +478,72 @@ impl StateMachine {
};
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(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();
// 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() {
// We need a copy of the arguments for this device
let args = args.clone();
let mut options = MakeCredentialsOptions::default();
if let Some(info) = dev.get_authenticator_info() {
// Check if extensions have been requested that are not supported by the device
if let Some(true) = args.extensions.hmac_secret {
if !info.supports_hmac_secret() {
callback.call(Err(AuthenticatorError::UnsupportedOption(
UnsupportedOption::HmacSecret,
)));
return;
}
}
// Set options based on the arguments and the device info.
// The user verification option will be set in `determine_puap_if_needed`.
options.resident_key = match args.resident_key_req {
ResidentKeyRequirement::Required => Some(true),
ResidentKeyRequirement::Preferred => {
// Use a resident key if the authenticator supports it
Some(info.options.resident_key)
}
ResidentKeyRequirement::Discouraged => Some(false),
}
} else {
// Check that the request can be processed by a CTAP1 device.
// See CTAP 2.1 Section 10.2. Some additional checks are performed in
// MakeCredentials::RequestCtap1
if args.resident_key_req == ResidentKeyRequirement::Required {
callback.call(Err(AuthenticatorError::UnsupportedOption(
UnsupportedOption::ResidentKey,
)));
return;
}
if args.user_verification_req == UserVerificationRequirement::Required {
callback.call(Err(AuthenticatorError::UnsupportedOption(
UnsupportedOption::UserVerification,
)));
return;
}
if !args
.pub_cred_params
.iter()
.any(|x| x.alg == COSEAlgorithm::ES256)
{
callback.call(Err(AuthenticatorError::UnsupportedOption(
UnsupportedOption::PubCredParams,
)));
return;
}
}
let mut makecred = MakeCredentials::new(
ClientDataHash(args.client_data_hash),
RelyingPartyWrapper::Data(args.relying_party),
Some(args.user),
args.pub_cred_params,
args.exclude_list,
options,
args.extensions,
args.pin,
);
let mut skip_uv = false;
while alive() {
let pin_uv_auth_result = match Self::determine_puap_if_needed(
@ -464,6 +551,7 @@ impl StateMachine {
&mut dev,
skip_uv,
PinUvAuthTokenPermission::MakeCredential,
args.user_verification_req,
&status,
&callback,
alive,
@ -552,29 +640,30 @@ impl StateMachine {
pub fn sign(
&mut self,
timeout: u64,
params: GetAssertion,
args: SignArgs,
status: Sender<crate::StatusUpdate>,
callback: StateCallback<crate::Result<crate::SignResult>>,
) {
if params.use_ctap1_fallback {
if args.use_ctap1_fallback {
/* Firefox uses this when security.webauthn.ctap2 is false. */
let flags = match params.options.user_verification {
Some(true) => SignFlags::REQUIRE_USER_VERIFICATION,
_ => SignFlags::empty(),
};
let mut app_ids = vec![params.rp.hash().0.to_vec()];
if let Some(app_id) = params.alternate_rp_id {
app_ids.push(
RelyingPartyWrapper::Data(RelyingParty {
id: app_id,
..Default::default()
})
.hash()
.0
.to_vec(),
);
let mut flags = SignFlags::empty();
if args.user_verification_req == UserVerificationRequirement::Required {
flags |= SignFlags::REQUIRE_USER_VERIFICATION;
}
let key_handles = params
let mut app_ids = vec![];
let rp_id = RelyingPartyWrapper::Data(RelyingParty {
id: args.relying_party_id,
..Default::default()
});
app_ids.push(rp_id.hash().as_ref().to_vec());
if let Some(app_id) = args.alternate_rp_id {
let app_id = RelyingPartyWrapper::Data(RelyingParty {
id: app_id,
..Default::default()
});
app_ids.push(app_id.hash().as_ref().to_vec());
}
let key_handles = args
.allow_list
.iter()
.map(|cred_desc| KeyHandle {
@ -582,7 +671,7 @@ impl StateMachine {
transports: AuthenticatorTransports::empty(),
})
.collect();
let challenge = params.client_data_hash;
let challenge = ClientDataHash(args.client_data_hash);
self.legacy_sign(
flags,
@ -605,7 +694,7 @@ impl StateMachine {
callback.clone(),
status,
move |info, selector, status, alive| {
let mut dev = match Self::init_and_select(info, &selector, false, alive) {
let mut dev = match Self::init_and_select(info, &selector, &status, false, alive) {
None => {
return;
}
@ -613,27 +702,61 @@ impl StateMachine {
};
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();
// 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;
}
// We need a copy of the arguments for this device
let args = args.clone();
if let Some(info) = dev.get_authenticator_info() {
// Check if extensions have been requested that are not supported by the device
if args.extensions.hmac_secret.is_some() && !info.supports_hmac_secret() {
callback.call(Err(AuthenticatorError::UnsupportedOption(
UnsupportedOption::HmacSecret,
)));
return;
}
} else {
// Check that the request can be processed by a CTAP1 device.
// See CTAP 2.1 Section 10.3. Some additional checks are performed in
// GetAssertion::RequestCtap1
if args.user_verification_req == UserVerificationRequirement::Required {
callback.call(Err(AuthenticatorError::UnsupportedOption(
UnsupportedOption::UserVerification,
)));
return;
}
if args.allow_list.is_empty() {
callback.call(Err(AuthenticatorError::UnsupportedOption(
UnsupportedOption::EmptyAllowList,
)));
return;
}
}
let mut get_assertion = GetAssertion::new(
ClientDataHash(args.client_data_hash),
RelyingPartyWrapper::Data(RelyingParty {
id: args.relying_party_id,
name: None,
icon: None,
}),
args.allow_list,
GetAssertionOptions {
user_presence: Some(args.user_presence_req),
user_verification: None,
},
args.extensions,
args.pin,
args.alternate_rp_id,
);
let mut skip_uv = false;
while alive() {
let pin_uv_auth_result = match Self::determine_puap_if_needed(
&mut getassertion,
&mut get_assertion,
&mut dev,
skip_uv,
PinUvAuthTokenPermission::GetAssertion,
args.user_verification_req,
&status,
&callback,
alive,
@ -645,7 +768,7 @@ impl StateMachine {
};
// Third, use the shared secret in the extensions, if requested
if let Some(extension) = getassertion.extensions.hmac_secret.as_mut() {
if let Some(extension) = get_assertion.extensions.hmac_secret.as_mut() {
if let Some(secret) = dev.get_shared_secret() {
match extension.calculate(secret) {
Ok(x) => x,
@ -658,20 +781,20 @@ impl StateMachine {
}
debug!("------------------------------------------------------------------");
debug!("{getassertion:?} using {pin_uv_auth_result:?}");
debug!("{get_assertion:?} using {pin_uv_auth_result:?}");
debug!("------------------------------------------------------------------");
let mut resp = dev.send_msg_cancellable(&getassertion, alive);
let mut resp = dev.send_msg_cancellable(&get_assertion, alive);
if resp.is_err() {
// Retry with a different RP ID if one was supplied. This is intended to be
// used with the AppID provided in the WebAuthn FIDO AppID extension.
if let Some(alternate_rp_id) = getassertion.alternate_rp_id {
getassertion.rp = RelyingPartyWrapper::Data(RelyingParty {
if let Some(alternate_rp_id) = get_assertion.alternate_rp_id {
get_assertion.rp = RelyingPartyWrapper::Data(RelyingParty {
id: alternate_rp_id,
..Default::default()
});
getassertion.alternate_rp_id = None;
resp = dev.send_msg_cancellable(&getassertion, alive);
get_assertion.alternate_rp_id = None;
resp = dev.send_msg_cancellable(&get_assertion, alive);
}
}
if resp.is_ok() {
@ -754,6 +877,45 @@ impl StateMachine {
}
}
pub fn reset_helper(
dev: &mut Device,
selector: Sender<DeviceSelectorEvent>,
status: Sender<crate::StatusUpdate>,
callback: StateCallback<crate::Result<crate::ResetResult>>,
keep_alive: &dyn Fn() -> bool,
) {
let reset = Reset {};
info!("Device {:?} continues with the reset process", dev.id());
debug!("------------------------------------------------------------------");
debug!("{:?}", reset);
debug!("------------------------------------------------------------------");
let resp = dev.send_cbor_cancellable(&reset, keep_alive);
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)));
}
}
}
pub fn reset(
&mut self,
timeout: u64,
@ -769,52 +931,112 @@ impl StateMachine {
callback.clone(),
status,
move |info, selector, status, alive| {
let reset = Reset {};
let mut dev = match Self::init_and_select(info, &selector, true, alive) {
let mut dev = match Self::init_and_select(info, &selector, &status, true, alive) {
None => {
return;
}
Some(dev) => dev,
};
info!("Device {:?} continues with the reset process", dev.id());
debug!("------------------------------------------------------------------");
debug!("{:?}", reset);
debug!("------------------------------------------------------------------");
let resp = dev.send_cbor_cancellable(&reset, alive);
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::reset_helper(&mut dev, selector, status, callback.clone(), alive);
},
);
self.transaction = Some(try_or!(transaction, move |e| cbc.call(Err(e))));
}
pub fn set_or_change_pin_helper(
dev: &mut Device,
mut current_pin: Option<Pin>,
new_pin: Pin,
status: Sender<crate::StatusUpdate>,
callback: StateCallback<crate::Result<crate::ResetResult>>,
alive: &dyn Fn() -> bool,
) {
let mut shared_secret = match dev.establish_shared_secret() {
Ok(s) => s,
Err(e) => {
callback.call(Err(AuthenticatorError::HIDError(e)));
return;
}
};
let authinfo = match dev.get_authenticator_info() {
Some(i) => i.clone(),
None => {
callback.call(Err(HIDError::DeviceNotInitialized.into()));
return;
}
};
// If the device has a min PIN use that, otherwise default to 4 according to Spec
if new_pin.as_bytes().len() < authinfo.min_pin_length.unwrap_or(4) as usize {
callback.call(Err(AuthenticatorError::PinError(PinError::PinIsTooShort)));
return;
}
// As per Spec: "Maximum PIN Length: UTF-8 representation MUST NOT exceed 63 bytes"
if new_pin.as_bytes().len() >= 64 {
callback.call(Err(AuthenticatorError::PinError(PinError::PinIsTooLong(
new_pin.as_bytes().len(),
))));
return;
}
// Check if a client-pin is already set, or if a new one should be created
let res = if Some(true) == authinfo.options.client_pin {
let mut res;
let mut was_invalid = false;
let mut retries = None;
loop {
// current_pin will only be Some() in the interactive mode (running `manage()`)
// In case that PIN is wrong, we want to avoid an endless-loop here with re-trying
// that wrong PIN all the time. So we `take()` it, and only test it once.
// If that PIN is wrong, we fall back to the "ask_user_for_pin"-method.
let curr_pin = match current_pin.take() {
None => {
match Self::ask_user_for_pin(was_invalid, retries, &status, &callback) {
Ok(pin) => pin,
_ => {
return;
}
}
}
Some(pin) => pin,
};
res = ChangeExistingPin::new(&authinfo, &shared_secret, &curr_pin, &new_pin)
.map_err(HIDError::Command)
.and_then(|msg| dev.send_cbor_cancellable(&msg, alive))
.map_err(|e| repackage_pin_errors(dev, e));
if let Err(AuthenticatorError::PinError(PinError::InvalidPin(r))) = res {
was_invalid = true;
retries = r;
// We need to re-establish the shared secret for the next round.
match dev.establish_shared_secret() {
Ok(s) => {
shared_secret = s;
}
Err(e) => {
callback.call(Err(AuthenticatorError::HIDError(e)));
return;
}
};
continue;
} else {
break;
}
}
res
} else {
dev.send_cbor_cancellable(&SetNewPin::new(&shared_secret, &new_pin), alive)
.map_err(AuthenticatorError::HIDError)
};
callback.call(res);
}
pub fn set_pin(
&mut self,
timeout: u64,
@ -832,95 +1054,21 @@ impl StateMachine {
callback.clone(),
status,
move |info, selector, status, alive| {
let mut dev = match Self::init_and_select(info, &selector, true, alive) {
let mut dev = match Self::init_and_select(info, &selector, &status, true, alive) {
None => {
return;
}
Some(dev) => dev,
};
let mut shared_secret = match dev.establish_shared_secret() {
Ok(s) => s,
Err(e) => {
callback.call(Err(AuthenticatorError::HIDError(e)));
return;
}
};
let authinfo = match dev.get_authenticator_info() {
Some(i) => i.clone(),
None => {
callback.call(Err(HIDError::DeviceNotInitialized.into()));
return;
}
};
// 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 was_invalid = false;
let mut retries = None;
loop {
let current_pin = match Self::ask_user_for_pin(
was_invalid,
retries,
&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_cancellable(&msg, alive))
.map_err(|e| repackage_pin_errors(&mut dev, e));
if let Err(AuthenticatorError::PinError(PinError::InvalidPin(r))) = res {
was_invalid = true;
retries = r;
// We need to re-establish the shared secret for the next round.
match dev.establish_shared_secret() {
Ok(s) => {
shared_secret = s;
}
Err(e) => {
callback.call(Err(AuthenticatorError::HIDError(e)));
return;
}
};
continue;
} else {
break;
}
}
res
} else {
dev.send_cbor_cancellable(&SetNewPin::new(&shared_secret, &new_pin), alive)
.map_err(AuthenticatorError::HIDError)
};
callback.call(res);
Self::set_or_change_pin_helper(
&mut dev,
None,
new_pin.clone(),
status,
callback.clone(),
alive,
);
},
);
self.transaction = Some(try_or!(transaction, move |e| cbc.call(Err(e))));
@ -938,7 +1086,6 @@ impl StateMachine {
) {
// Abort any prior register/sign calls.
self.cancel();
let cbc = callback.clone();
let transaction = Transaction::new(
@ -1166,4 +1313,95 @@ impl StateMachine {
self.transaction = Some(try_or!(transaction, |e| cbc.call(Err(e))));
}
// Function to interactively manage a specific token.
// Difference to register/sign: These want to do something and don't care
// with which token they do it.
// This function wants to manipulate a specific token. For this, we first
// have to select one and then do something with it, based on what it
// supports (Set PIN, Change PIN, Reset, etc.).
// Hence, we first go through the discovery-phase, then provide the user
// with the AuthenticatorInfo and then let them interactively decide what to do
pub fn manage(
&mut self,
timeout: u64,
status: Sender<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, &status, true, alive) {
None => {
return;
}
Some(dev) => dev,
};
info!("Device {:?} selected for interactive management.", dev.id());
// Sending the user the info about the token
let (tx, rx) = channel();
send_status(
&status,
crate::StatusUpdate::InteractiveManagement((
tx,
dev.get_device_info(),
dev.get_authenticator_info().cloned(),
)),
);
while alive() {
match rx.recv_timeout(Duration::from_millis(400)) {
Ok(InteractiveRequest::Reset) => {
Self::reset_helper(&mut dev, selector, status, callback.clone(), alive);
}
Ok(InteractiveRequest::ChangePIN(curr_pin, new_pin)) => {
Self::set_or_change_pin_helper(
&mut dev,
Some(curr_pin),
new_pin,
status,
callback.clone(),
alive,
);
}
Ok(InteractiveRequest::SetPIN(pin)) => {
Self::set_or_change_pin_helper(
&mut dev,
None,
pin,
status,
callback.clone(),
alive,
);
}
Err(RecvTimeoutError::Timeout) => {
if !alive() {
// We got stopped at some point
callback.call(Err(AuthenticatorError::CancelledByUser));
break;
}
continue;
}
Err(RecvTimeoutError::Disconnected) => {
// recv() failed, because the other side is dropping the Sender.
info!(
"Callback dropped the channel, so we abort the interactive session"
);
callback.call(Err(AuthenticatorError::CancelledByUser));
}
}
break;
}
},
);
self.transaction = Some(try_or!(transaction, move |e| cbc.call(Err(e))));
}
}

View file

@ -1,10 +1,15 @@
use super::{u2ftypes, Pin};
use serde::{
ser::{Serialize, SerializeStruct},
Serialize as DeriveSer, Serializer,
};
use crate::ctap2::commands::get_info::AuthenticatorInfo;
use serde::{Deserialize, Serialize as DeriveSer, Serializer};
use std::sync::mpsc::Sender;
#[derive(Debug, Deserialize, DeriveSer)]
pub enum InteractiveRequest {
Reset,
ChangePIN(Pin, Pin),
SetPIN(Pin),
}
// Simply ignoring the Sender when serializing
pub(crate) fn serialize_pin_required<S>(_: &Sender<Pin>, s: S) -> Result<S::Ok, S::Error>
where
@ -64,30 +69,14 @@ pub enum StatusUpdate {
/// 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::PinUvError(e) => map.serialize_field("PinUvError", &e)?,
StatusUpdate::SelectDeviceNotice => map.serialize_field("SelectDeviceNotice", &())?,
StatusUpdate::DeviceSelected(dev_info) => {
map.serialize_field("DeviceSelected", &dev_info)?
}
}
map.end()
}
/// Sent when a token was selected for interactive management
InteractiveManagement(
(
Sender<InteractiveRequest>,
u2ftypes::U2FDeviceInfo,
Option<AuthenticatorInfo>,
),
),
}
pub(crate) fn send_status(status: &Sender<StatusUpdate>, msg: StatusUpdate) {
@ -96,54 +85,3 @@ pub(crate) fn send_status(status: &Sender<StatusUpdate>, msg: StatusUpdate) {
Err(e) => error!("Couldn't send status: {:?}", e),
};
}
#[cfg(test)]
pub mod tests {
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_status_pin_uv() {
let (tx, _rx) = channel();
let st = StatusUpdate::PinUvError(StatusPinUv::InvalidPin(tx.clone(), Some(3)));
let json = to_string(&st).expect("Failed to serialize");
assert_eq!(&json, r#"{"PinUvError":{"InvalidPin":3}}"#);
let st = StatusUpdate::PinUvError(StatusPinUv::InvalidPin(tx, None));
let json = to_string(&st).expect("Failed to serialize");
assert_eq!(&json, r#"{"PinUvError":{"InvalidPin":null}}"#);
let st = StatusUpdate::PinUvError(StatusPinUv::PinBlocked);
let json = to_string(&st).expect("Failed to serialize");
assert_eq!(&json, r#"{"PinUvError":"PinBlocked"}"#);
}
#[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}}}"#
);
}
}

View file

@ -1,12 +1,14 @@
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;
// This import is used, but Rust 1.68 gives a warning
#[allow(unused_imports)]
use crate::u2ftypes::U2FDevice;
pub type DeviceID = <Device as HIDDevice>::Id;
pub type DeviceBuildParameters = <Device as HIDDevice>::BuildParameters;
@ -33,7 +35,7 @@ pub enum DeviceSelectorEvent {
DevicesAdded(Vec<DeviceID>),
DeviceRemoved(DeviceID),
NotAToken(DeviceID),
ImAToken((Device, Sender<DeviceCommand>)),
ImAToken((DeviceID, Sender<DeviceCommand>)),
SelectedToken(DeviceID),
}
@ -45,11 +47,7 @@ pub struct DeviceSelector {
}
impl DeviceSelector {
// Devices are hashed according to their DeviceID (usually a path),
// all other members (which can be mutable) are not used, but clippy
// can't check this.
#![allow(clippy::mutable_key_type)]
pub fn run(status: Sender<crate::StatusUpdate>) -> Self {
pub fn run() -> Self {
let (selector_send, selector_rec) = channel();
// let new_device_callback = Arc::new(new_device_cb);
let runloop = RunLoop::new(move |alive| {
@ -57,7 +55,8 @@ impl DeviceSelector {
// 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"
// Device IDs of devices that responded with "ImAToken" mapping to channels that are
// waiting to receive a DeviceCommand
let mut tokens = HashMap::new();
while alive() {
let d = Duration::from_secs(100);
@ -75,14 +74,8 @@ impl DeviceSelector {
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));
DeviceSelectorEvent::SelectedToken(ref id) => {
Self::cancel_all(tokens, Some(id));
break; // We are done here. The selected device continues without us.
}
DeviceSelectorEvent::DevicesAdded(ids) => {
@ -92,26 +85,20 @@ impl DeviceSelector {
}
continue;
}
DeviceSelectorEvent::DeviceRemoved(id) => {
DeviceSelectorEvent::DeviceRemoved(ref id) => {
debug!("Device removed event: {:?}", id);
if !waiting_for_response.remove(&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.iter().for_each(|(dev_id, tx)| {
if dev_id == id {
let _ = tx.send(DeviceCommand::Removed);
}
});
tokens.retain(|dev, _| dev.id() != id);
tokens.retain(|dev_id, _| dev_id != id);
if tokens.is_empty() {
blinking = false;
continue;
@ -127,27 +114,20 @@ impl DeviceSelector {
continue;
}
}
DeviceSelectorEvent::NotAToken(id) => {
DeviceSelectorEvent::NotAToken(ref id) => {
debug!("Device not a token event: {:?}", id);
waiting_for_response.remove(&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();
DeviceSelectorEvent::ImAToken((id, tx)) => {
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);
if tx.send(DeviceCommand::Blink).is_ok() {
tokens.insert(id, tx.clone());
}
continue;
} else {
tokens.insert(id, tx.clone());
}
}
}
@ -155,17 +135,13 @@ impl DeviceSelector {
// 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
let (dev_id, tx) = tokens.drain().next().unwrap(); // We just checked that it can't be empty
if tx.send(DeviceCommand::Continue).is_err() {
// Device thread died in the meantime (which shouldn't happen).
// Tokens is empty, so we just start over again
continue;
}
send_status(
&status,
crate::StatusUpdate::DeviceSelected(dev.get_device_info()),
);
Self::cancel_all(tokens, Some(&dev.id()));
Self::cancel_all(tokens, Some(&dev_id));
break; // We are done here
} else {
blinking = true;
@ -175,7 +151,6 @@ impl DeviceSelector {
// 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);
}
}
}
@ -190,9 +165,9 @@ impl DeviceSelector {
self.sender.clone()
}
fn cancel_all(tokens: HashMap<Device, Sender<DeviceCommand>>, exclude: Option<&DeviceID>) {
for (dev, tx) in tokens.iter() {
if Some(&dev.id()) != exclude {
fn cancel_all(tokens: HashMap<DeviceID, Sender<DeviceCommand>>, exclude: Option<&DeviceID>) {
for (dev_id, tx) in tokens.iter() {
if Some(dev_id) != exclude {
let _ = tx.send(DeviceCommand::Cancel);
}
}
@ -212,16 +187,7 @@ pub mod tests {
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 {
@ -257,7 +223,7 @@ pub mod tests {
selector
.sender
.send(DeviceSelectorEvent::ImAToken((
dev.clone_device_as_write_only().unwrap(),
dev.id(),
dev.sender.clone().unwrap(),
)))
.unwrap();
@ -281,22 +247,6 @@ pub mod tests {
);
}
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>,
@ -320,8 +270,7 @@ pub mod tests {
// 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);
let selector = DeviceSelector::run();
// Adding all
add_devices(devices.iter(), &selector);
@ -333,12 +282,10 @@ pub mod tests {
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);
}
// This test is mostly for testing stop() and clone_sender()
@ -346,8 +293,7 @@ pub mod tests {
fn test_device_selector_stop() {
let device = Device::new("device selector 1").unwrap();
let (status_tx, _) = channel();
let mut selector = DeviceSelector::run(status_tx);
let mut selector = DeviceSelector::run();
// Adding all
selector
@ -378,15 +324,13 @@ pub mod tests {
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);
let selector = DeviceSelector::run();
// Adding all, except the last one (we simulate that this one is not yet plugged in)
add_devices(devices.iter().take(5), &selector);
// Interleave tokens and non-tokens
send_i_am_token(&devices[2], &selector);
recv_status(&devices[2], &status_rx, ExpectedUpdate::DeviceAvailable);
devices.iter_mut().for_each(|d| {
if !d.is_u2f() {
@ -395,7 +339,6 @@ pub mod tests {
});
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!(
@ -406,11 +349,9 @@ pub mod tests {
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
@ -435,15 +376,13 @@ pub mod tests {
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);
let selector = DeviceSelector::run();
// Adding all, except the last one (we simulate that this one is not yet plugged in)
add_devices(devices.iter().take(5), &selector);
// Interleave tokens and non-tokens
send_i_am_token(&devices[2], &selector);
recv_status(&devices[2], &status_rx, ExpectedUpdate::DeviceAvailable);
devices.iter_mut().for_each(|d| {
if !d.is_u2f() {
@ -452,7 +391,6 @@ pub mod tests {
});
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!(
@ -463,23 +401,19 @@ pub mod tests {
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
@ -504,8 +438,7 @@ pub mod tests {
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);
let selector = DeviceSelector::run();
// Adding all, except the last one (we simulate that this one is not yet plugged in)
add_devices(devices.iter(), &selector);
@ -519,29 +452,24 @@ pub mod tests {
});
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);
}
}

View file

@ -212,25 +212,6 @@ impl HIDDevice for Device {
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 {}

View file

@ -35,13 +35,12 @@ impl Transaction {
+ 'static,
T: 'static,
{
let status_sender = status.clone();
let device_selector = DeviceSelector::run(status);
let device_selector = DeviceSelector::run();
let selector_sender = device_selector.clone_sender();
let thread = RunLoop::new_with_timeout(
move |alive| {
// Create a new device monitor.
let mut monitor = Monitor::new(new_device_cb, selector_sender, status_sender);
let mut monitor = Monitor::new(new_device_cb, selector_sender, status);
// Start polling for new devices.
try_or!(monitor.run(alive), |_| callback

View file

@ -30,7 +30,6 @@ where
fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo);
fn set_shared_secret(&mut self, secret: SharedSecret);
fn get_shared_secret(&self) -> Option<&SharedSecret>;
fn clone_device_as_write_only(&self) -> Result<Self, HIDError>;
fn supports_ctap1(&self) -> bool {
// CAPABILITY_NMSG:

View file

@ -158,26 +158,6 @@ impl HIDDevice for Device {
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 {}

View file

@ -35,13 +35,12 @@ impl Transaction {
+ 'static,
T: 'static,
{
let status_sender = status.clone();
let device_selector = DeviceSelector::run(status);
let device_selector = DeviceSelector::run();
let selector_sender = device_selector.clone_sender();
let thread = RunLoop::new_with_timeout(
move |alive| {
// Create a new device monitor.
let mut monitor = Monitor::new(new_device_cb, selector_sender, status_sender);
let mut monitor = Monitor::new(new_device_cb, selector_sender, status);
// Start polling for new devices.
try_or!(monitor.run(alive), |_| callback

View file

@ -204,19 +204,6 @@ impl HIDDevice for Device {
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 {}

View file

@ -46,8 +46,7 @@ impl Transaction {
{
let (tx, rx) = channel();
let timeout = (timeout as f64) / 1000.0;
let status_sender = status.clone();
let device_selector = DeviceSelector::run(status);
let device_selector = DeviceSelector::run();
let selector_sender = device_selector.clone_sender();
let builder = thread::Builder::new();
let thread = builder
@ -61,7 +60,7 @@ impl Transaction {
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);
let mut monitor = Monitor::new(new_device_cb, selector_sender, status);
try_or!(monitor.start(), |_| callback
.call(Err(errors::AuthenticatorError::Platform)));

View file

@ -173,19 +173,6 @@ impl HIDDevice for Device {
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()
}

View file

@ -225,21 +225,6 @@ impl HIDDevice for Device {
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 {}

View file

@ -35,13 +35,12 @@ impl Transaction {
+ 'static,
T: 'static,
{
let status_sender = status.clone();
let device_selector = DeviceSelector::run(status);
let device_selector = DeviceSelector::run();
let selector_sender = device_selector.clone_sender();
let thread = RunLoop::new_with_timeout(
move |alive| {
// Create a new device monitor.
let mut monitor = Monitor::new(new_device_cb, selector_sender, status_sender);
let mut monitor = Monitor::new(new_device_cb, selector_sender, status);
// Start polling for new devices.
try_or!(monitor.run(alive), |_| callback

View file

@ -201,27 +201,6 @@ impl HIDDevice for Device {
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 {}

View file

@ -35,13 +35,12 @@ impl Transaction {
+ 'static,
T: 'static,
{
let status_sender = status.clone();
let device_selector = DeviceSelector::run(status);
let device_selector = DeviceSelector::run();
let selector_sender = device_selector.clone_sender();
let thread = RunLoop::new_with_timeout(
move |alive| {
// Create a new device monitor.
let mut monitor = Monitor::new(new_device_cb, selector_sender, status_sender);
let mut monitor = Monitor::new(new_device_cb, selector_sender, status);
// Start polling for new devices.
try_or!(monitor.run(alive), |_| callback

View file

@ -96,10 +96,6 @@ impl HIDDevice for Device {
fn get_shared_secret(&self) -> Option<&SharedSecret> {
unimplemented!()
}
fn clone_device_as_write_only(&self) -> Result<Self, HIDError> {
unimplemented!()
}
}
impl FidoDevice for Device {}

View file

@ -16,7 +16,7 @@ impl Transaction {
pub fn new<F, T>(
timeout: u64,
callback: StateCallback<crate::Result<T>>,
status: Sender<crate::StatusUpdate>,
_status: Sender<crate::StatusUpdate>,
new_device_cb: F,
) -> crate::Result<Self>
where
@ -31,7 +31,7 @@ impl Transaction {
T: 'static,
{
// Just to silence "unused"-warnings
let mut device_selector = DeviceSelector::run(status);
let mut device_selector = DeviceSelector::run();
let _ = DeviceSelectorEvent::DevicesAdded(vec![]);
let _ = DeviceSelectorEvent::DeviceRemoved(PathBuf::new());
let _ = device_selector.clone_sender();

View file

@ -149,24 +149,6 @@ impl HIDDevice for Device {
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 {}

View file

@ -35,13 +35,12 @@ impl Transaction {
+ 'static,
T: 'static,
{
let status_sender = status.clone();
let device_selector = DeviceSelector::run(status);
let device_selector = DeviceSelector::run();
let selector_sender = device_selector.clone_sender();
let thread = RunLoop::new_with_timeout(
move |alive| {
// Create a new device monitor.
let mut monitor = Monitor::new(new_device_cb, selector_sender, status_sender);
let mut monitor = Monitor::new(new_device_cb, selector_sender, status);
// Start polling for new devices.
try_or!(monitor.run(alive), |_| callback