Bug 1827748 - vendor authenticator-rs v0.4.0-alpha.12. r=keeler,glandium,supply-chain-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D175294
This commit is contained in:
John Schanck 2023-04-13 18:59:35 +00:00
parent 74407531f5
commit 20bcb2f55f
36 changed files with 1446 additions and 697 deletions

17
Cargo.lock generated
View File

@ -320,9 +320,9 @@ dependencies = [
[[package]]
name = "authenticator"
version = "0.4.0-alpha.11"
version = "0.4.0-alpha.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e73e25e6ae754b553f930c48af8f0e5ca71e641c419151e95bafd5644ff6e21b"
checksum = "c14dd8025f8c43525d3bcb5fba75838095ddfe99f2284a72cd1b2a9380c47ae2"
dependencies = [
"base64",
"bitflags 1.3.2",
@ -332,7 +332,7 @@ dependencies = [
"libc",
"libudev",
"log",
"memoffset 0.6.99",
"memoffset",
"nom",
"nss-gk-api",
"pkcs11-bindings",
@ -973,7 +973,7 @@ dependencies = [
"autocfg",
"cfg-if 1.0.0",
"crossbeam-utils",
"memoffset 0.8.0",
"memoffset",
"scopeguard",
]
@ -3145,13 +3145,6 @@ dependencies = [
"libc",
]
[[package]]
name = "memoffset"
version = "0.6.99"
dependencies = [
"memoffset 0.8.0",
]
[[package]]
name = "memoffset"
version = "0.8.0"
@ -3246,7 +3239,7 @@ dependencies = [
"libc",
"mach2",
"memmap2",
"memoffset 0.8.0",
"memoffset",
"minidump-common",
"nix 0.26.2",
"scroll",

View File

@ -121,9 +121,6 @@ wasi = { path = "build/rust/wasi" }
# Patch bindgen 0.63 to 0.64
bindgen = { path = "build/rust/bindgen" }
# Patch memoffset 0.6 to 0.8
memoffset = { path = "build/rust/memoffset" }
# Patch ntapi 0.3 to 0.4
ntapi = { path = "build/rust/ntapi" }

View File

@ -1,11 +0,0 @@
[package]
name = "memoffset"
version = "0.6.99"
edition = "2018"
license = "MPL-2.0"
[lib]
path = "lib.rs"
[dependencies.memoffset]
version = "0.8"

View File

@ -1,5 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
pub use memoffset::*;

View File

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

View File

@ -19,7 +19,7 @@ use authenticator::{
},
errors::{AuthenticatorError, PinError, U2FTokenError},
statecallback::StateCallback,
Assertion, Pin, RegisterResult, SignResult, StatusUpdate,
Assertion, Pin, RegisterResult, SignResult, StatusPinUv, StatusUpdate,
};
use moz_task::RunnableBuilder;
use nserror::{
@ -78,20 +78,13 @@ fn make_pin_required_prompt(
fn authrs_to_nserror(e: &AuthenticatorError) -> nsresult {
match e {
AuthenticatorError::U2FToken(U2FTokenError::Unknown) => NS_ERROR_DOM_UNKNOWN_ERR,
AuthenticatorError::U2FToken(U2FTokenError::NotSupported) => NS_ERROR_DOM_NOT_SUPPORTED_ERR,
AuthenticatorError::U2FToken(U2FTokenError::InvalidState) => NS_ERROR_DOM_INVALID_STATE_ERR,
AuthenticatorError::U2FToken(U2FTokenError::ConstraintError) => NS_ERROR_DOM_UNKNOWN_ERR,
AuthenticatorError::U2FToken(U2FTokenError::NotAllowed) => NS_ERROR_DOM_NOT_ALLOWED_ERR,
AuthenticatorError::PinError(PinError::PinRequired) => NS_ERROR_DOM_OPERATION_ERR,
AuthenticatorError::PinError(PinError::PinIsTooShort) => NS_ERROR_DOM_UNKNOWN_ERR,
AuthenticatorError::PinError(PinError::PinIsTooLong(_)) => NS_ERROR_DOM_UNKNOWN_ERR,
AuthenticatorError::PinError(PinError::InvalidKeyLen) => NS_ERROR_DOM_UNKNOWN_ERR,
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_UNKNOWN_ERR,
AuthenticatorError::PinError(PinError::Crypto(_)) => NS_ERROR_DOM_UNKNOWN_ERR,
_ => NS_ERROR_DOM_UNKNOWN_ERR,
}
}
@ -358,50 +351,46 @@ fn status_callback(
};
controller.send_prompt(tid, &notification_str);
}
Ok(StatusUpdate::PinError(error, sender)) => match error {
PinError::PinRequired => {
let guard = pin_receiver.lock();
if let Ok(mut entry) = guard {
entry.replace((tid, sender));
} else {
return;
}
let notification_str =
make_pin_required_prompt(tid, origin, browsing_context_id, false, -1);
controller.send_prompt(tid, &notification_str);
continue;
Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => {
let guard = pin_receiver.lock();
if let Ok(mut entry) = guard {
entry.replace((tid, sender));
} else {
return;
}
PinError::InvalidPin(attempts) => {
let guard = pin_receiver.lock();
if let Ok(mut entry) = guard {
entry.replace((tid, sender));
} else {
return;
}
let notification_str = make_pin_required_prompt(
tid,
origin,
browsing_context_id,
true,
attempts.map_or(-1, |x| x as i64),
);
controller.send_prompt(tid, &notification_str);
continue;
let notification_str =
make_pin_required_prompt(tid, origin, browsing_context_id, false, -1);
controller.send_prompt(tid, &notification_str);
}
Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, attempts))) => {
let guard = pin_receiver.lock();
if let Ok(mut entry) = guard {
entry.replace((tid, sender));
} else {
return;
}
PinError::PinAuthBlocked => {
let notification_str =
make_pin_error_prompt("pin-auth-blocked", tid, origin, browsing_context_id);
controller.send_prompt(tid, &notification_str);
}
PinError::PinBlocked => {
let notification_str =
make_pin_error_prompt("device-blocked", tid, origin, browsing_context_id);
controller.send_prompt(tid, &notification_str);
}
e => {
warn!("Unexpected error: {:?}", e)
}
},
let notification_str = make_pin_required_prompt(
tid,
origin,
browsing_context_id,
true,
attempts.map_or(-1, |x| x as i64),
);
controller.send_prompt(tid, &notification_str);
}
Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => {
let notification_str =
make_pin_error_prompt("pin-auth-blocked", tid, origin, browsing_context_id);
controller.send_prompt(tid, &notification_str);
}
Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => {
let notification_str =
make_pin_error_prompt("device-blocked", tid, origin, browsing_context_id);
controller.send_prompt(tid, &notification_str);
}
Ok(StatusUpdate::PinUvError(e)) => {
warn!("Unexpected error: {:?}", e)
}
Err(RecvError) => {
debug!("STATUS: end");
return;

View File

@ -266,7 +266,7 @@ delta = "0.1.8 -> 0.1.9"
[[audits.authenticator]]
who = "John M. Schanck <jschanck@mozilla.com>"
criteria = "safe-to-deploy"
version = "0.4.0-alpha.11"
version = "0.4.0-alpha.12"
notes = "Maintained by the CryptoEng team at Mozilla."
[[audits.autocfg]]

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.11"
version = "0.4.0-alpha.12"
dependencies = [
"assert_matches",
"base64",
@ -176,9 +176,9 @@ checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
[[package]]
name = "cc"
version = "1.0.76"
version = "1.0.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a284da2e6fe2092f2353e51713435363112dfd60030e22add80be333fb928f"
checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574"
[[package]]
name = "cexpr"
@ -544,9 +544,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "0.14.20"
version = "0.14.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac"
checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c"
dependencies = [
"bytes 1.2.1",
"futures-channel",
@ -615,15 +615,15 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libc"
version = "0.2.136"
version = "0.2.137"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55edcf6c0bb319052dea84732cf99db461780fd5e8d3eb46ab6ff312ab31f197"
checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
[[package]]
name = "libloading"
version = "0.7.3"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd"
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
dependencies = [
"cfg-if",
"winapi",
@ -666,9 +666,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "memoffset"
version = "0.6.5"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1"
dependencies = [
"autocfg",
]
@ -768,9 +768,9 @@ dependencies = [
[[package]]
name = "num_cpus"
version = "1.13.1"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
dependencies = [
"hermit-abi",
"libc",
@ -778,9 +778,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.15.0"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
[[package]]
name = "openssl"
@ -876,15 +876,15 @@ dependencies = [
[[package]]
name = "pkg-config"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
[[package]]
name = "ppv-lite86"
version = "0.2.16"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
@ -951,9 +951,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.6.0"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
dependencies = [
"aho-corasick",
"memchr",
@ -962,9 +962,9 @@ dependencies = [
[[package]]
name = "regex-syntax"
version = "0.6.27"
version = "0.6.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]]
name = "remove_dir_all"
@ -1020,9 +1020,9 @@ checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
[[package]]
name = "scoped-tls"
version = "1.0.0"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "serde"
@ -1088,9 +1088,9 @@ dependencies = [
[[package]]
name = "sha-1"
version = "0.10.0"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c"
dependencies = [
"cfg-if",
"cpufeatures",
@ -1248,9 +1248,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "1.8.0"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
dependencies = [
"proc-macro2",
"quote",

View File

@ -12,7 +12,7 @@
[package]
edition = "2018"
name = "authenticator"
version = "0.4.0-alpha.11"
version = "0.4.0-alpha.12"
authors = [
"J.C. Jones <jc@mozilla.com>",
"Tim Taubert <ttaubert@mozilla.com>",
@ -154,7 +154,7 @@ version = "^0.2"
version = "0.9"
[target."cfg(target_os = \"windows\")".dependencies.memoffset]
version = "0.6"
version = "0.8"
[target."cfg(target_os = \"windows\")".dependencies.winapi]
version = "^0.3"

View File

@ -116,9 +116,10 @@ fn main() {
Ok(StatusUpdate::Success { dev_info }) => {
println!("STATUS: success using device: {dev_info}");
}
Ok(StatusUpdate::PinError(..))
| Ok(StatusUpdate::SelectDeviceNotice)
| Ok(StatusUpdate::DeviceSelected(..)) => {
Ok(StatusUpdate::DeviceSelected(dev_info)) => {
println!("STATUS: Continuing with device: {dev_info}");
}
Ok(StatusUpdate::PinUvError(..)) | Ok(StatusUpdate::SelectDeviceNotice) => {
panic!("STATUS: This can't happen for CTAP1!");
}
Err(RecvError) => {

View File

@ -11,9 +11,8 @@ use authenticator::{
ctap2::server::{
PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, Transport, User,
},
errors::PinError,
statecallback::StateCallback,
COSEAlgorithm, Pin, RegisterResult, SignResult, StatusUpdate,
COSEAlgorithm, Pin, RegisterResult, SignResult, StatusPinUv, StatusUpdate,
};
use getopts::Options;
use sha2::{Digest, Sha256};
@ -100,35 +99,46 @@ fn main() {
Ok(StatusUpdate::DeviceSelected(dev_info)) => {
println!("STATUS: Continuing with device: {dev_info}");
}
Ok(StatusUpdate::PinError(error, sender)) => match error {
PinError::PinRequired => {
let raw_pin = rpassword::prompt_password_stderr("Enter PIN: ")
.expect("Failed to read PIN");
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
continue;
}
PinError::InvalidPin(attempts) => {
println!(
"Wrong PIN! {}",
attempts.map_or("Try again.".to_string(), |a| format!(
"You have {a} attempts left."
))
);
let raw_pin = rpassword::prompt_password_stderr("Enter PIN: ")
.expect("Failed to read PIN");
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
continue;
}
PinError::PinAuthBlocked => {
panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.")
}
PinError::PinBlocked => {
panic!("Too many failed attempts. Your device has been blocked. Reset it.")
}
e => {
panic!("Unexpected error: {:?}", e)
}
},
Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => {
let raw_pin =
rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN");
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
continue;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, attempts))) => {
println!(
"Wrong PIN! {}",
attempts.map_or("Try again.".to_string(), |a| format!(
"You have {a} attempts left."
))
);
let raw_pin =
rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN");
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
continue;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => {
panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.")
}
Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => {
panic!("Too many failed attempts. Your device has been blocked. Reset it.")
}
Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(attempts))) => {
println!(
"Wrong UV! {}",
attempts.map_or("Try again.".to_string(), |a| format!(
"You have {a} attempts left."
))
);
continue;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) => {
println!("Too many failed UV-attempts.");
continue;
}
Ok(StatusUpdate::PinUvError(e)) => {
panic!("Unexpected error: {:?}", e)
}
Err(RecvError) => {
println!("STATUS: end");
return;
@ -191,12 +201,8 @@ fn main() {
register_tx.send(rv).unwrap();
}));
if let Err(e) = manager.register(
timeout_ms,
ctap_args.into(),
status_tx.clone(),
callback,
) {
if let Err(e) = manager.register(timeout_ms, ctap_args.into(), status_tx.clone(), callback)
{
panic!("Couldn't register: {:?}", e);
};
@ -264,12 +270,7 @@ fn main() {
sign_tx.send(rv).unwrap();
}));
if let Err(e) = manager.sign(
timeout_ms,
ctap_args.into(),
status_tx,
callback,
) {
if let Err(e) = manager.sign(timeout_ms, ctap_args.into(), status_tx, callback) {
panic!("Couldn't sign: {:?}", e);
}

View File

@ -10,9 +10,8 @@ use authenticator::{
ctap2::server::{
PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, Transport, User,
},
errors::PinError,
statecallback::StateCallback,
COSEAlgorithm, Pin, RegisterResult, SignResult, StatusUpdate,
COSEAlgorithm, Pin, RegisterResult, SignResult, StatusPinUv, StatusUpdate,
};
use getopts::Options;
use sha2::{Digest, Sha256};
@ -32,9 +31,7 @@ fn print_usage(program: &str, opts: Options) {
fn register_user(manager: &mut AuthenticatorService, username: &str, timeout_ms: u64) {
println!();
println!("*********************************************************************");
println!(
"Asking a security key to register now with user: {username}"
);
println!("Asking a security key to register now with user: {username}");
println!("*********************************************************************");
println!("Asking a security key to register now...");
@ -70,35 +67,46 @@ fn register_user(manager: &mut AuthenticatorService, username: &str, timeout_ms:
Ok(StatusUpdate::DeviceSelected(dev_info)) => {
println!("STATUS: Continuing with device: {dev_info}");
}
Ok(StatusUpdate::PinError(error, sender)) => match error {
PinError::PinRequired => {
let raw_pin = rpassword::prompt_password_stderr("Enter PIN: ")
.expect("Failed to read PIN");
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
continue;
}
PinError::InvalidPin(attempts) => {
println!(
"Wrong PIN! {}",
attempts.map_or("Try again.".to_string(), |a| format!(
"You have {a} attempts left."
))
);
let raw_pin = rpassword::prompt_password_stderr("Enter PIN: ")
.expect("Failed to read PIN");
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
continue;
}
PinError::PinAuthBlocked => {
panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.")
}
PinError::PinBlocked => {
panic!("Too many failed attempts. Your device has been blocked. Reset it.")
}
e => {
panic!("Unexpected error: {:?}", e)
}
},
Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => {
let raw_pin =
rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN");
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
continue;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, attempts))) => {
println!(
"Wrong PIN! {}",
attempts.map_or("Try again.".to_string(), |a| format!(
"You have {a} attempts left."
))
);
let raw_pin =
rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN");
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
continue;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => {
panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.")
}
Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => {
panic!("Too many failed attempts. Your device has been blocked. Reset it.")
}
Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(attempts))) => {
println!(
"Wrong UV! {}",
attempts.map_or("Try again.".to_string(), |a| format!(
"You have {a} attempts left."
))
);
continue;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) => {
println!("Too many failed UV-attempts.");
continue;
}
Ok(StatusUpdate::PinUvError(e)) => {
panic!("Unexpected error: {:?}", e)
}
Err(RecvError) => {
println!("STATUS: end");
return;
@ -150,12 +158,7 @@ fn register_user(manager: &mut AuthenticatorService, username: &str, timeout_ms:
register_tx.send(rv).unwrap();
}));
if let Err(e) = manager.register(
timeout_ms,
ctap_args.into(),
status_tx,
callback,
) {
if let Err(e) = manager.register(timeout_ms, ctap_args.into(), status_tx, callback) {
panic!("Couldn't register: {:?}", e);
};
@ -258,35 +261,46 @@ fn main() {
Ok(StatusUpdate::DeviceSelected(dev_info)) => {
println!("STATUS: Continuing with device: {dev_info}");
}
Ok(StatusUpdate::PinError(error, sender)) => match error {
PinError::PinRequired => {
let raw_pin = rpassword::prompt_password_stderr("Enter PIN: ")
.expect("Failed to read PIN");
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
continue;
}
PinError::InvalidPin(attempts) => {
println!(
"Wrong PIN! {}",
attempts.map_or("Try again.".to_string(), |a| format!(
"You have {a} attempts left."
))
);
let raw_pin = rpassword::prompt_password_stderr("Enter PIN: ")
.expect("Failed to read PIN");
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
continue;
}
PinError::PinAuthBlocked => {
panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.")
}
PinError::PinBlocked => {
panic!("Too many failed attempts. Your device has been blocked. Reset it.")
}
e => {
panic!("Unexpected error: {:?}", e)
}
},
Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => {
let raw_pin =
rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN");
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
continue;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, attempts))) => {
println!(
"Wrong PIN! {}",
attempts.map_or("Try again.".to_string(), |a| format!(
"You have {a} attempts left."
))
);
let raw_pin =
rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN");
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
continue;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => {
panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.")
}
Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => {
panic!("Too many failed attempts. Your device has been blocked. Reset it.")
}
Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(attempts))) => {
println!(
"Wrong UV! {}",
attempts.map_or("Try again.".to_string(), |a| format!(
"You have {a} attempts left."
))
);
continue;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) => {
println!("Too many failed UV-attempts.");
continue;
}
Ok(StatusUpdate::PinUvError(e)) => {
panic!("Unexpected error: {:?}", e)
}
Err(RecvError) => {
println!("STATUS: end");
return;
@ -315,12 +329,7 @@ fn main() {
sign_tx.send(rv).unwrap();
}));
if let Err(e) = manager.sign(
timeout_ms,
ctap_args.into(),
status_tx,
callback,
) {
if let Err(e) = manager.sign(timeout_ms, ctap_args.into(), status_tx, callback) {
panic!("Couldn't sign: {:?}", e);
}

View File

@ -116,7 +116,7 @@ fn main() {
Ok(StatusUpdate::Success { dev_info }) => {
println!("STATUS: success using device: {dev_info}");
}
Ok(StatusUpdate::PinError(..))
Ok(StatusUpdate::PinUvError(..))
| Ok(StatusUpdate::SelectDeviceNotice)
| Ok(StatusUpdate::DeviceSelected(..)) => {
panic!("STATUS: This can't happen for CTAP1!");

View File

@ -109,7 +109,7 @@ fn main() {
println!("STATUS: Continuing with device: {dev_info}");
break;
}
Ok(StatusUpdate::PinError(..)) => panic!("Reset should never ask for a PIN!"),
Ok(StatusUpdate::PinUvError(..)) => panic!("Reset should never ask for a PIN!"),
Ok(_) => { /* Ignore all other updates */ }
Err(RecvError) => {
println!("RecvError");

View File

@ -5,7 +5,7 @@
use authenticator::{
authenticatorservice::{AuthenticatorService, CtapVersion},
statecallback::StateCallback,
Pin, PinError, StatusUpdate,
Pin, StatusPinUv, StatusUpdate,
};
use getopts::Options;
use std::sync::mpsc::{channel, RecvError};
@ -85,35 +85,46 @@ fn main() {
Ok(StatusUpdate::DeviceSelected(dev_info)) => {
println!("STATUS: Continuing with device: {dev_info}");
}
Ok(StatusUpdate::PinError(error, sender)) => match error {
PinError::PinRequired => {
let raw_pin = rpassword::prompt_password_stderr("Enter current PIN: ")
.expect("Failed to read PIN");
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
continue;
}
PinError::InvalidPin(attempts) => {
println!(
"Wrong PIN! {}",
attempts.map_or("Try again.".to_string(), |a| format!(
"You have {a} attempts left."
))
);
let raw_pin = rpassword::prompt_password_stderr("Enter current PIN: ")
.expect("Failed to read PIN");
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
continue;
}
PinError::PinAuthBlocked => {
panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.")
}
PinError::PinBlocked => {
panic!("Too many failed attempts. Your device has been blocked. Reset it.")
}
e => {
panic!("Unexpected error: {:?}", e)
}
},
Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => {
let raw_pin =
rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN");
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
continue;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, attempts))) => {
println!(
"Wrong PIN! {}",
attempts.map_or("Try again.".to_string(), |a| format!(
"You have {a} attempts left."
))
);
let raw_pin =
rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN");
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
continue;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => {
panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.")
}
Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => {
panic!("Too many failed attempts. Your device has been blocked. Reset it.")
}
Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(attempts))) => {
println!(
"Wrong UV! {}",
attempts.map_or("Try again.".to_string(), |a| format!(
"You have {a} attempts left."
))
);
continue;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) => {
println!("Too many failed UV-attempts.");
continue;
}
Ok(StatusUpdate::PinUvError(e)) => {
panic!("Unexpected error: {:?}", e)
}
Err(RecvError) => {
println!("STATUS: end");
return;

View File

@ -10,9 +10,9 @@ use authenticator::{
ctap2::server::{
PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, Transport, User,
},
errors::{AuthenticatorError, CommandError, HIDError, PinError},
errors::{AuthenticatorError, CommandError, HIDError},
statecallback::StateCallback,
COSEAlgorithm, Pin, RegisterResult, StatusUpdate,
COSEAlgorithm, Pin, RegisterResult, StatusPinUv, StatusUpdate,
};
use getopts::Options;
@ -96,35 +96,46 @@ fn main() {
Ok(StatusUpdate::DeviceSelected(dev_info)) => {
println!("STATUS: Continuing with device: {dev_info}");
}
Ok(StatusUpdate::PinError(error, sender)) => match error {
PinError::PinRequired => {
let raw_pin = rpassword::prompt_password_stderr("Enter PIN: ")
.expect("Failed to read PIN");
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
continue;
}
PinError::InvalidPin(attempts) => {
println!(
"Wrong PIN! {}",
attempts.map_or("Try again.".to_string(), |a| format!(
"You have {a} attempts left."
))
);
let raw_pin = rpassword::prompt_password_stderr("Enter PIN: ")
.expect("Failed to read PIN");
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
continue;
}
PinError::PinAuthBlocked => {
panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.")
}
PinError::PinBlocked => {
panic!("Too many failed attempts. Your device has been blocked. Reset it.")
}
e => {
panic!("Unexpected error: {:?}", e)
}
},
Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => {
let raw_pin =
rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN");
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
continue;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, attempts))) => {
println!(
"Wrong PIN! {}",
attempts.map_or("Try again.".to_string(), |a| format!(
"You have {a} attempts left."
))
);
let raw_pin =
rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN");
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
continue;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => {
panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.")
}
Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => {
panic!("Too many failed attempts. Your device has been blocked. Reset it.")
}
Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(attempts))) => {
println!(
"Wrong UV! {}",
attempts.map_or("Try again.".to_string(), |a| format!(
"You have {a} attempts left."
))
);
continue;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) => {
println!("Too many failed UV-attempts.");
continue;
}
Ok(StatusUpdate::PinUvError(e)) => {
panic!("Unexpected error: {:?}", e)
}
Err(RecvError) => {
println!("STATUS: end");
return;

View File

@ -2,6 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use crate::ctap2::commands::client_pin::PinUvAuthTokenPermission;
use crate::ctap2::commands::get_info::AuthenticatorInfo;
use crate::errors::AuthenticatorError;
use crate::{ctap2::commands::CommandError, transport::errors::HIDError};
@ -314,11 +315,16 @@ impl SharedSecret {
pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
self.pin_protocol.0.decrypt(&self.key, ciphertext)
}
pub fn decrypt_pin_token(&self, encrypted_pin_token: &[u8]) -> Result<PinToken, CryptoError> {
pub fn decrypt_pin_token(
&self,
permissions: PinUvAuthTokenPermission,
encrypted_pin_token: &[u8],
) -> Result<PinUvAuthToken, CryptoError> {
let pin_token = self.decrypt(encrypted_pin_token)?;
Ok(PinToken {
Ok(PinUvAuthToken {
pin_protocol: self.pin_protocol.clone(),
pin_token,
permissions,
})
}
pub fn authenticate(&self, message: &[u8]) -> Result<Vec<u8>, CryptoError> {
@ -333,13 +339,14 @@ impl SharedSecret {
}
#[derive(Clone)]
pub struct PinToken {
pub struct PinUvAuthToken {
pub pin_protocol: PinUvAuthProtocol,
pin_token: Vec<u8>,
// TODO(jms): add permissions
#[allow(dead_code)] // Not yet used
permissions: PinUvAuthTokenPermission,
}
impl PinToken {
impl PinUvAuthToken {
pub fn derive(&self, message: &[u8]) -> Result<PinUvAuthParam, CryptoError> {
let pin_auth = self.pin_protocol.0.authenticate(&self.pin_token, message)?;
Ok(PinUvAuthParam {

View File

@ -1,5 +1,9 @@
#![allow(non_upper_case_globals)]
// Note: Needed for PinUvAuthTokenPermission
// The current version of `bitflags` doesn't seem to allow
// to set this for an individual bitflag-struct.
use super::{get_info::AuthenticatorInfo, Command, CommandError, RequestCtap2, StatusCode};
use crate::crypto::{COSEKey, CryptoError, PinToken, PinUvAuthProtocol, SharedSecret};
use crate::crypto::{COSEKey, CryptoError, PinUvAuthProtocol, PinUvAuthToken, SharedSecret};
use crate::transport::errors::HIDError;
use crate::u2ftypes::U2FDevice;
use serde::{
@ -19,25 +23,34 @@ use std::fmt;
#[derive(Debug, Copy, Clone)]
#[repr(u8)]
pub enum PINSubcommand {
GetRetries = 0x01,
GetPinRetries = 0x01,
GetKeyAgreement = 0x02,
SetPIN = 0x03,
ChangePIN = 0x04,
GetPINToken = 0x05, // superseded by GetPinUvAuth*
GetPinUvAuthTokenUsingUvWithPermissions = 0x06,
GetUVRetries = 0x07,
GetUvRetries = 0x07,
GetPinUvAuthTokenUsingPinWithPermissions = 0x09, // Yes, 0x08 is missing
}
#[derive(Debug, Copy, Clone)]
#[repr(u8)]
pub enum PinUvAuthTokenPermission {
MakeCredential = 0x01, // rp_id required
GetAssertion = 0x02, // rp_id required
CredentialManagement = 0x04, // rp_id optional
BioEnrollment = 0x08, // rp_id ignored
LargeBlobWrite = 0x10, // rp_id ignored
AuthenticatorConfiguration = 0x20, // rp_id ignored
bitflags! {
pub struct PinUvAuthTokenPermission: u8 {
const MakeCredential = 0x01; // rp_id required
const GetAssertion = 0x02; // rp_id required
const CredentialManagement = 0x04; // rp_id optional
const BioEnrollment = 0x08; // rp_id ignored
const LargeBlobWrite = 0x10; // rp_id ignored
const AuthenticatorConfiguration = 0x20; // rp_id ignored
}
}
impl Default for PinUvAuthTokenPermission {
fn default() -> Self {
// CTAP 2.1 spec:
// If authenticatorClientPIN's getPinToken subcommand is invoked, default permissions
// of `mc` and `ga` (value 0x03) are granted for the returned pinUvAuthToken.
PinUvAuthTokenPermission::MakeCredential | PinUvAuthTokenPermission::GetAssertion
}
}
#[derive(Debug)]
@ -56,7 +69,7 @@ impl Default for ClientPIN {
fn default() -> Self {
ClientPIN {
pin_protocol: None,
subcommand: PINSubcommand::GetRetries,
subcommand: PINSubcommand::GetPinRetries,
key_agreement: None,
pin_auth: None,
new_pin_enc: None,
@ -222,10 +235,8 @@ pub struct GetKeyAgreement {
}
impl GetKeyAgreement {
pub fn new(info: &AuthenticatorInfo) -> Result<Self, CommandError> {
Ok(GetKeyAgreement {
pin_protocol: PinUvAuthProtocol::try_from(info)?,
})
pub fn new(pin_protocol: PinUvAuthProtocol) -> Self {
GetKeyAgreement { pin_protocol }
}
}
@ -272,7 +283,7 @@ impl<'sc, 'pin> GetPinToken<'sc, 'pin> {
}
impl<'sc, 'pin> ClientPINSubCommand for GetPinToken<'sc, 'pin> {
type Output = PinToken;
type Output = PinUvAuthToken;
fn as_client_pin(&self) -> Result<ClientPIN, CommandError> {
let input = self.pin.for_pin_token();
@ -297,9 +308,13 @@ impl<'sc, 'pin> ClientPINSubCommand for GetPinToken<'sc, 'pin> {
from_slice(input).map_err(CommandError::Deserializing)?;
match get_pin_response.pin_token {
Some(encrypted_pin_token) => {
// CTAP 2.1 spec:
// If authenticatorClientPIN's getPinToken subcommand is invoked, default permissions
// of `mc` and `ga` (value 0x03) are granted for the returned pinUvAuthToken.
let default_permissions = PinUvAuthTokenPermission::default();
let pin_token = self
.shared_secret
.decrypt_pin_token(encrypted_pin_token.as_ref())?;
.decrypt_pin_token(default_permissions, encrypted_pin_token.as_ref())?;
Ok(pin_token)
}
None => Err(CommandError::MissingRequiredField("key_agreement")),
@ -332,7 +347,7 @@ impl<'sc, 'pin> GetPinUvAuthTokenUsingPinWithPermissions<'sc, 'pin> {
}
impl<'sc, 'pin> ClientPINSubCommand for GetPinUvAuthTokenUsingPinWithPermissions<'sc, 'pin> {
type Output = PinToken;
type Output = PinUvAuthToken;
fn as_client_pin(&self) -> Result<ClientPIN, CommandError> {
let input = self.pin.for_pin_token();
@ -343,7 +358,103 @@ impl<'sc, 'pin> ClientPINSubCommand for GetPinUvAuthTokenUsingPinWithPermissions
subcommand: PINSubcommand::GetPinUvAuthTokenUsingPinWithPermissions,
key_agreement: Some(self.shared_secret.client_input().clone()),
pin_hash_enc: Some(ByteBuf::from(pin_hash_enc)),
permissions: Some(self.permissions as u8),
permissions: Some(self.permissions.bits()),
rp_id: self.rp_id.clone(), /* TODO: This could probably be done less wasteful with
* &str all the way */
..ClientPIN::default()
})
}
fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> {
let value: Value = from_slice(input).map_err(CommandError::Deserializing)?;
debug!(
"GetPinUvAuthTokenUsingPinWithPermissions::parse_response_payload {:?}",
value
);
let get_pin_response: ClientPinResponse =
from_slice(input).map_err(CommandError::Deserializing)?;
match get_pin_response.pin_token {
Some(encrypted_pin_token) => {
let pin_token = self
.shared_secret
.decrypt_pin_token(self.permissions, encrypted_pin_token.as_ref())?;
Ok(pin_token)
}
None => Err(CommandError::MissingRequiredField("key_agreement")),
}
}
}
macro_rules! implementRetries {
($name:ident, $getter:ident) => {
#[derive(Debug)]
pub struct $name {}
impl $name {
pub fn new() -> Self {
Self {}
}
}
impl ClientPINSubCommand for $name {
type Output = u8;
fn as_client_pin(&self) -> Result<ClientPIN, CommandError> {
Ok(ClientPIN {
subcommand: PINSubcommand::$name,
..ClientPIN::default()
})
}
fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> {
let value: Value = from_slice(input).map_err(CommandError::Deserializing)?;
debug!("{}::parse_response_payload {:?}", stringify!($name), value);
let get_pin_response: ClientPinResponse =
from_slice(input).map_err(CommandError::Deserializing)?;
match get_pin_response.$getter {
Some($getter) => Ok($getter),
None => Err(CommandError::MissingRequiredField(stringify!($getter))),
}
}
}
};
}
implementRetries!(GetPinRetries, pin_retries);
implementRetries!(GetUvRetries, uv_retries);
#[derive(Debug)]
pub struct GetPinUvAuthTokenUsingUvWithPermissions<'sc> {
shared_secret: &'sc SharedSecret,
permissions: PinUvAuthTokenPermission,
rp_id: Option<String>,
}
impl<'sc> GetPinUvAuthTokenUsingUvWithPermissions<'sc> {
pub fn new(
shared_secret: &'sc SharedSecret,
permissions: PinUvAuthTokenPermission,
rp_id: Option<String>,
) -> Self {
GetPinUvAuthTokenUsingUvWithPermissions {
shared_secret,
permissions,
rp_id,
}
}
}
impl<'sc> ClientPINSubCommand for GetPinUvAuthTokenUsingUvWithPermissions<'sc> {
type Output = PinUvAuthToken;
fn as_client_pin(&self) -> Result<ClientPIN, CommandError> {
Ok(ClientPIN {
pin_protocol: Some(self.shared_secret.pin_protocol.clone()),
subcommand: PINSubcommand::GetPinUvAuthTokenUsingUvWithPermissions,
key_agreement: Some(self.shared_secret.client_input().clone()),
permissions: Some(self.permissions.bits()),
rp_id: self.rp_id.clone(), /* TODO: This could probably be done less wasteful with
* &str all the way */
..ClientPIN::default()
@ -360,7 +471,7 @@ impl<'sc, 'pin> ClientPINSubCommand for GetPinUvAuthTokenUsingPinWithPermissions
Some(encrypted_pin_token) => {
let pin_token = self
.shared_secret
.decrypt_pin_token(encrypted_pin_token.as_ref())?;
.decrypt_pin_token(self.permissions, encrypted_pin_token.as_ref())?;
Ok(pin_token)
}
None => Err(CommandError::MissingRequiredField("key_agreement")),
@ -368,38 +479,6 @@ impl<'sc, 'pin> ClientPINSubCommand for GetPinUvAuthTokenUsingPinWithPermissions
}
}
#[derive(Debug)]
pub struct GetRetries {}
impl GetRetries {
pub fn new() -> Self {
GetRetries {}
}
}
impl ClientPINSubCommand for GetRetries {
type Output = u8;
fn as_client_pin(&self) -> Result<ClientPIN, CommandError> {
Ok(ClientPIN {
subcommand: PINSubcommand::GetRetries,
..ClientPIN::default()
})
}
fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> {
let value: Value = from_slice(input).map_err(CommandError::Deserializing)?;
debug!("GetKeyAgreement::parse_response_payload {:?}", value);
let get_pin_response: ClientPinResponse =
from_slice(input).map_err(CommandError::Deserializing)?;
match get_pin_response.pin_retries {
Some(pin_retries) => Ok(pin_retries),
None => Err(CommandError::MissingRequiredField("pin_retries")),
}
}
}
#[derive(Debug)]
pub struct SetNewPin<'sc, 'pin> {
shared_secret: &'sc SharedSecret,
@ -641,38 +720,45 @@ pub enum PinError {
PinRequired,
PinIsTooShort,
PinIsTooLong(usize),
InvalidKeyLen,
InvalidPin(Option<u8>),
InvalidUv(Option<u8>),
PinAuthBlocked,
PinBlocked,
PinNotSet,
UvBlocked,
/// Used for CTAP2.0 UV (fingerprints)
PinAuthInvalid,
Crypto(CryptoError),
}
impl fmt::Display for PinError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
PinError::PinRequired => write!(f, "PinError: Pin required."),
PinError::PinIsTooShort => write!(f, "PinError: pin is too short"),
PinError::PinIsTooLong(len) => write!(f, "PinError: pin is too long ({len})"),
PinError::InvalidKeyLen => write!(f, "PinError: invalid key len"),
PinError::PinRequired => write!(f, "Pin required."),
PinError::PinIsTooShort => write!(f, "pin is too short"),
PinError::PinIsTooLong(len) => write!(f, "pin is too long ({len})"),
PinError::InvalidPin(ref e) => {
let mut res = write!(f, "PinError: Invalid Pin.");
let mut res = write!(f, "Invalid Pin.");
if let Some(pin_retries) = e {
res = write!(f, " Retries left: {pin_retries:?}")
}
res
}
PinError::PinAuthBlocked => write!(
f,
"PinError: Pin authentication blocked. Device needs power cycle."
),
PinError::PinBlocked => write!(
f,
"PinError: No retries left. Pin blocked. Device needs reset."
),
PinError::PinNotSet => write!(f, "PinError: Pin needed but not set on device."),
PinError::Crypto(ref e) => write!(f, "PinError: Crypto backend error: {e:?}"),
PinError::InvalidUv(ref e) => {
let mut res = write!(f, "Invalid Uv.");
if let Some(uv_retries) = e {
res = write!(f, " Retries left: {uv_retries:?}")
}
res
}
PinError::PinAuthBlocked => {
write!(f, "Pin authentication blocked. Device needs power cycle.")
}
PinError::PinBlocked => write!(f, "No retries left. Pin blocked. Device needs reset."),
PinError::PinNotSet => write!(f, "Pin needed but not set on device."),
PinError::UvBlocked => write!(f, "No retries left. Uv blocked. Device needs reset."),
PinError::PinAuthInvalid => write!(f, "PinAuth invalid."),
PinError::Crypto(ref e) => write!(f, "Crypto backend error: {e:?}"),
}
}
}

View File

@ -1,11 +1,11 @@
use super::{
Command, CommandError, PinAuthCommand, Request, RequestCtap1, RequestCtap2, Retryable,
Command, CommandError, PinUvAuthCommand, Request, RequestCtap1, RequestCtap2, Retryable,
StatusCode,
};
use crate::consts::{
PARAMETER_SIZE, U2F_AUTHENTICATE, U2F_CHECK_IS_REGISTERED, U2F_REQUEST_USER_PRESENCE,
};
use crate::crypto::{COSEKey, CryptoError, PinUvAuthParam, SharedSecret};
use crate::crypto::{COSEKey, CryptoError, PinUvAuthParam, PinUvAuthToken, SharedSecret};
use crate::ctap2::attestation::{AuthenticatorData, AuthenticatorDataFlags};
use crate::ctap2::client_data::{ClientDataHash, CollectedClientData, CollectedClientDataWrapper};
use crate::ctap2::commands::client_pin::Pin;
@ -176,7 +176,7 @@ pub struct GetAssertion {
pub(crate) extensions: GetAssertionExtensions,
pub(crate) options: GetAssertionOptions,
pub(crate) pin: Option<Pin>,
pub(crate) pin_auth: Option<PinUvAuthParam>,
pub(crate) pin_uv_auth_param: Option<PinUvAuthParam>,
// This is used to implement the FIDO AppID extension.
pub(crate) alternate_rp_id: Option<String>,
@ -200,13 +200,13 @@ impl GetAssertion {
extensions,
options,
pin,
pin_auth: None,
pin_uv_auth_param: None,
alternate_rp_id,
})
}
}
impl PinAuthCommand for GetAssertion {
impl PinUvAuthCommand for GetAssertion {
fn pin(&self) -> &Option<Pin> {
&self.pin
}
@ -215,20 +215,47 @@ impl PinAuthCommand for GetAssertion {
self.pin = pin;
}
fn pin_auth(&self) -> &Option<PinUvAuthParam> {
&self.pin_auth
}
fn set_pin_auth(&mut self, pin_auth: Option<PinUvAuthParam>) {
self.pin_auth = pin_auth;
fn set_pin_uv_auth_param(
&mut self,
pin_uv_auth_token: Option<PinUvAuthToken>,
) -> Result<(), AuthenticatorError> {
let mut param = None;
if let Some(token) = pin_uv_auth_token {
param = Some(
token
.derive(self.client_data_hash().as_ref())
.map_err(CommandError::Crypto)?,
);
}
self.pin_uv_auth_param = param;
Ok(())
}
fn client_data_hash(&self) -> ClientDataHash {
self.client_data_wrapper.hash()
}
fn unset_uv_option(&mut self) {
self.options.user_verification = None;
fn set_uv_option(&mut self, uv: Option<bool>) {
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
RelyingPartyWrapper::Hash(..) => None,
RelyingPartyWrapper::Data(r) => Some(&r.id),
}
}
fn set_discouraged_uv_option(&mut self) {
// "[..] 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);
}
}
@ -249,7 +276,7 @@ impl Serialize for GetAssertion {
if self.options.has_some() {
map_len += 1;
}
if self.pin_auth.is_some() {
if self.pin_uv_auth_param.is_some() {
map_len += 2;
}
@ -276,9 +303,9 @@ impl Serialize for GetAssertion {
if self.options.has_some() {
map.serialize_entry(&5, &self.options)?;
}
if let Some(pin_auth) = &self.pin_auth {
map.serialize_entry(&6, &pin_auth)?;
map.serialize_entry(&7, &pin_auth.pin_protocol.id())?;
if let Some(pin_uv_auth_param) = &self.pin_uv_auth_param {
map.serialize_entry(&6, &pin_uv_auth_param)?;
map.serialize_entry(&7, &pin_uv_auth_param.pin_protocol.id())?;
}
map.end()
}

View File

@ -5,7 +5,7 @@ use crate::transport::errors::HIDError;
use crate::u2ftypes::U2FDevice;
use serde::{
de::{Error as SError, IgnoredAny, MapAccess, Visitor},
Deserialize, Deserializer,
Deserialize, Deserializer, Serialize,
};
use serde_cbor::{de::from_slice, Value};
use std::collections::BTreeMap;
@ -108,6 +108,173 @@ pub(crate) struct AuthenticatorOptions {
// it has no built-in verification method. Not to be trusted...
#[serde(rename = "uv")]
pub(crate) user_verification: Option<bool>,
// ----------------------------------------------------
// CTAP 2.1 options
// ----------------------------------------------------
/// If pinUvAuthToken is:
/// present and set to true
/// if the clientPin option id is present and set to true, then the
/// authenticator supports authenticatorClientPIN's getPinUvAuthTokenUsingPinWithPermissions
/// subcommand. If the uv option id is present and set to true, then
/// the authenticator supports authenticatorClientPIN's getPinUvAuthTokenUsingUvWithPermissions
/// subcommand.
/// present and set to false, or absent.
/// the authenticator does not support authenticatorClientPIN's
/// getPinUvAuthTokenUsingPinWithPermissions and getPinUvAuthTokenUsingUvWithPermissions
/// subcommands.
#[serde(rename = "pinUvAuthToken")]
pub(crate) pin_uv_auth_token: Option<bool>,
/// If this noMcGaPermissionsWithClientPin is:
/// present and set to true
/// A pinUvAuthToken obtained via getPinUvAuthTokenUsingPinWithPermissions
/// (or getPinToken) cannot be used for authenticatorMakeCredential or
/// authenticatorGetAssertion commands, because it will lack the necessary
/// mc and ga permissions. In this situation, platforms SHOULD NOT attempt
/// to use getPinUvAuthTokenUsingPinWithPermissions if using
/// getPinUvAuthTokenUsingUvWithPermissions fails.
/// present and set to false, or absent.
/// A pinUvAuthToken obtained via getPinUvAuthTokenUsingPinWithPermissions
/// (or getPinToken) can be used for authenticatorMakeCredential or
/// authenticatorGetAssertion commands.
/// Note: noMcGaPermissionsWithClientPin MUST only be present if the
/// clientPin option ID is present.
#[serde(rename = "noMcGaPermissionsWithClientPin")]
pub(crate) no_mc_ga_permissions_with_client_pin: Option<bool>,
/// If largeBlobs is:
/// present and set to true
/// the authenticator supports the authenticatorLargeBlobs command.
/// present and set to false, or absent.
/// The authenticatorLargeBlobs command is NOT supported.
#[serde(rename = "largeBlobs")]
pub(crate) large_blobs: Option<bool>,
/// Enterprise Attestation feature support:
/// If ep is:
/// Present and set to true
/// The authenticator is enterprise attestation capable, and enterprise
/// attestation is enabled.
/// Present and set to false
/// The authenticator is enterprise attestation capable, and enterprise
/// attestation is disabled.
/// Absent
/// The Enterprise Attestation feature is NOT supported.
#[serde(rename = "ep")]
pub(crate) ep: Option<bool>,
/// If bioEnroll is:
/// present and set to true
/// the authenticator supports the authenticatorBioEnrollment commands,
/// and has at least one bio enrollment presently provisioned.
/// present and set to false
/// the authenticator supports the authenticatorBioEnrollment commands,
/// and does not yet have any bio enrollments provisioned.
/// absent
/// the authenticatorBioEnrollment commands are NOT supported.
#[serde(rename = "bioEnroll")]
pub(crate) bio_enroll: Option<bool>,
/// "FIDO_2_1_PRE" Prototype Credential management support:
/// If userVerificationMgmtPreview is:
/// present and set to true
/// the authenticator supports the Prototype authenticatorBioEnrollment (0x41)
/// commands, and has at least one bio enrollment presently provisioned.
/// present and set to false
/// the authenticator supports the Prototype authenticatorBioEnrollment (0x41)
/// commands, and does not yet have any bio enrollments provisioned.
/// absent
/// the Prototype authenticatorBioEnrollment (0x41) commands are not supported.
#[serde(rename = "userVerificationMgmtPreview")]
pub(crate) 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.
/// If uvBioEnroll is:
/// present and set to true
/// requesting the be permission when invoking getPinUvAuthTokenUsingUvWithPermissions
/// is supported.
/// present and set to false, or absent.
/// requesting the be permission when invoking getPinUvAuthTokenUsingUvWithPermissions
/// is NOT supported.
#[serde(rename = "uvBioEnroll")]
pub(crate) uv_bio_enroll: Option<bool>,
/// authenticatorConfig command support:
/// If authnrCfg is:
/// present and set to true
/// the authenticatorConfig command is supported.
/// present and set to false, or absent.
/// the authenticatorConfig command is NOT supported.
#[serde(rename = "authnrCfg")]
pub(crate) authnr_cfg: Option<bool>,
/// getPinUvAuthTokenUsingUvWithPermissions support for requesting the acfg permission:
/// This option ID MUST only be present if authnrCfg is also present.
/// If uvAcfg is:
/// present and set to true
/// requesting the acfg permission when invoking getPinUvAuthTokenUsingUvWithPermissions
/// is supported.
/// present and set to false, or absent.
/// requesting the acfg permission when invoking getPinUvAuthTokenUsingUvWithPermissions
/// is NOT supported.
#[serde(rename = "uvAcfg")]
pub(crate) uv_acfg: Option<bool>,
/// Credential management support:
/// If credMgmt is:
/// present and set to true
/// the authenticatorCredentialManagement command is supported.
/// present and set to false, or absent.
/// the authenticatorCredentialManagement command is NOT supported.
#[serde(rename = "credMgmt")]
pub(crate) cred_mgmt: Option<bool>,
/// "FIDO_2_1_PRE" Prototype Credential management support:
/// If credentialMgmtPreview is:
/// present and set to true
/// the Prototype authenticatorCredentialManagement (0x41) command is supported.
/// present and set to false, or absent.
/// the Prototype authenticatorCredentialManagement (0x41) command is NOT supported.
#[serde(rename = "credentialMgmtPreview")]
pub(crate) credential_mgmt_preview: Option<bool>,
/// Support for the Set Minimum PIN Length feature.
/// If setMinPINLength is:
/// present and set to true
/// the setMinPINLength subcommand is supported.
/// present and set to false, or absent.
/// the setMinPINLength subcommand is NOT supported.
/// Note: setMinPINLength MUST only be present if the clientPin option ID is present.
#[serde(rename = "setMinPINLength")]
pub(crate) set_min_pin_length: Option<bool>,
/// Support for making non-discoverable credentials without requiring User Verification.
/// If makeCredUvNotRqd is:
/// present and set to true
/// the authenticator allows creation of non-discoverable credentials without
/// requiring any form of user verification, if the platform requests this behaviour.
/// present and set to false, or absent.
/// the authenticator requires some form of user verification for creating
/// non-discoverable credentials, regardless of the parameters the platform supplies
/// for the authenticatorMakeCredential command.
/// Authenticators SHOULD include this option with the value true.
#[serde(rename = "makeCredUvNotRqd")]
pub(crate) make_cred_uv_not_rqd: Option<bool>,
/// Support for the Always Require User Verification feature:
/// If alwaysUv is
/// present and set to true
/// the authenticator supports the Always Require User Verification feature and it is enabled.
/// present and set to false
/// the authenticator supports the Always Require User Verification feature but it is disabled.
/// absent
/// the authenticator does not support the Always Require User Verification feature.
/// Note: If the alwaysUv option ID is present and true the authenticator MUST set the value
/// of makeCredUvNotRqd to false.
#[serde(rename = "alwaysUv")]
pub(crate) always_uv: Option<bool>,
}
impl Default for AuthenticatorOptions {
@ -118,13 +285,36 @@ impl Default for AuthenticatorOptions {
client_pin: None,
user_presence: true,
user_verification: None,
pin_uv_auth_token: None,
no_mc_ga_permissions_with_client_pin: None,
large_blobs: None,
ep: None,
bio_enroll: None,
user_verification_mgmt_preview: None,
uv_bio_enroll: None,
authnr_cfg: None,
uv_acfg: None,
cred_mgmt: None,
credential_mgmt_preview: None,
set_min_pin_length: None,
make_cred_uv_not_rqd: None,
always_uv: None,
}
}
}
#[allow(non_camel_case_types)]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum AuthenticatorVersion {
U2F_V2,
FIDO_2_0,
FIDO_2_1_PRE,
FIDO_2_1,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct AuthenticatorInfo {
pub(crate) versions: Vec<String>,
pub(crate) versions: Vec<AuthenticatorVersion>,
pub(crate) extensions: Vec<String>,
pub(crate) aaguid: AAGuid,
pub(crate) options: AuthenticatorOptions,
@ -527,7 +717,7 @@ pub mod tests {
from_slice(&AUTHENTICATOR_INFO_PAYLOAD).unwrap();
let expected = AuthenticatorInfo {
versions: vec!["U2F_V2".to_string(), "FIDO_2_0".to_string()],
versions: vec![AuthenticatorVersion::U2F_V2, AuthenticatorVersion::FIDO_2_0],
extensions: vec!["uvm".to_string(), "hmac-secret".to_string()],
aaguid: AAGuid(AAGUID_RAW),
options: AuthenticatorOptions {
@ -536,6 +726,7 @@ pub mod tests {
client_pin: Some(false),
user_presence: true,
user_verification: None,
..Default::default()
},
max_msg_size: Some(1200),
pin_protocols: vec![1],
@ -580,10 +771,10 @@ pub mod tests {
let expected = AuthenticatorInfo {
versions: vec![
"U2F_V2".to_string(),
"FIDO_2_0".to_string(),
"FIDO_2_1_PRE".to_string(),
"FIDO_2_1".to_string(),
AuthenticatorVersion::U2F_V2,
AuthenticatorVersion::FIDO_2_0,
AuthenticatorVersion::FIDO_2_1_PRE,
AuthenticatorVersion::FIDO_2_1,
],
extensions: vec![
"credProtect".to_string(),
@ -602,6 +793,20 @@ pub mod tests {
client_pin: Some(true),
user_presence: true,
user_verification: Some(true),
pin_uv_auth_token: Some(true),
no_mc_ga_permissions_with_client_pin: None,
large_blobs: Some(true),
ep: None,
bio_enroll: Some(true),
user_verification_mgmt_preview: Some(true),
uv_bio_enroll: None,
authnr_cfg: Some(true),
uv_acfg: None,
cred_mgmt: Some(true),
credential_mgmt_preview: Some(true),
set_min_pin_length: Some(true),
make_cred_uv_not_rqd: Some(false),
always_uv: Some(true),
},
max_msg_size: Some(1200),
pin_protocols: vec![2, 1],
@ -693,7 +898,7 @@ pub mod tests {
.get_authenticator_info()
.expect("Didn't get any authenticator_info");
let expected = AuthenticatorInfo {
versions: vec!["U2F_V2".to_string(), "FIDO_2_0".to_string()],
versions: vec![AuthenticatorVersion::U2F_V2, AuthenticatorVersion::FIDO_2_0],
extensions: vec!["uvm".to_string(), "hmac-secret".to_string()],
aaguid: AAGuid(AAGUID_RAW),
options: AuthenticatorOptions {
@ -702,6 +907,7 @@ pub mod tests {
client_pin: Some(false),
user_presence: true,
user_verification: None,
..Default::default()
},
max_msg_size: Some(1200),
pin_protocols: vec![1],

View File

@ -1,11 +1,11 @@
use super::{
Command, CommandError, PinAuthCommand, Request, RequestCtap1, RequestCtap2, Retryable,
Command, CommandError, PinUvAuthCommand, Request, RequestCtap1, RequestCtap2, Retryable,
StatusCode,
};
use crate::consts::{PARAMETER_SIZE, U2F_REGISTER, U2F_REQUEST_USER_PRESENCE};
use crate::crypto::{
parse_u2f_der_certificate, COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve,
PinUvAuthParam,
PinUvAuthParam, PinUvAuthToken,
};
use crate::ctap2::attestation::{
AAGuid, AttestationObject, AttestationStatement, AttestationStatementFidoU2F,
@ -20,6 +20,7 @@ use crate::ctap2::server::{
PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty,
RelyingPartyWrapper, User,
};
use crate::errors::AuthenticatorError;
use crate::transport::{
errors::{ApduErrorStatus, HIDError},
FidoDevice,
@ -110,7 +111,7 @@ pub struct MakeCredentials {
pub(crate) extensions: MakeCredentialsExtensions,
pub(crate) options: MakeCredentialsOptions,
pub(crate) pin: Option<Pin>,
pub(crate) pin_auth: Option<PinUvAuthParam>,
pub(crate) pin_uv_auth_param: Option<PinUvAuthParam>,
pub(crate) enterprise_attestation: Option<u64>,
}
@ -135,13 +136,13 @@ impl MakeCredentials {
extensions,
options,
pin,
pin_auth: None,
pin_uv_auth_param: None,
enterprise_attestation: None,
})
}
}
impl PinAuthCommand for MakeCredentials {
impl PinUvAuthCommand for MakeCredentials {
fn pin(&self) -> &Option<Pin> {
&self.pin
}
@ -150,20 +151,51 @@ impl PinAuthCommand for MakeCredentials {
self.pin = pin;
}
fn pin_auth(&self) -> &Option<PinUvAuthParam> {
&self.pin_auth
}
fn set_pin_auth(&mut self, pin_auth: Option<PinUvAuthParam>) {
self.pin_auth = pin_auth;
fn set_pin_uv_auth_param(
&mut self,
pin_uv_auth_token: Option<PinUvAuthToken>,
) -> Result<(), AuthenticatorError> {
let mut param = None;
if let Some(token) = pin_uv_auth_token {
param = Some(
token
.derive(self.client_data_hash().as_ref())
.map_err(CommandError::Crypto)?,
);
}
self.pin_uv_auth_param = param;
Ok(())
}
fn client_data_hash(&self) -> ClientDataHash {
self.client_data_wrapper.hash()
}
fn unset_uv_option(&mut self) {
self.options.user_verification = None;
fn set_uv_option(&mut self, uv: Option<bool>) {
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
RelyingPartyWrapper::Hash(..) => None,
RelyingPartyWrapper::Data(r) => Some(&r.id),
}
}
fn set_discouraged_uv_option(&mut self) {
// "[..] 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))
}
}
@ -185,7 +217,7 @@ impl Serialize for MakeCredentials {
if self.options.has_some() {
map_len += 1;
}
if self.pin_auth.is_some() {
if self.pin_uv_auth_param.is_some() {
map_len += 2;
}
if self.enterprise_attestation.is_some() {
@ -216,9 +248,9 @@ impl Serialize for MakeCredentials {
if self.options.has_some() {
map.serialize_entry(&0x07, &self.options)?;
}
if let Some(pin_auth) = &self.pin_auth {
map.serialize_entry(&0x08, &pin_auth)?;
map.serialize_entry(&0x09, &pin_auth.pin_protocol.id())?;
if let Some(pin_uv_auth_param) = &self.pin_uv_auth_param {
map.serialize_entry(&0x08, &pin_uv_auth_param)?;
map.serialize_entry(&0x09, &pin_uv_auth_param.pin_protocol.id())?;
}
if let Some(enterprise_attestation) = self.enterprise_attestation {
map.serialize_entry(&0x0a, &enterprise_attestation)?;
@ -279,11 +311,7 @@ impl RequestCtap1 for MakeCredentials {
)));
}
let flags = if self.options.ask_user_verification() {
U2F_REQUEST_USER_PRESENCE
} else {
0
};
let flags = U2F_REQUEST_USER_PRESENCE;
let mut register_data = Vec::with_capacity(2 * PARAMETER_SIZE);
if self.is_ctap2_request() {
@ -944,7 +972,7 @@ pub mod test {
// CBOR Header
0x0, // CLA
0x1, // INS U2F_Register
0x0, // P1 Flags
0x3, // P1 Flags
0x0, // P2
0x0, 0x0, 0x40, // Lc
// NOTE: This has been taken from CTAP2.0 spec, but the clientDataHash has been replaced

View File

@ -1,6 +1,6 @@
use crate::crypto::{CryptoError, PinUvAuthParam};
use crate::crypto::{CryptoError, PinUvAuthToken};
use crate::ctap2::client_data::ClientDataHash;
use crate::ctap2::commands::client_pin::{GetPinToken, GetRetries, Pin, PinError};
use crate::ctap2::commands::client_pin::{GetPinRetries, GetUvRetries, Pin, PinError};
use crate::errors::AuthenticatorError;
use crate::transport::errors::{ApduErrorStatus, HIDError};
use crate::transport::FidoDevice;
@ -92,66 +92,85 @@ pub trait RequestCtap2: fmt::Debug {
Dev: FidoDevice + Read + Write + fmt::Debug;
}
pub(crate) trait PinAuthCommand {
#[derive(Debug, PartialEq, Clone)]
pub(crate) enum PinUvAuthResult {
/// Request is CTAP1 and does not need PinUvAuth
RequestIsCtap1,
/// Device is not capable of CTAP2
DeviceIsCtap1,
/// Device does not support UV or PINs
NoAuthTypeSupported,
/// Request doesn't want user verification (uv = "discouraged")
NoAuthRequired,
/// Device is CTAP2.0 and has internal UV capability
UsingInternalUv,
/// Successfully established PinUvAuthToken via GetPinToken (CTAP2.0)
SuccessGetPinToken,
/// Successfully established PinUvAuthToken via UV (CTAP2.1)
SuccessGetPinUvAuthTokenUsingUvWithPermissions,
/// Successfully established PinUvAuthToken via Pin (CTAP2.1)
SuccessGetPinUvAuthTokenUsingPinWithPermissions,
}
/// Helper-trait to determine pin_uv_auth_param from PIN or UV.
pub(crate) trait PinUvAuthCommand: RequestCtap2 {
fn pin(&self) -> &Option<Pin>;
fn set_pin(&mut self, pin: Option<Pin>);
fn pin_auth(&self) -> &Option<PinUvAuthParam>;
fn set_pin_auth(&mut self, pin_auth: Option<PinUvAuthParam>);
fn set_pin_uv_auth_param(
&mut self,
pin_uv_auth_token: Option<PinUvAuthToken>,
) -> Result<(), AuthenticatorError>;
fn client_data_hash(&self) -> ClientDataHash;
fn unset_uv_option(&mut self);
fn determine_pin_auth<D: FidoDevice>(&mut self, dev: &mut D) -> Result<(), AuthenticatorError> {
if !dev.supports_ctap2() {
self.set_pin_auth(None);
return Ok(());
}
let client_data_hash = self.client_data_hash();
let pin_auth = calculate_pin_auth(dev, &client_data_hash, self.pin())
.map_err(|e| repackage_pin_errors(dev, e))?;
self.set_pin_auth(pin_auth);
Ok(())
}
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 set_discouraged_uv_option(&mut self);
}
pub(crate) fn repackage_pin_errors<D: FidoDevice>(
dev: &mut D,
error: AuthenticatorError,
error: HIDError,
) -> AuthenticatorError {
match error {
AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
StatusCode::PinInvalid,
_,
))) => {
HIDError::Command(CommandError::StatusCode(StatusCode::PinInvalid, _)) => {
// If the given PIN was wrong, determine no. of left retries
let cmd = GetRetries::new();
let cmd = GetPinRetries::new();
let retries = dev.send_cbor(&cmd).ok(); // If we got retries, wrap it in Some, otherwise ignore err
AuthenticatorError::PinError(PinError::InvalidPin(retries))
}
AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
StatusCode::PinAuthBlocked,
_,
))) => AuthenticatorError::PinError(PinError::PinAuthBlocked),
AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
StatusCode::PinBlocked,
_,
))) => AuthenticatorError::PinError(PinError::PinBlocked),
AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
StatusCode::PinRequired,
_,
))) => AuthenticatorError::PinError(PinError::PinRequired),
AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
StatusCode::PinNotSet,
_,
))) => AuthenticatorError::PinError(PinError::PinNotSet),
HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthBlocked, _)) => {
AuthenticatorError::PinError(PinError::PinAuthBlocked)
}
HIDError::Command(CommandError::StatusCode(StatusCode::PinBlocked, _)) => {
AuthenticatorError::PinError(PinError::PinBlocked)
}
HIDError::Command(CommandError::StatusCode(StatusCode::PinRequired, _)) => {
AuthenticatorError::PinError(PinError::PinRequired)
}
HIDError::Command(CommandError::StatusCode(StatusCode::PinNotSet, _)) => {
AuthenticatorError::PinError(PinError::PinNotSet)
}
HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthInvalid, _)) => {
AuthenticatorError::PinError(PinError::PinAuthInvalid)
}
HIDError::Command(CommandError::StatusCode(StatusCode::UvInvalid, _)) => {
// If the internal UV failed, determine no. of left retries
let cmd = GetUvRetries::new();
let retries = dev.send_cbor(&cmd).ok(); // If we got retries, wrap it in Some, otherwise ignore err
AuthenticatorError::PinError(PinError::InvalidUv(retries))
}
HIDError::Command(CommandError::StatusCode(StatusCode::UvBlocked, _)) => {
AuthenticatorError::PinError(PinError::UvBlocked)
}
// TODO(MS): Add "PinPolicyViolated"
err => err,
err => AuthenticatorError::HIDError(err),
}
}
// Spec: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticator-api
// and: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#authenticator-api
#[repr(u8)]
#[derive(Debug)]
#[derive(Debug, PartialEq, Clone)]
pub enum Command {
MakeCredentials = 0x01,
GetAssertion = 0x02,
@ -262,6 +281,18 @@ pub enum StatusCode {
ActionTimeout,
/// User presence is required for the requested operation.
UpRequired,
/// built-in user verification is disabled.
UvBlocked,
/// A checksum did not match.
IntegrityFailure,
/// The requested subcommand is either invalid or not implemented.
InvalidSubcommand,
/// built-in user verification unsuccessful. The platform SHOULD retry.
UvInvalid,
/// The permissions parameter contains an unauthorized permission.
UnauthorizedPermission,
/// Other unspecified error.
Other,
/// Unknown status.
Unknown(u8),
@ -321,7 +352,12 @@ impl From<u8> for StatusCode {
0x39 => StatusCode::RequestTooLarge,
0x3A => StatusCode::ActionTimeout,
0x3B => StatusCode::UpRequired,
0x3C => StatusCode::UvBlocked,
0x3D => StatusCode::IntegrityFailure,
0x3E => StatusCode::InvalidSubcommand,
0x3F => StatusCode::UvInvalid,
0x40 => StatusCode::UnauthorizedPermission,
0x7F => StatusCode::Other,
othr => StatusCode::Unknown(othr),
}
}
@ -372,6 +408,12 @@ impl From<StatusCode> for u8 {
StatusCode::RequestTooLarge => 0x39,
StatusCode::ActionTimeout => 0x3A,
StatusCode::UpRequired => 0x3B,
StatusCode::UvBlocked => 0x3C,
StatusCode::IntegrityFailure => 0x3D,
StatusCode::InvalidSubcommand => 0x3E,
StatusCode::UvInvalid => 0x3F,
StatusCode::UnauthorizedPermission => 0x40,
StatusCode::Other => 0x7F,
StatusCode::Unknown(othr) => othr,
}
@ -416,38 +458,3 @@ impl fmt::Display for CommandError {
}
impl StdErrorT for CommandError {}
pub(crate) fn calculate_pin_auth<Dev>(
dev: &mut Dev,
client_data_hash: &ClientDataHash,
pin: &Option<Pin>,
) -> Result<Option<PinUvAuthParam>, AuthenticatorError>
where
Dev: FidoDevice,
{
// Not reusing the shared secret here, if it exists, since we might start again
// with a different PIN (e.g. if the last one was wrong)
let (shared_secret, info) = dev.establish_shared_secret()?;
// TODO(MS): What to do if token supports client_pin, but none has been set: Some(false)
// AND a Pin is not None?
if info.options.client_pin == Some(true) {
let pin = pin
.as_ref()
.ok_or(HIDError::Command(CommandError::StatusCode(
StatusCode::PinRequired,
None,
)))?;
let pin_command = GetPinToken::new(&shared_secret, pin);
let pin_token = dev.send_cbor(&pin_command)?;
Ok(Some(
pin_token
.derive(client_data_hash.as_ref())
.map_err(CommandError::Crypto)?,
))
} else {
Ok(None)
}
}

View File

@ -13,7 +13,7 @@ use crate::ctap2::server::{
};
use crate::errors::{AuthenticatorError, U2FTokenError};
use crate::statecallback::StateCallback;
use crate::{AttestationObject, CollectedClientDataWrapper, Pin, StatusUpdate};
use crate::{AttestationObject, CollectedClientDataWrapper, Pin, StatusPinUv, StatusUpdate};
use crate::{RegisterResult, SignResult};
use libc::size_t;
use rand::{thread_rng, Rng};
@ -338,7 +338,11 @@ pub unsafe extern "C" fn rust_ctap2_mgr_sign(
let alternate_rp_id = if alternate_relying_party_id.is_null() {
None
} else {
Some(CStr::from_ptr(alternate_relying_party_id).to_string_lossy().to_string())
Some(
CStr::from_ptr(alternate_relying_party_id)
.to_string_lossy()
.to_string(),
)
};
let origin = CStr::from_ptr(origin_ptr).to_string_lossy().to_string();
let challenge = from_raw(challenge.ptr, challenge.len);
@ -972,7 +976,8 @@ pub unsafe extern "C" fn rust_ctap2_status_update_send_pin(
}
match &*res {
StatusUpdate::PinError(_, sender) => {
StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))
| StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, _)) => {
if let Ok(pin) = CStr::from_ptr(c_pin).to_str() {
sender
.send(Pin::new(pin))

View File

@ -33,6 +33,7 @@ pub enum AuthenticatorError {
CryptoError,
PinError(PinError),
UnsupportedOption(UnsupportedOption),
CancelledByUser,
}
impl AuthenticatorError {
@ -69,10 +70,9 @@ impl fmt::Display for AuthenticatorError {
write!(f, "A u2f token error occurred {err:?}")
}
AuthenticatorError::Custom(ref err) => write!(f, "A custom error occurred {err:?}"),
AuthenticatorError::VersionMismatch(manager, version) => write!(
f,
"{manager} expected arguments of version CTAP{version}"
),
AuthenticatorError::VersionMismatch(manager, version) => {
write!(f, "{manager} expected arguments of version CTAP{version}")
}
AuthenticatorError::HIDError(ref e) => write!(f, "Device error: {e}"),
AuthenticatorError::CryptoError => {
write!(f, "The cryptography implementation encountered an error")
@ -81,6 +81,9 @@ impl fmt::Display for AuthenticatorError {
AuthenticatorError::UnsupportedOption(ref e) => {
write!(f, "Unsupported option: {e:?}")
}
AuthenticatorError::CancelledByUser => {
write!(f, "Cancelled by user.")
}
}
}
}

View File

@ -3,12 +3,14 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use crate::consts::PARAMETER_SIZE;
use crate::ctap2::commands::client_pin::{ChangeExistingPin, Pin, PinError, SetNewPin};
use crate::ctap2::commands::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::reset::Reset;
use crate::ctap2::commands::{
repackage_pin_errors, CommandError, PinAuthCommand, Request, StatusCode,
repackage_pin_errors, CommandError, PinUvAuthCommand, PinUvAuthResult, Request, StatusCode,
};
use crate::ctap2::server::{RelyingParty, RelyingPartyWrapper};
use crate::errors::{self, AuthenticatorError, UnsupportedOption};
@ -20,7 +22,7 @@ use crate::transport::platform::transaction::Transaction;
use crate::transport::{errors::HIDError, hid::HIDDevice, FidoDevice, Nonce};
use crate::u2fprotocol::{u2f_init_device, u2f_is_keyhandle_valid, u2f_register, u2f_sign};
use crate::u2ftypes::U2FDevice;
use crate::{send_status, RegisterResult, SignResult, StatusUpdate};
use crate::{send_status, RegisterResult, SignResult, StatusPinUv, StatusUpdate};
use std::sync::mpsc::{channel, Sender};
use std::thread;
use std::time::Duration;
@ -353,9 +355,7 @@ impl StateMachineCtap2 {
// We can be cancelled from the user (through keep_alive()) or from the device selector
// (through a DeviceCommand::Cancel on rx). We'll combine those signals into a single
// predicate to pass to Device::block_and_blink.
let keep_blinking = || {
keep_alive() && !matches!(rx.try_recv(), Ok(DeviceCommand::Cancel))
};
let keep_blinking = || keep_alive() && !matches!(rx.try_recv(), Ok(DeviceCommand::Cancel));
// Blocking recv. DeviceSelector will tell us what to do
match rx.recv() {
@ -392,58 +392,222 @@ impl StateMachineCtap2 {
}
fn ask_user_for_pin<U>(
error: PinError,
was_invalid: bool,
retries: Option<u8>,
status: &Sender<StatusUpdate>,
callback: &StateCallback<crate::Result<U>>,
) -> Result<Pin, ()> {
info!("PIN Error that requires user interaction detected. Sending it back and waiting for a reply");
let (tx, rx) = channel();
send_status(status, crate::StatusUpdate::PinError(error.clone(), tx));
if was_invalid {
send_status(
status,
crate::StatusUpdate::PinUvError(StatusPinUv::InvalidPin(tx, retries)),
);
} else {
send_status(
status,
crate::StatusUpdate::PinUvError(StatusPinUv::PinRequired(tx)),
);
}
match rx.recv() {
Ok(pin) => Ok(pin),
Err(_) => {
// recv() can only fail, if the other side is dropping the Sender. We are using this as a trick
// to let the callback decide if this PinError is recoverable (e.g. with User input) or not (e.g.
// locked token). If it is deemed unrecoverable, we error out the 'normal' way with the same error.
error!("Callback dropped the channel, so we forward the error to the results-callback: {:?}", error);
callback.call(Err(AuthenticatorError::PinError(error)));
// recv() can only fail, if the other side is dropping the Sender.
error!("Callback dropped the channel. Aborting.");
callback.call(Err(AuthenticatorError::CancelledByUser));
Err(())
}
}
}
fn determine_pin_auth<T: PinAuthCommand, U>(
/// Try to fetch PinUvAuthToken from the device and derive from it PinUvAuthParam.
/// Prefer UV, fallback to PIN.
/// Prefer newer pinUvAuth-methods, if supported by the device.
fn get_pin_uv_auth_param<T: PinUvAuthCommand + Request<V>, V>(
cmd: &mut T,
dev: &mut Device,
permission: PinUvAuthTokenPermission,
skip_uv: bool,
) -> Result<PinUvAuthResult, AuthenticatorError> {
let info = dev
.get_authenticator_info()
.ok_or(AuthenticatorError::HIDError(HIDError::DeviceNotInitialized))?;
// Only use UV, if the device supports it and we don't skip it
// which happens as a fallback, if UV-usage failed too many times
// Note: In theory, we could also repeatedly query GetInfo here and check
// if uv is set to Some(true), as tokens should set it to Some(false)
// if UV is blocked (too many failed attempts). But the CTAP2.0-spec is
// vague and I don't trust all tokens to implement it that way. So we
// keep track of it ourselves, using `skip_uv`.
let supports_uv = info.options.user_verification == Some(true);
let supports_pin = info.options.client_pin.is_some();
let pin_configured = info.options.client_pin == Some(true);
// 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)
return Ok(PinUvAuthResult::NoAuthTypeSupported);
}
let (res, pin_auth_token) = if info.options.pin_uv_auth_token == Some(true) {
if !skip_uv && supports_uv {
// CTAP 2.1 - UV
let pin_auth_token = dev
.get_pin_uv_auth_token_using_uv_with_permissions(permission, cmd.get_rp_id());
(
PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions,
pin_auth_token,
)
} else if supports_pin && pin_configured {
// CTAP 2.1 - PIN
let pin_auth_token = dev.get_pin_uv_auth_token_using_pin_with_permissions(
cmd.pin(),
permission,
cmd.get_rp_id(),
);
(
PinUvAuthResult::SuccessGetPinUvAuthTokenUsingPinWithPermissions,
pin_auth_token,
)
} else {
return Ok(PinUvAuthResult::NoAuthTypeSupported);
}
} else {
// CTAP 2.0 fallback
if !skip_uv && supports_uv && cmd.pin().is_none() {
// If the device supports internal user-verification (e.g. fingerprints),
// skip PIN-stuff
// We may need the shared secret for HMAC-extension, so we
// have to establish one
let _shared_secret = dev.establish_shared_secret()?;
return Ok(PinUvAuthResult::UsingInternalUv);
}
let pin_auth_token = dev.get_pin_token(cmd.pin());
(PinUvAuthResult::SuccessGetPinToken, pin_auth_token)
};
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)
}
/// PUAP, as per spec: PinUvAuthParam
/// Determines, if we need to establish a PinUvAuthParam, based on the
/// capabilities of the device and the incoming request.
/// If it is needed, tries to establish one and save it inside the Request.
/// Returns Ok() if we can proceed with sending the actual Request to
/// the device, Err() otherwise.
/// Handles asking the user for a PIN, if needed and sending StatusUpdates
/// regarding PIN and UV usage.
fn determine_puap_if_needed<T: PinUvAuthCommand + Request<V>, U, V>(
cmd: &mut T,
dev: &mut Device,
mut skip_uv: bool,
permission: PinUvAuthTokenPermission,
status: &Sender<StatusUpdate>,
callback: &StateCallback<crate::Result<U>>,
) -> Result<(), ()> {
loop {
match cmd.determine_pin_auth(dev) {
Ok(_) => {
break;
alive: &dyn Fn() -> bool,
) -> Result<PinUvAuthResult, ()> {
// Starting from a blank slate.
cmd.set_pin_uv_auth_param(None).map_err(|_| ())?;
if !cmd.is_ctap2_request() {
return Ok(PinUvAuthResult::RequestIsCtap1);
}
// CTAP1/U2F-only devices do not support PinUvAuth, so we skip it
if !dev.supports_ctap2() {
return Ok(PinUvAuthResult::DeviceIsCtap1);
}
// TODO: API needs a better way to express "uv = discouraged"
if cmd.get_uv_option() == Some(false) {
cmd.set_discouraged_uv_option();
return Ok(PinUvAuthResult::NoAuthRequired);
}
while alive() {
debug!("-----------------------------------------------------------------");
debug!("Getting pinUvAuthParam");
match Self::get_pin_uv_auth_param(cmd, dev, permission, skip_uv) {
Ok(r) => {
return Ok(r);
}
Err(AuthenticatorError::PinError(e)) => {
let pin = Self::ask_user_for_pin(e, status, callback)?;
cmd.set_pin(Some(pin));
continue;
Err(AuthenticatorError::PinError(PinError::PinRequired)) => {
if let Ok(pin) = Self::ask_user_for_pin(false, None, status, callback) {
cmd.set_pin(Some(pin));
skip_uv = true;
continue;
} else {
return Err(());
}
}
Err(AuthenticatorError::PinError(PinError::InvalidPin(retries))) => {
if let Ok(pin) = Self::ask_user_for_pin(true, retries, status, callback) {
cmd.set_pin(Some(pin));
continue;
} else {
return Err(());
}
}
Err(AuthenticatorError::PinError(PinError::InvalidUv(retries))) => {
if retries == Some(0) {
skip_uv = true;
}
send_status(
status,
StatusUpdate::PinUvError(StatusPinUv::InvalidUv(retries)),
)
}
Err(e @ AuthenticatorError::PinError(PinError::PinAuthBlocked)) => {
send_status(
status,
StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked),
);
error!("Error when determining pinAuth: {:?}", e);
callback.call(Err(e));
return Err(());
}
Err(e @ AuthenticatorError::PinError(PinError::PinBlocked)) => {
send_status(status, StatusUpdate::PinUvError(StatusPinUv::PinBlocked));
error!("Error when determining pinAuth: {:?}", e);
callback.call(Err(e));
return Err(());
}
Err(e @ AuthenticatorError::PinError(PinError::PinNotSet)) => {
send_status(status, StatusUpdate::PinUvError(StatusPinUv::PinNotSet));
error!("Error when determining pinAuth: {:?}", e);
callback.call(Err(e));
return Err(());
}
Err(AuthenticatorError::PinError(PinError::UvBlocked)) => {
skip_uv = true;
send_status(status, StatusUpdate::PinUvError(StatusPinUv::UvBlocked))
}
// Used for CTAP2.0 UV (fingerprints)
Err(AuthenticatorError::PinError(PinError::PinAuthInvalid)) => {
skip_uv = true;
send_status(
status,
StatusUpdate::PinUvError(StatusPinUv::InvalidUv(None)),
)
}
Err(e) => {
error!("Error when determining pinAuth: {:?}", e);
callback.call(Err(e));
return Err(());
}
};
}
}
// CTAP 2.0 spec is a bit vague here, but CTAP 2.1 is very specific, that the request
// should either include pinAuth OR uv=true, but not both at the same time.
// Do not set user_verification, if pinAuth is provided
if cmd.pin_auth().is_some() {
cmd.unset_uv_option();
}
Ok(())
Err(())
}
pub fn register(
@ -481,16 +645,6 @@ impl StateMachineCtap2 {
// return;
//}
// TODO(baloo): not sure about this, have to ask
// Iterate the exclude list and see if there are any matches.
// If so, we'll keep polling the device anyway to test for user
// consent, to be consistent with CTAP2 device behavior.
//let excluded = key_handles.iter().any(|key_handle| {
// is_valid_transport(key_handle.transports)
// && u2f_is_keyhandle_valid(dev, &challenge, &application, &key_handle.credential)
// .unwrap_or(false) /* no match on failure */
//});
// TODO(MS): This is wasteful, but the current setup with read only-functions doesn't allow me
// to modify "params" directly.
let mut makecred = params.clone();
@ -506,46 +660,96 @@ impl StateMachineCtap2 {
}
}
}
// Second, ask for PIN and get the shared secret
if Self::determine_pin_auth(&mut makecred, &mut dev, &status, &callback)
.is_err()
{
return;
}
}
debug!("------------------------------------------------------------------");
debug!("{:?}", makecred);
debug!("------------------------------------------------------------------");
let resp = dev.send_msg_cancellable(&makecred, alive);
if resp.is_ok() {
send_status(
let mut skip_uv = false;
while alive() {
let pin_uv_auth_result = match Self::determine_puap_if_needed(
&mut makecred,
&mut dev,
skip_uv,
PinUvAuthTokenPermission::MakeCredential,
&status,
crate::StatusUpdate::Success {
dev_info: dev.get_device_info(),
},
);
// The DeviceSelector could already be dead, but it might also wait
// for us to respond, in order to cancel all other tokens in case
// we skipped the "blinking"-action and went straight for the actual
// request.
let _ = selector.send(DeviceSelectorEvent::SelectedToken(dev.id()));
}
match resp {
Ok(MakeCredentialsResult::CTAP2(attestation, client_data)) => {
callback.call(Ok(RegisterResult::CTAP2(attestation, client_data)))
}
Ok(MakeCredentialsResult::CTAP1(data)) => {
callback.call(Ok(RegisterResult::CTAP1(data, dev.get_device_info())))
}
&callback,
alive,
) {
Ok(r) => r,
Err(()) => {
return;
}
};
Err(HIDError::Command(CommandError::StatusCode(
StatusCode::ChannelBusy,
_,
))) => {}
Err(e) => {
warn!("error happened: {}", e);
callback.call(Err(AuthenticatorError::HIDError(e)));
debug!("------------------------------------------------------------------");
debug!("{makecred:?} using {pin_uv_auth_result:?}");
debug!("------------------------------------------------------------------");
let resp = dev.send_msg_cancellable(&makecred, 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(MakeCredentialsResult::CTAP2(attestation, client_data)) => {
callback.call(Ok(RegisterResult::CTAP2(attestation, client_data)));
break;
}
Ok(MakeCredentialsResult::CTAP1(data)) => {
callback.call(Ok(RegisterResult::CTAP1(data, dev.get_device_info())));
break;
}
Err(HIDError::Command(CommandError::StatusCode(
StatusCode::ChannelBusy,
_,
))) => {
// Channel busy. Client SHOULD retry the request after a short delay.
thread::sleep(Duration::from_millis(100));
continue;
}
Err(HIDError::Command(CommandError::StatusCode(
StatusCode::PinAuthInvalid,
_,
))) if pin_uv_auth_result == PinUvAuthResult::UsingInternalUv => {
// This should only happen for CTAP2.0 tokens that use internal UV and
// failed (e.g. wrong fingerprint used), while doing MakeCredentials
send_status(
&status,
StatusUpdate::PinUvError(StatusPinUv::InvalidUv(None)),
);
continue;
}
Err(HIDError::Command(CommandError::StatusCode(
StatusCode::PinRequired,
_,
))) if pin_uv_auth_result == PinUvAuthResult::UsingInternalUv => {
// This should only happen for CTAP2.0 tokens that use internal UV and failed
// repeatedly, so that we have to fall back to PINs
skip_uv = true;
continue;
}
Err(HIDError::Command(CommandError::StatusCode(
StatusCode::UvBlocked,
_,
))) if pin_uv_auth_result
== PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions =>
{
// This should only happen for CTAP2.1 tokens that use internal UV and failed
// repeatedly, so that we have to fall back to PINs
skip_uv = true;
continue;
}
Err(e) => {
warn!("error happened: {e}");
callback.call(Err(AuthenticatorError::HIDError(e)));
break;
}
}
}
},
@ -593,80 +797,132 @@ impl StateMachineCtap2 {
}
}
}
}
let mut skip_uv = false;
while alive() {
let pin_uv_auth_result = match Self::determine_puap_if_needed(
&mut getassertion,
&mut dev,
skip_uv,
PinUvAuthTokenPermission::GetAssertion,
&status,
&callback,
alive,
) {
Ok(r) => r,
Err(()) => {
return;
}
};
// Second, ask for PIN and get the shared secret
if Self::determine_pin_auth(&mut getassertion, &mut dev, &status, &callback)
.is_err()
{
return;
}
// Third, use the shared secret in the extensions, if requested
if let Some(extension) = getassertion.extensions.hmac_secret.as_mut() {
if let Some(secret) = dev.get_shared_secret() {
match extension.calculate(secret) {
Ok(x) => x,
Err(e) => {
callback.call(Err(e));
return;
if params.is_ctap2_request() {
// Third, use the shared secret in the extensions, if requested
if let Some(extension) = getassertion.extensions.hmac_secret.as_mut() {
if let Some(secret) = dev.get_shared_secret() {
match extension.calculate(secret) {
Ok(x) => x,
Err(e) => {
callback.call(Err(e));
return;
}
}
}
}
}
}
debug!("------------------------------------------------------------------");
debug!("{:?}", getassertion);
debug!("------------------------------------------------------------------");
debug!("------------------------------------------------------------------");
debug!("{getassertion:?} using {pin_uv_auth_result:?}");
debug!("------------------------------------------------------------------");
let mut resp = dev.send_msg_cancellable(&getassertion, 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 {
id: alternate_rp_id,
..Default::default()
});
getassertion.alternate_rp_id = None;
resp = dev.send_msg_cancellable(&getassertion, alive);
let mut resp = dev.send_msg_cancellable(&getassertion, 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 {
id: alternate_rp_id,
..Default::default()
});
getassertion.alternate_rp_id = None;
resp = dev.send_msg_cancellable(&getassertion, 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(GetAssertionResult::CTAP1(resp)) => {
let app_id = getassertion.rp.hash().as_ref().to_vec();
let key_handle = getassertion.allow_list[0].id.clone();
if resp.is_ok() {
send_status(
&status,
crate::StatusUpdate::Success {
dev_info: dev.get_device_info(),
},
);
// The DeviceSelector could already be dead, but it might also wait
// for us to respond, in order to cancel all other tokens in case
// we skipped the "blinking"-action and went straight for the actual
// request.
let _ = selector.send(DeviceSelectorEvent::SelectedToken(dev.id()));
}
match resp {
Ok(GetAssertionResult::CTAP1(resp)) => {
let app_id = getassertion.rp.hash().as_ref().to_vec();
let key_handle = getassertion.allow_list[0].id.clone();
callback.call(Ok(SignResult::CTAP1(
app_id,
key_handle,
resp,
dev.get_device_info(),
)))
}
Ok(GetAssertionResult::CTAP2(assertion, client_data)) => {
callback.call(Ok(SignResult::CTAP2(assertion, client_data)))
}
Err(HIDError::Command(CommandError::StatusCode(
StatusCode::ChannelBusy,
_,
))) => {}
Err(e) => {
warn!("error happened: {}", e);
callback.call(Err(AuthenticatorError::HIDError(e)));
callback.call(Ok(SignResult::CTAP1(
app_id,
key_handle,
resp,
dev.get_device_info(),
)));
break;
}
Ok(GetAssertionResult::CTAP2(assertion, client_data)) => {
callback.call(Ok(SignResult::CTAP2(assertion, client_data)));
break;
}
Err(HIDError::Command(CommandError::StatusCode(
StatusCode::ChannelBusy,
_,
))) => {
// Channel busy. Client SHOULD retry the request after a short delay.
thread::sleep(Duration::from_millis(100));
continue;
}
Err(HIDError::Command(CommandError::StatusCode(
StatusCode::OperationDenied,
_,
))) if pin_uv_auth_result == PinUvAuthResult::UsingInternalUv => {
// This should only happen for CTAP2.0 tokens that use internal UV and failed
// (e.g. wrong fingerprint used), while doing GetAssertion
// Yes, this is a different error code than for MakeCredential.
send_status(
&status,
StatusUpdate::PinUvError(StatusPinUv::InvalidUv(None)),
);
continue;
}
Err(HIDError::Command(CommandError::StatusCode(
StatusCode::PinRequired,
_,
))) if pin_uv_auth_result == PinUvAuthResult::UsingInternalUv => {
// This should only happen for CTAP2.0 tokens that use internal UV and failed
// repeatedly, so that we have to fall back to PINs
skip_uv = true;
continue;
}
Err(HIDError::Command(CommandError::StatusCode(
StatusCode::UvBlocked,
_,
))) if pin_uv_auth_result
== PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions =>
{
// This should only happen for CTAP2.1 tokens that use internal UV and failed
// repeatedly, so that we have to fall back to PINs
skip_uv = true;
continue;
}
Err(e) => {
warn!("error happened: {e}");
callback.call(Err(AuthenticatorError::HIDError(e)));
break;
}
}
}
},
@ -768,7 +1024,7 @@ impl StateMachineCtap2 {
Some(dev) => dev,
};
let (mut shared_secret, authinfo) = match dev.establish_shared_secret() {
let mut shared_secret = match dev.establish_shared_secret() {
Ok(s) => s,
Err(e) => {
callback.call(Err(AuthenticatorError::HIDError(e)));
@ -776,6 +1032,14 @@ impl StateMachineCtap2 {
}
};
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)));
@ -792,9 +1056,15 @@ impl StateMachineCtap2 {
// Check if a client-pin is already set, or if a new one should be created
let res = if authinfo.options.client_pin.unwrap_or_default() {
let mut res;
let mut error = PinError::PinRequired;
let mut was_invalid = false;
let mut retries = None;
loop {
let current_pin = match Self::ask_user_for_pin(error, &status, &callback) {
let current_pin = match Self::ask_user_for_pin(
was_invalid,
retries,
&status,
&callback,
) {
Ok(pin) => pin,
_ => {
return;
@ -809,14 +1079,14 @@ impl StateMachineCtap2 {
)
.map_err(HIDError::Command)
.and_then(|msg| dev.send_cbor_cancellable(&msg, alive))
.map_err(AuthenticatorError::HIDError)
.map_err(|e| repackage_pin_errors(&mut dev, e));
if let Err(AuthenticatorError::PinError(e)) = res {
error = 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, _)) => {
Ok(s) => {
shared_secret = s;
}
Err(e) => {

View File

@ -1,7 +1,53 @@
use super::{u2ftypes, Pin, PinError};
use serde::ser::{Serialize, SerializeStruct};
use super::{u2ftypes, Pin};
use serde::{
ser::{Serialize, SerializeStruct},
Serialize as DeriveSer, Serializer,
};
use std::sync::mpsc::Sender;
// Simply ignoring the Sender when serializing
pub(crate) fn serialize_pin_required<S>(_: &Sender<Pin>, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
s.serialize_none()
}
// Simply ignoring the Sender when serializing
pub(crate) fn serialize_pin_invalid<S>(
_: &Sender<Pin>,
retries: &Option<u8>,
s: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if let Some(r) = retries {
s.serialize_u8(*r)
} else {
s.serialize_none()
}
}
#[derive(Debug, DeriveSer)]
pub enum StatusPinUv {
#[serde(serialize_with = "serialize_pin_required")]
PinRequired(Sender<Pin>),
#[serde(serialize_with = "serialize_pin_invalid")]
InvalidPin(Sender<Pin>, Option<u8>),
PinIsTooShort,
PinIsTooLong(usize),
InvalidUv(Option<u8>),
// This SHOULD ever only happen for CTAP2.0 devices that
// use internal UV (e.g. fingerprint sensors) and failed (e.g. wrong
// finger used).
// PinAuthInvalid, // Folded into InvalidUv
PinAuthBlocked,
PinBlocked,
PinNotSet,
UvBlocked,
}
#[derive(Debug)]
pub enum StatusUpdate {
/// Device found
@ -12,7 +58,7 @@ pub enum StatusUpdate {
Success { dev_info: u2ftypes::U2FDeviceInfo },
/// Sent if a PIN is needed (or was wrong), or some other kind of PIN-related
/// error occurred. The Sender is for sending back a PIN (if needed).
PinError(PinError, Sender<Pin>),
PinUvError(StatusPinUv),
/// Sent, if multiple devices are found and the user has to select one
SelectDeviceNotice,
/// Sent, once a device was selected (either automatically or by user-interaction)
@ -34,7 +80,7 @@ impl Serialize for StatusUpdate {
map.serialize_field("DeviceUnavailable", &dev_info)?
}
StatusUpdate::Success { dev_info } => map.serialize_field("Success", &dev_info)?,
StatusUpdate::PinError(e, _) => map.serialize_field("PinError", &e)?,
StatusUpdate::PinUvError(e) => map.serialize_field("PinUvError", &e)?,
StatusUpdate::SelectDeviceNotice => map.serialize_field("SelectDeviceNotice", &())?,
StatusUpdate::DeviceSelected(dev_info) => {
map.serialize_field("DeviceSelected", &dev_info)?
@ -66,15 +112,19 @@ pub mod tests {
}
#[test]
fn serialize_invalid_pin() {
fn serialize_status_pin_uv() {
let (tx, _rx) = channel();
let st = StatusUpdate::PinError(PinError::InvalidPin(Some(3)), tx.clone());
let st = StatusUpdate::PinUvError(StatusPinUv::InvalidPin(tx.clone(), Some(3)));
let json = to_string(&st).expect("Failed to serialize");
assert_eq!(&json, r#"{"PinError":{"InvalidPin":3}}"#);
assert_eq!(&json, r#"{"PinUvError":{"InvalidPin":3}}"#);
let st = StatusUpdate::PinError(PinError::InvalidPin(None), tx);
let st = StatusUpdate::PinUvError(StatusPinUv::InvalidPin(tx, None));
let json = to_string(&st).expect("Failed to serialize");
assert_eq!(&json, r#"{"PinError":{"InvalidPin":null}}"#);
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]

View File

@ -5,9 +5,10 @@
extern crate libc;
use crate::consts::{CID_BROADCAST, MAX_HID_RPT_SIZE};
use crate::ctap2::commands::get_info::AuthenticatorInfo;
use crate::transport::hid::HIDDevice;
use crate::transport::platform::uhid;
use crate::transport::{AuthenticatorInfo, FidoDevice, HIDError, SharedSecret};
use crate::transport::{FidoDevice, HIDError, SharedSecret};
use crate::u2ftypes::{U2FDevice, U2FDeviceInfo};
use crate::util::from_unix_result;
use crate::util::io_err;

View File

@ -4,9 +4,10 @@
extern crate libc;
use crate::consts::CID_BROADCAST;
use crate::ctap2::commands::get_info::AuthenticatorInfo;
use crate::transport::hid::HIDDevice;
use crate::transport::platform::{hidraw, monitor};
use crate::transport::{AuthenticatorInfo, FidoDevice, HIDError, SharedSecret};
use crate::transport::{FidoDevice, HIDError, SharedSecret};
use crate::u2ftypes::{U2FDevice, U2FDeviceInfo};
use crate::util::from_unix_result;
use std::fs::OpenOptions;

View File

@ -5,9 +5,10 @@
extern crate log;
use crate::consts::{CID_BROADCAST, MAX_HID_RPT_SIZE};
use crate::ctap2::commands::get_info::AuthenticatorInfo;
use crate::transport::hid::HIDDevice;
use crate::transport::platform::iokit::*;
use crate::transport::{AuthenticatorInfo, FidoDevice, HIDError, SharedSecret};
use crate::transport::{FidoDevice, HIDError, SharedSecret};
use crate::u2ftypes::{U2FDevice, U2FDeviceInfo};
use core_foundation::base::*;
use core_foundation::string::*;

View File

@ -1,8 +1,10 @@
use crate::consts::HIDCmd;
use crate::crypto::SharedSecret;
use crate::ctap2::commands::client_pin::GetKeyAgreement;
use crate::ctap2::commands::get_info::{AuthenticatorInfo, GetInfo};
use crate::crypto::{PinUvAuthProtocol, PinUvAuthToken, SharedSecret};
use crate::ctap2::commands::client_pin::{
GetKeyAgreement, GetPinToken, GetPinUvAuthTokenUsingPinWithPermissions,
GetPinUvAuthTokenUsingUvWithPermissions, PinUvAuthTokenPermission,
};
use crate::ctap2::commands::get_info::{AuthenticatorVersion, GetInfo};
use crate::ctap2::commands::get_version::GetVersion;
use crate::ctap2::commands::make_credentials::dummy_make_credentials_cmd;
use crate::ctap2::commands::selection::Selection;
@ -13,6 +15,8 @@ use crate::transport::device_selector::BlinkResult;
use crate::transport::errors::{ApduErrorStatus, HIDError};
use crate::transport::hid::HIDDevice;
use crate::util::io_err;
use crate::Pin;
use std::convert::TryFrom;
use std::thread;
use std::time::Duration;
@ -199,9 +203,9 @@ pub trait FidoDevice: HIDDevice {
}
fn block_and_blink(&mut self, keep_alive: &dyn Fn() -> bool) -> BlinkResult {
let supports_select_cmd = self
.get_authenticator_info()
.map_or(false, |i| i.versions.contains(&String::from("FIDO_2_1")));
let supports_select_cmd = self.get_authenticator_info().map_or(false, |i| {
i.versions.contains(&AuthenticatorVersion::FIDO_2_1)
});
let resp = if supports_select_cmd {
let msg = Selection {};
self.send_cbor_cancellable(&msg, keep_alive)
@ -246,29 +250,82 @@ pub trait FidoDevice: HIDDevice {
}
}
fn establish_shared_secret(&mut self) -> Result<(SharedSecret, AuthenticatorInfo), HIDError> {
fn establish_shared_secret(&mut self) -> Result<SharedSecret, HIDError> {
if !self.supports_ctap2() {
return Err(HIDError::UnsupportedCommand);
}
let info = if let Some(authenticator_info) = self.get_authenticator_info().cloned() {
authenticator_info
} else {
// We should already have it, since it is queried upon `init()`, but just to be safe
let info_command = GetInfo::default();
let info = self.send_cbor(&info_command)?;
debug!("infos: {:?}", info);
self.set_authenticator_info(info.clone());
info
};
let info = self
.get_authenticator_info()
.ok_or(HIDError::DeviceNotInitialized)?;
let pin_protocol = PinUvAuthProtocol::try_from(info)?;
// Not reusing the shared secret here, if it exists, since we might start again
// with a different PIN (e.g. if the last one was wrong)
let pin_command = GetKeyAgreement::new(&info)?;
let pin_command = GetKeyAgreement::new(pin_protocol);
let device_key_agreement = self.send_cbor(&pin_command)?;
let shared_secret = device_key_agreement.shared_secret()?;
self.set_shared_secret(shared_secret.clone());
Ok((shared_secret, info))
Ok(shared_secret)
}
/// CTAP 2.0-only version:
/// "Getting pinUvAuthToken using getPinToken (superseded)"
fn get_pin_token(&mut self, pin: &Option<Pin>) -> Result<PinUvAuthToken, HIDError> {
// Asking the user for PIN before establishing the shared secret
let pin = pin
.as_ref()
.ok_or(CommandError::StatusCode(StatusCode::PinRequired, None))?;
// Not reusing the shared secret here, if it exists, since we might start again
// with a different PIN (e.g. if the last one was wrong)
let shared_secret = self.establish_shared_secret()?;
let pin_command = GetPinToken::new(&shared_secret, pin);
let pin_token = self.send_cbor(&pin_command)?;
Ok(pin_token)
}
fn get_pin_uv_auth_token_using_uv_with_permissions(
&mut self,
permission: PinUvAuthTokenPermission,
rp_id: Option<&String>,
) -> Result<PinUvAuthToken, HIDError> {
// Explicitly not reusing the shared secret here
let shared_secret = self.establish_shared_secret()?;
let pin_command = GetPinUvAuthTokenUsingUvWithPermissions::new(
&shared_secret,
permission,
rp_id.cloned(),
);
let pin_auth_token = self.send_cbor(&pin_command)?;
Ok(pin_auth_token)
}
fn get_pin_uv_auth_token_using_pin_with_permissions(
&mut self,
pin: &Option<Pin>,
permission: PinUvAuthTokenPermission,
rp_id: Option<&String>,
) -> Result<PinUvAuthToken, HIDError> {
// Asking the user for PIN before establishing the shared secret
let pin = pin
.as_ref()
.ok_or(CommandError::StatusCode(StatusCode::PinRequired, None))?;
// Not reusing the shared secret here, if it exists, since we might start again
// with a different PIN (e.g. if the last one was wrong)
let shared_secret = self.establish_shared_secret()?;
let pin_command = GetPinUvAuthTokenUsingPinWithPermissions::new(
&shared_secret,
pin,
permission,
rp_id.cloned(),
);
let pin_auth_token = self.send_cbor(&pin_command)?;
Ok(pin_auth_token)
}
}

View File

@ -4,11 +4,12 @@
extern crate libc;
use crate::consts::{CID_BROADCAST, MAX_HID_RPT_SIZE};
use crate::ctap2::commands::get_info::AuthenticatorInfo;
use crate::transport::hid::HIDDevice;
use crate::transport::platform::fd::Fd;
use crate::transport::platform::monitor::WrappedOpenDevice;
use crate::transport::platform::uhid;
use crate::transport::{AuthenticatorInfo, FidoDevice, HIDError, SharedSecret};
use crate::transport::{FidoDevice, HIDError, SharedSecret};
use crate::u2ftypes::{U2FDevice, U2FDeviceInfo};
use crate::util::io_err;
use std::ffi::OsString;

View File

@ -4,9 +4,10 @@
extern crate libc;
use crate::consts::{CID_BROADCAST, MAX_HID_RPT_SIZE};
use crate::ctap2::commands::get_info::AuthenticatorInfo;
use crate::transport::hid::HIDDevice;
use crate::transport::platform::monitor::WrappedOpenDevice;
use crate::transport::{AuthenticatorInfo, FidoDevice, HIDError, SharedSecret};
use crate::transport::{FidoDevice, HIDError, SharedSecret};
use crate::u2ftypes::{U2FDevice, U2FDeviceInfo};
use crate::util::{from_unix_result, io_err};
use std::ffi::{CString, OsString};

View File

@ -2,9 +2,10 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use crate::ctap2::commands::get_info::AuthenticatorInfo;
use crate::transport::hid::HIDDevice;
use crate::transport::FidoDevice;
use crate::transport::{AuthenticatorInfo, HIDError, SharedSecret};
use crate::transport::{HIDError, SharedSecret};
use crate::u2ftypes::{U2FDevice, U2FDeviceInfo};
use std::hash::Hash;
use std::io;

View File

@ -4,8 +4,9 @@
use super::winapi::DeviceCapabilities;
use crate::consts::{CID_BROADCAST, FIDO_USAGE_PAGE, FIDO_USAGE_U2FHID, MAX_HID_RPT_SIZE};
use crate::ctap2::commands::get_info::AuthenticatorInfo;
use crate::transport::hid::HIDDevice;
use crate::transport::{AuthenticatorInfo, FidoDevice, HIDError, SharedSecret};
use crate::transport::{FidoDevice, HIDError, SharedSecret};
use crate::u2ftypes::{U2FDevice, U2FDeviceInfo};
use std::fs::{File, OpenOptions};
use std::hash::{Hash, Hasher};

View File

@ -216,7 +216,7 @@ impl DeviceInterfaceDetailData {
}
fn path(&self) -> String {
unsafe { from_wide_ptr((*self.data).DevicePath.as_ptr(), self.path_len - 2) }
unsafe { from_wide_ptr(ptr::addr_of!((*self.data).DevicePath[0]), self.path_len - 2) }
}
}