Bug 1867353 - vendor authenticator-rs v0.4.0-alpha.24. r=keeler,supply-chain-reviewers
Differential Revision: https://phabricator.services.mozilla.com/D195024
This commit is contained in:
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -302,9 +302,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "authenticator"
|
||||
version = "0.4.0-alpha.23"
|
||||
version = "0.4.0-alpha.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c0ae75b6f32065fdecb5f1f0d4b8cbe60d2828e9cde7892d589ae9d99c16af5"
|
||||
checksum = "be346361f2602704c3a48d71530df852a59558b9774a144432d91fdfe775f298"
|
||||
dependencies = [
|
||||
"base64 0.21.3",
|
||||
"bitflags 1.999.999",
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
authors = ["Martin Sirringhaus", "John Schanck"]
|
||||
|
||||
[dependencies]
|
||||
authenticator = { version = "0.4.0-alpha.23", features = ["gecko"] }
|
||||
authenticator = { version = "0.4.0-alpha.24", features = ["gecko"] }
|
||||
base64 = "^0.21"
|
||||
cstr = "0.2"
|
||||
log = "0.4"
|
||||
|
||||
@@ -57,6 +57,13 @@ user-id = 175410
|
||||
user-login = "jschanck"
|
||||
user-name = "John Schanck"
|
||||
|
||||
[[publisher.authenticator]]
|
||||
version = "0.4.0-alpha.24"
|
||||
when = "2023-11-29"
|
||||
user-id = 175410
|
||||
user-login = "jschanck"
|
||||
user-name = "John Schanck"
|
||||
|
||||
[[publisher.bhttp]]
|
||||
version = "0.3.1"
|
||||
when = "2023-02-23"
|
||||
|
||||
File diff suppressed because one or more lines are too long
2
third_party/rust/authenticator/Cargo.lock
generated
vendored
2
third_party/rust/authenticator/Cargo.lock
generated
vendored
@@ -39,7 +39,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "authenticator"
|
||||
version = "0.4.0-alpha.23"
|
||||
version = "0.4.0-alpha.24"
|
||||
dependencies = [
|
||||
"assert_matches",
|
||||
"base64",
|
||||
|
||||
2
third_party/rust/authenticator/Cargo.toml
vendored
2
third_party/rust/authenticator/Cargo.toml
vendored
@@ -12,7 +12,7 @@
|
||||
[package]
|
||||
edition = "2018"
|
||||
name = "authenticator"
|
||||
version = "0.4.0-alpha.23"
|
||||
version = "0.4.0-alpha.24"
|
||||
authors = [
|
||||
"J.C. Jones <jc@mozilla.com>",
|
||||
"Tim Taubert <ttaubert@mozilla.com>",
|
||||
|
||||
5
third_party/rust/authenticator/rust-toolchain.toml
vendored
Normal file
5
third_party/rust/authenticator/rust-toolchain.toml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
[toolchain]
|
||||
# Current rust version for mozilla-central
|
||||
# https://firefox-source-docs.mozilla.org/writing-rust-code/update-policy.html
|
||||
channel = "1.73.0"
|
||||
components = ["rustfmt", "clippy", "rust-analyzer"]
|
||||
@@ -289,8 +289,6 @@ impl Serialize for AuthenticatorData {
|
||||
data.extend([self.flags.bits()]); // (2) "flags", len=1 (u8)
|
||||
data.extend(self.counter.to_be_bytes()); // (3) "signCount", len=4, 32-bit unsigned big-endian integer.
|
||||
|
||||
// TODO(MS): Here flags=AT needs to be set, but this data comes from the security device
|
||||
// and we (probably?) need to just trust the device to set the right flags
|
||||
if let Some(cred) = &self.credential_data {
|
||||
// see https://www.w3.org/TR/webauthn-2/#sctn-attested-credential-data
|
||||
// Attested Credential Data
|
||||
@@ -308,9 +306,12 @@ impl Serialize for AuthenticatorData {
|
||||
.map_err(|_| SerError::custom("Failed to serialize auth_data"))?,
|
||||
);
|
||||
}
|
||||
// TODO(MS): Here flags=ED needs to be set, but this data comes from the security device
|
||||
// and we (probably?) need to just trust the device to set the right flags
|
||||
if self.extensions.has_some() {
|
||||
// If we have parsed extension data, then we should serialize it even if the authenticator
|
||||
// failed to set the extension data flag.
|
||||
// If we don't have parsed extension data, then what we output depends on the flag.
|
||||
// If the flag is set, we output the empty CBOR map. If it is not set, we output nothing.
|
||||
if self.extensions.has_some() || self.flags.contains(AuthenticatorDataFlags::EXTENSION_DATA)
|
||||
{
|
||||
data.extend(
|
||||
// (5) "extensions", len=variable
|
||||
&serde_cbor::to_vec(&self.extensions)
|
||||
@@ -1078,6 +1079,29 @@ pub mod test {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_extension_data() {
|
||||
let mut parsed_auth_data: AuthenticatorData =
|
||||
from_slice(&SAMPLE_AUTH_DATA_MAKE_CREDENTIAL).unwrap();
|
||||
assert!(parsed_auth_data
|
||||
.flags
|
||||
.contains(AuthenticatorDataFlags::EXTENSION_DATA));
|
||||
|
||||
// Remove the extension data but keep the extension data flag set.
|
||||
parsed_auth_data.extensions = Default::default();
|
||||
let with_flag = to_vec(&parsed_auth_data).expect("could not serialize auth data");
|
||||
// The serialized auth data should end with an empty map (CBOR 0xA0).
|
||||
assert_eq!(with_flag[with_flag.len() - 1], 0xA0);
|
||||
|
||||
// Remove the extension data flag.
|
||||
parsed_auth_data
|
||||
.flags
|
||||
.remove(AuthenticatorDataFlags::EXTENSION_DATA);
|
||||
let without_flag = to_vec(&parsed_auth_data).expect("could not serialize auth data");
|
||||
// The serialized auth data should be one byte shorter.
|
||||
assert!(with_flag.len() == without_flag.len() + 1);
|
||||
}
|
||||
|
||||
/// See: https://github.com/mozilla/authenticator-rs/issues/187
|
||||
#[test]
|
||||
fn test_aaguid_output() {
|
||||
|
||||
@@ -14,7 +14,7 @@ use serde_bytes::ByteBuf;
|
||||
use serde_cbor::{from_slice, to_vec, Value};
|
||||
use std::fmt;
|
||||
|
||||
use super::{Command, CommandError, PinUvAuthCommand, RequestCtap2, StatusCode};
|
||||
use super::{Command, CommandError, CtapResponse, PinUvAuthCommand, RequestCtap2, StatusCode};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum BioEnrollmentModality {
|
||||
@@ -518,6 +518,8 @@ pub struct BioEnrollmentResponse {
|
||||
pub(crate) max_template_friendly_name: Option<u64>,
|
||||
}
|
||||
|
||||
impl CtapResponse for BioEnrollmentResponse {}
|
||||
|
||||
impl<'de> Deserialize<'de> for BioEnrollmentResponse {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#![allow(non_upper_case_globals)]
|
||||
use super::CtapResponse;
|
||||
// Note: Needed for PinUvAuthTokenPermission
|
||||
// The current version of `bitflags` doesn't seem to allow
|
||||
// to set this for an individual bitflag-struct.
|
||||
@@ -155,6 +156,8 @@ pub struct ClientPinResponse {
|
||||
pub uv_retries: Option<u8>,
|
||||
}
|
||||
|
||||
impl CtapResponse for ClientPinResponse {}
|
||||
|
||||
impl<'de> Deserialize<'de> for ClientPinResponse {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{Command, CommandError, PinUvAuthCommand, RequestCtap2, StatusCode};
|
||||
use super::{Command, CommandError, CtapResponse, PinUvAuthCommand, RequestCtap2, StatusCode};
|
||||
use crate::{
|
||||
crypto::{COSEKey, PinUvAuthParam, PinUvAuthToken},
|
||||
ctap2::server::{
|
||||
@@ -171,6 +171,8 @@ pub struct CredentialManagementResponse {
|
||||
pub large_blob_key: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl CtapResponse for CredentialManagementResponse {}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct CredentialRpListEntry {
|
||||
/// RP Information
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::get_info::AuthenticatorInfo;
|
||||
use super::{
|
||||
Command, CommandError, PinUvAuthCommand, RequestCtap1, RequestCtap2, Retryable, StatusCode,
|
||||
Command, CommandError, CtapResponse, PinUvAuthCommand, RequestCtap1, RequestCtap2, Retryable,
|
||||
StatusCode,
|
||||
};
|
||||
use crate::consts::{
|
||||
PARAMETER_SIZE, U2F_AUTHENTICATE, U2F_DONT_ENFORCE_USER_PRESENCE_AND_SIGN,
|
||||
@@ -292,6 +293,9 @@ impl Serialize for GetAssertion {
|
||||
}
|
||||
}
|
||||
|
||||
type GetAssertionOutput = Vec<GetAssertionResult>;
|
||||
impl CtapResponse for GetAssertionOutput {}
|
||||
|
||||
impl RequestCtap1 for GetAssertion {
|
||||
type Output = Vec<GetAssertionResult>;
|
||||
type AdditionalInfo = PublicKeyCredentialDescriptor;
|
||||
@@ -508,6 +512,7 @@ impl GetAssertionResult {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct GetAssertionResponse {
|
||||
pub credentials: Option<PublicKeyCredentialDescriptor>,
|
||||
pub auth_data: AuthenticatorData,
|
||||
@@ -516,6 +521,8 @@ pub struct GetAssertionResponse {
|
||||
pub number_of_credentials: Option<usize>,
|
||||
}
|
||||
|
||||
impl CtapResponse for GetAssertionResponse {}
|
||||
|
||||
impl<'de> Deserialize<'de> for GetAssertionResponse {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{Command, CommandError, RequestCtap2, StatusCode};
|
||||
use super::{Command, CommandError, CtapResponse, RequestCtap2, StatusCode};
|
||||
use crate::ctap2::attestation::AAGuid;
|
||||
use crate::ctap2::server::PublicKeyCredentialParameters;
|
||||
use crate::transport::errors::HIDError;
|
||||
@@ -368,6 +368,8 @@ impl AuthenticatorInfo {
|
||||
}
|
||||
}
|
||||
|
||||
impl CtapResponse for AuthenticatorInfo {}
|
||||
|
||||
macro_rules! parse_next_optional_value {
|
||||
($name:expr, $map:expr) => {
|
||||
if $name.is_some() {
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
use super::{CommandError, RequestCtap1, Retryable};
|
||||
use super::{CommandError, CtapResponse, RequestCtap1, Retryable};
|
||||
use crate::consts::U2F_VERSION;
|
||||
use crate::transport::errors::{ApduErrorStatus, HIDError};
|
||||
use crate::transport::{FidoDevice, VirtualFidoDevice};
|
||||
use crate::u2ftypes::CTAP1RequestAPDU;
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum U2FInfo {
|
||||
U2F_V2,
|
||||
}
|
||||
|
||||
impl CtapResponse for U2FInfo {}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
// TODO(baloo): if one does not issue U2F_VERSION before makecredentials or getassertion, token
|
||||
// will return error (ConditionsNotSatified), test this in unit tests
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::get_info::{AuthenticatorInfo, AuthenticatorVersion};
|
||||
use super::{
|
||||
Command, CommandError, PinUvAuthCommand, RequestCtap1, RequestCtap2, Retryable, StatusCode,
|
||||
Command, CommandError, CtapResponse, PinUvAuthCommand, RequestCtap1, RequestCtap2, Retryable,
|
||||
StatusCode,
|
||||
};
|
||||
use crate::consts::{PARAMETER_SIZE, U2F_REGISTER, U2F_REQUEST_USER_PRESENCE};
|
||||
use crate::crypto::{
|
||||
@@ -199,6 +200,8 @@ impl<'de> Deserialize<'de> for MakeCredentialsResult {
|
||||
}
|
||||
}
|
||||
|
||||
impl CtapResponse for MakeCredentialsResult {}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize)]
|
||||
#[cfg_attr(test, derive(Deserialize))]
|
||||
pub struct MakeCredentialsOptions {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use crate::crypto::{CryptoError, PinUvAuthParam, PinUvAuthToken};
|
||||
use crate::ctap2::commands::client_pin::{
|
||||
ClientPinResponse, GetPinRetries, GetUvRetries, PinError,
|
||||
};
|
||||
use crate::ctap2::commands::client_pin::{GetPinRetries, GetUvRetries, PinError};
|
||||
use crate::ctap2::commands::get_info::AuthenticatorInfo;
|
||||
use crate::ctap2::server::UserVerificationRequirement;
|
||||
use crate::errors::AuthenticatorError;
|
||||
@@ -50,7 +48,7 @@ impl<T> From<T> for Retryable<T> {
|
||||
}
|
||||
|
||||
pub trait RequestCtap1: fmt::Debug {
|
||||
type Output;
|
||||
type Output: CtapResponse;
|
||||
// E.g.: For GetAssertion, which key-handle is currently being tested
|
||||
type AdditionalInfo;
|
||||
|
||||
@@ -75,7 +73,7 @@ pub trait RequestCtap1: fmt::Debug {
|
||||
}
|
||||
|
||||
pub trait RequestCtap2: fmt::Debug {
|
||||
type Output;
|
||||
type Output: CtapResponse;
|
||||
|
||||
fn command(&self) -> Command;
|
||||
|
||||
@@ -93,6 +91,10 @@ pub trait RequestCtap2: fmt::Debug {
|
||||
) -> Result<Self::Output, HIDError>;
|
||||
}
|
||||
|
||||
// Sadly, needs to be 'static to enable us in tests to collect them in a Vec
|
||||
// but all of them are 'static, so this is currently no problem.
|
||||
pub trait CtapResponse: std::fmt::Debug + 'static {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PinUvAuthResult {
|
||||
/// Request is CTAP1 and does not need PinUvAuth
|
||||
@@ -158,7 +160,7 @@ pub(crate) fn repackage_pin_errors<D: FidoDevice>(
|
||||
let cmd = GetPinRetries::new();
|
||||
// Treat any error as if the device returned a valid response without a pinRetries
|
||||
// field.
|
||||
let resp = dev.send_cbor(&cmd).unwrap_or(ClientPinResponse::default());
|
||||
let resp = dev.send_cbor(&cmd).unwrap_or_default();
|
||||
AuthenticatorError::PinError(PinError::InvalidPin(resp.pin_retries))
|
||||
}
|
||||
HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthBlocked, _)) => {
|
||||
@@ -181,7 +183,7 @@ pub(crate) fn repackage_pin_errors<D: FidoDevice>(
|
||||
let cmd = GetUvRetries::new();
|
||||
// Treat any error as if the device returned a valid response without a uvRetries
|
||||
// field.
|
||||
let resp = dev.send_cbor(&cmd).unwrap_or(ClientPinResponse::default());
|
||||
let resp = dev.send_cbor(&cmd).unwrap_or_default();
|
||||
AuthenticatorError::PinError(PinError::InvalidUv(resp.uv_retries))
|
||||
}
|
||||
HIDError::Command(CommandError::StatusCode(StatusCode::UvBlocked, _)) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::client_data::ClientDataHash;
|
||||
use super::commands::get_assertion::{GetAssertion, GetAssertionExtensions, GetAssertionOptions};
|
||||
use super::commands::{PinUvAuthCommand, RequestCtap1, Retryable};
|
||||
use super::commands::{CtapResponse, PinUvAuthCommand, RequestCtap1, Retryable};
|
||||
use crate::consts::{PARAMETER_SIZE, U2F_AUTHENTICATE, U2F_CHECK_IS_REGISTERED};
|
||||
use crate::crypto::PinUvAuthToken;
|
||||
use crate::ctap2::server::{PublicKeyCredentialDescriptor, RelyingParty};
|
||||
@@ -22,8 +22,11 @@ pub struct CheckKeyHandle<'assertion> {
|
||||
pub rp: &'assertion RelyingParty,
|
||||
}
|
||||
|
||||
type EmptyResponse = ();
|
||||
impl CtapResponse for EmptyResponse {}
|
||||
|
||||
impl<'assertion> RequestCtap1 for CheckKeyHandle<'assertion> {
|
||||
type Output = ();
|
||||
type Output = EmptyResponse;
|
||||
type AdditionalInfo = ();
|
||||
|
||||
fn ctap1_format(&self) -> Result<(Vec<u8>, Self::AdditionalInfo), HIDError> {
|
||||
@@ -167,7 +170,16 @@ pub(crate) fn do_credential_list_filtering_ctap2<Dev: FidoDevice>(
|
||||
// Filter out all credentials the device returned. Those are valid.
|
||||
let credential_ids = response
|
||||
.iter_mut()
|
||||
.filter_map(|result| result.assertion.credentials.take())
|
||||
.filter_map(|result| {
|
||||
// CTAP 2.0 devices can omit the credentials in their response,
|
||||
// if the given allowList was only 1 entry long. If so, we have
|
||||
// to fill it in ourselfs.
|
||||
if chunk.len() == 1 && result.assertion.credentials.is_none() {
|
||||
Some(chunk[0].clone())
|
||||
} else {
|
||||
result.assertion.credentials.take()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
// Replace credential_id_list with the valid credentials
|
||||
final_list = credential_ids;
|
||||
@@ -205,3 +217,314 @@ pub(crate) fn silently_discover_credentials<Dev: FidoDevice>(
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
crypto::{COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve},
|
||||
ctap2::{
|
||||
attestation::{
|
||||
AAGuid, AttestedCredentialData, AuthenticatorData, AuthenticatorDataFlags,
|
||||
Extension,
|
||||
},
|
||||
commands::{CommandError, StatusCode},
|
||||
server::{AuthenticationExtensionsClientOutputs, AuthenticatorAttachment, Transport},
|
||||
},
|
||||
transport::{
|
||||
device_selector::tests::{make_device_simple_u2f, make_device_with_pin},
|
||||
hid::HIDDevice,
|
||||
platform::device::Device,
|
||||
},
|
||||
Assertion, GetAssertionResult,
|
||||
};
|
||||
|
||||
fn new_relying_party(name: &str) -> RelyingParty {
|
||||
RelyingParty {
|
||||
id: String::from(name),
|
||||
name: Some(String::from(name)),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_silent_assert(
|
||||
rp: &RelyingParty,
|
||||
allow_list: &[PublicKeyCredentialDescriptor],
|
||||
) -> GetAssertion {
|
||||
GetAssertion::new(
|
||||
ClientDataHash(Sha256::digest("").into()),
|
||||
rp.clone(),
|
||||
allow_list.to_vec(),
|
||||
GetAssertionOptions {
|
||||
user_verification: None, // defaults to Some(false) if puap is absent
|
||||
user_presence: Some(false),
|
||||
},
|
||||
GetAssertionExtensions::default(),
|
||||
)
|
||||
}
|
||||
|
||||
fn new_credential(fill: u8, repeat: usize) -> PublicKeyCredentialDescriptor {
|
||||
PublicKeyCredentialDescriptor {
|
||||
id: vec![fill; repeat],
|
||||
transports: vec![Transport::USB],
|
||||
}
|
||||
}
|
||||
|
||||
fn new_assertion_response(
|
||||
rp: &RelyingParty,
|
||||
cred: Option<&PublicKeyCredentialDescriptor>,
|
||||
) -> GetAssertionResult {
|
||||
let credential_data = cred.map(|cred| AttestedCredentialData {
|
||||
aaguid: AAGuid::default(),
|
||||
credential_id: cred.id.clone(),
|
||||
credential_public_key: COSEKey {
|
||||
alg: COSEAlgorithm::RS256,
|
||||
key: COSEKeyType::EC2(COSEEC2Key {
|
||||
curve: Curve::SECP256R1,
|
||||
x: vec![],
|
||||
y: vec![],
|
||||
}),
|
||||
},
|
||||
});
|
||||
GetAssertionResult {
|
||||
assertion: Assertion {
|
||||
credentials: cred.cloned(),
|
||||
auth_data: AuthenticatorData {
|
||||
rp_id_hash: rp.hash(),
|
||||
flags: AuthenticatorDataFlags::empty(),
|
||||
counter: 0,
|
||||
credential_data,
|
||||
extensions: Extension::default(),
|
||||
},
|
||||
signature: vec![],
|
||||
user: None,
|
||||
},
|
||||
attachment: AuthenticatorAttachment::Platform,
|
||||
extensions: AuthenticationExtensionsClientOutputs::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_check_key_handle<'a>(
|
||||
rp: &'a RelyingParty,
|
||||
client_data_hash: &'a ClientDataHash,
|
||||
cred: &'a PublicKeyCredentialDescriptor,
|
||||
) -> CheckKeyHandle<'a> {
|
||||
CheckKeyHandle {
|
||||
key_handle: cred.id.as_ref(),
|
||||
client_data_hash: client_data_hash.as_ref(),
|
||||
rp,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preflight_ctap1_empty() {
|
||||
let mut dev = Device::new("preflight").unwrap();
|
||||
make_device_simple_u2f(&mut dev);
|
||||
let client_data_hash = ClientDataHash(Sha256::digest("").into());
|
||||
let rp = new_relying_party("preflight test");
|
||||
let res = silently_discover_credentials(&mut dev, &[], &rp, &client_data_hash);
|
||||
assert!(res.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preflight_ctap1_multiple_replies() {
|
||||
let mut dev = Device::new_skipping_serialization("preflight").unwrap();
|
||||
make_device_simple_u2f(&mut dev);
|
||||
let rp = new_relying_party("preflight test");
|
||||
let cdh = ClientDataHash(Sha256::digest("").into());
|
||||
let allow_list = vec![
|
||||
new_credential(4, 4),
|
||||
new_credential(3, 4),
|
||||
new_credential(2, 4),
|
||||
new_credential(1, 4),
|
||||
];
|
||||
dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[0]));
|
||||
dev.add_upcoming_ctap_error(HIDError::ApduStatus(
|
||||
ApduErrorStatus::WrongData, // Not a registered cred
|
||||
));
|
||||
dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[1]));
|
||||
dev.add_upcoming_ctap_error(HIDError::ApduStatus(
|
||||
ApduErrorStatus::WrongData, // Not a registered cred
|
||||
));
|
||||
dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[2]));
|
||||
dev.add_upcoming_ctap_response(()); // Valid credential - the code exits here now and doesn't even look at the last one
|
||||
|
||||
let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &cdh);
|
||||
assert_eq!(res, vec![allow_list[2].clone()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preflight_ctap1_too_long_entries() {
|
||||
let mut dev = Device::new_skipping_serialization("preflight").unwrap();
|
||||
make_device_simple_u2f(&mut dev);
|
||||
let rp = new_relying_party("preflight test");
|
||||
let cdh = ClientDataHash(Sha256::digest("").into());
|
||||
let allow_list = vec![
|
||||
new_credential(4, 300), // ctap1 limit is 256
|
||||
new_credential(3, 4),
|
||||
new_credential(2, 4),
|
||||
new_credential(1, 4),
|
||||
];
|
||||
// allow_list[0] is filtered out due to its size
|
||||
dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[1]));
|
||||
dev.add_upcoming_ctap_error(HIDError::ApduStatus(
|
||||
ApduErrorStatus::WrongData, // Not a registered cred
|
||||
));
|
||||
dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[2]));
|
||||
dev.add_upcoming_ctap_response(()); // Valid credential - the code exits here now and doesn't even look at the last one
|
||||
|
||||
let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &cdh);
|
||||
assert_eq!(res, vec![allow_list[2].clone()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preflight_ctap2_empty() {
|
||||
let mut dev = Device::new("preflight").unwrap();
|
||||
make_device_with_pin(&mut dev);
|
||||
let rp = new_relying_party("preflight test");
|
||||
let client_data_hash = ClientDataHash(Sha256::digest("").into());
|
||||
let res = silently_discover_credentials(&mut dev, &[], &rp, &client_data_hash);
|
||||
assert!(res.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preflight_ctap20_no_cred_data() {
|
||||
// CTAP2.0 tokens are allowed to not send any credential-data in their
|
||||
// response, if the allow-list is of length one. See https://github.com/mozilla/authenticator-rs/issues/319
|
||||
let mut dev = Device::new_skipping_serialization("preflight").unwrap();
|
||||
make_device_with_pin(&mut dev);
|
||||
let rp = new_relying_party("preflight test");
|
||||
let client_data_hash = ClientDataHash(Sha256::digest("").into());
|
||||
let allow_list = vec![new_credential(1, 4)];
|
||||
dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &allow_list));
|
||||
dev.add_upcoming_ctap_response(vec![new_assertion_response(&rp, None)]);
|
||||
let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash);
|
||||
assert_eq!(res, allow_list);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preflight_ctap2_one_valid_entry() {
|
||||
let mut dev = Device::new_skipping_serialization("preflight").unwrap();
|
||||
make_device_with_pin(&mut dev);
|
||||
let rp = new_relying_party("preflight test");
|
||||
let client_data_hash = ClientDataHash(Sha256::digest("").into());
|
||||
let allow_list = vec![new_credential(1, 4)];
|
||||
dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &allow_list));
|
||||
dev.add_upcoming_ctap_response(vec![new_assertion_response(&rp, Some(&allow_list[0]))]);
|
||||
let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash);
|
||||
assert_eq!(res, allow_list);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preflight_ctap2_multiple_entries() {
|
||||
let mut dev = Device::new_skipping_serialization("preflight").unwrap();
|
||||
make_device_with_pin(&mut dev);
|
||||
let rp = new_relying_party("preflight test");
|
||||
let client_data_hash = ClientDataHash(Sha256::digest("").into());
|
||||
let allow_list = vec![
|
||||
new_credential(3, 4),
|
||||
new_credential(2, 4),
|
||||
new_credential(1, 4),
|
||||
new_credential(0, 4),
|
||||
];
|
||||
// Our test device doesn't say how many allow_list-entries it supports, so our code
|
||||
// defaults to one. Thus three requests, with three answers. Only one of them
|
||||
// valid.
|
||||
dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &[allow_list[0].clone()]));
|
||||
dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &[allow_list[1].clone()]));
|
||||
dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &[allow_list[2].clone()]));
|
||||
dev.add_upcoming_ctap_error(HIDError::Command(CommandError::StatusCode(
|
||||
StatusCode::NoCredentials,
|
||||
None,
|
||||
)));
|
||||
dev.add_upcoming_ctap_error(HIDError::Command(CommandError::StatusCode(
|
||||
StatusCode::NoCredentials,
|
||||
None,
|
||||
)));
|
||||
dev.add_upcoming_ctap_response(vec![new_assertion_response(&rp, Some(&allow_list[2]))]);
|
||||
let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash);
|
||||
assert_eq!(res, vec![allow_list[2].clone()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preflight_ctap2_multiple_replies() {
|
||||
let mut dev = Device::new_skipping_serialization("preflight").unwrap();
|
||||
make_device_with_pin(&mut dev);
|
||||
let rp = new_relying_party("preflight test");
|
||||
let client_data_hash = ClientDataHash(Sha256::digest("").into());
|
||||
let allow_list = vec![
|
||||
new_credential(4, 4),
|
||||
new_credential(3, 4),
|
||||
new_credential(2, 4),
|
||||
new_credential(1, 4),
|
||||
];
|
||||
let mut info = dev.get_authenticator_info().unwrap().clone();
|
||||
info.max_credential_count_in_list = Some(5);
|
||||
dev.set_authenticator_info(info);
|
||||
// Our test device now says that it supports 5 allow_list-entries,
|
||||
// so we can send all of them in one request
|
||||
dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &allow_list));
|
||||
dev.add_upcoming_ctap_response(vec![
|
||||
new_assertion_response(&rp, Some(&allow_list[1])),
|
||||
new_assertion_response(&rp, Some(&allow_list[2])),
|
||||
new_assertion_response(&rp, Some(&allow_list[3])),
|
||||
]);
|
||||
let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash);
|
||||
assert_eq!(res, allow_list[1..].to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preflight_ctap2_multiple_replies_some_invalid() {
|
||||
let mut dev = Device::new_skipping_serialization("preflight").unwrap();
|
||||
make_device_with_pin(&mut dev);
|
||||
let rp = new_relying_party("preflight test");
|
||||
let client_data_hash = ClientDataHash(Sha256::digest("").into());
|
||||
let allow_list = vec![
|
||||
new_credential(4, 4),
|
||||
new_credential(3, 4),
|
||||
new_credential(2, 4),
|
||||
new_credential(1, 4),
|
||||
];
|
||||
let mut info = dev.get_authenticator_info().unwrap().clone();
|
||||
info.max_credential_count_in_list = Some(5);
|
||||
dev.set_authenticator_info(info);
|
||||
// Our test device now says that it supports 5 allow_list-entries,
|
||||
// so we can send all of them in one request
|
||||
dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &allow_list));
|
||||
dev.add_upcoming_ctap_response(vec![
|
||||
new_assertion_response(&rp, Some(&allow_list[1])),
|
||||
new_assertion_response(&rp, None), // This will be ignored
|
||||
new_assertion_response(&rp, Some(&allow_list[2])),
|
||||
new_assertion_response(&rp, None), // This will be ignored
|
||||
]);
|
||||
let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash);
|
||||
assert_eq!(res, allow_list[1..=2].to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preflight_ctap2_too_long_entries() {
|
||||
let mut dev = Device::new_skipping_serialization("preflight").unwrap();
|
||||
make_device_with_pin(&mut dev);
|
||||
let rp = new_relying_party("preflight test");
|
||||
let client_data_hash = ClientDataHash(Sha256::digest("").into());
|
||||
let allow_list = vec![
|
||||
new_credential(4, 50), // too long
|
||||
new_credential(3, 4),
|
||||
new_credential(2, 50), // too long
|
||||
new_credential(1, 4),
|
||||
];
|
||||
let mut info = dev.get_authenticator_info().unwrap().clone();
|
||||
info.max_credential_count_in_list = Some(5);
|
||||
info.max_credential_id_length = Some(20);
|
||||
dev.set_authenticator_info(info);
|
||||
// Our test device now says that it supports 5 allow_list-entries,
|
||||
// so we can send all of them in one request, except for those
|
||||
// that got pre-filtered, as they were too long.
|
||||
dev.add_upcoming_ctap2_request(&new_silent_assert(
|
||||
&rp,
|
||||
&[allow_list[1].clone(), allow_list[3].clone()],
|
||||
));
|
||||
dev.add_upcoming_ctap_response(vec![new_assertion_response(&rp, Some(&allow_list[1]))]);
|
||||
let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash);
|
||||
assert_eq!(res, vec![allow_list[1].clone()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ pub mod tests {
|
||||
u2ftypes::U2FDeviceInfo,
|
||||
};
|
||||
|
||||
fn gen_info(id: String) -> U2FDeviceInfo {
|
||||
pub(crate) fn gen_info(id: String) -> U2FDeviceInfo {
|
||||
U2FDeviceInfo {
|
||||
vendor_name: String::from("ExampleVendor").into_bytes(),
|
||||
device_name: id.into_bytes(),
|
||||
@@ -200,13 +200,16 @@ pub mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn make_device_simple_u2f(dev: &mut Device) {
|
||||
pub(crate) fn make_device_simple_u2f(dev: &mut Device) {
|
||||
dev.set_device_info(gen_info(dev.id()));
|
||||
dev.set_cid([1, 2, 3, 4]); // Need to set something other than broadcast
|
||||
dev.downgrade_to_ctap1();
|
||||
dev.create_channel();
|
||||
}
|
||||
|
||||
fn make_device_with_pin(dev: &mut Device) {
|
||||
pub(crate) fn make_device_with_pin(dev: &mut Device) {
|
||||
dev.set_device_info(gen_info(dev.id()));
|
||||
dev.set_cid([1, 2, 3, 4]); // Need to set something other than broadcast
|
||||
dev.create_channel();
|
||||
let info = AuthenticatorInfo {
|
||||
options: AuthenticatorOptions {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::TestDevice;
|
||||
use crate::consts::{HIDCmd, CID_BROADCAST};
|
||||
|
||||
use crate::ctap2::commands::{CommandError, RequestCtap1, RequestCtap2, Retryable, StatusCode};
|
||||
use crate::transport::errors::{ApduErrorStatus, HIDError};
|
||||
use crate::transport::{FidoDevice, FidoDeviceIO, FidoProtocol};
|
||||
@@ -156,7 +156,10 @@ pub trait HIDDevice: FidoDevice + Read + Write {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: HIDDevice> FidoDeviceIO for T {
|
||||
#[cfg(not(test))]
|
||||
impl<T: HIDDevice> TestDevice for T {}
|
||||
|
||||
impl<T: HIDDevice + TestDevice> FidoDeviceIO for T {
|
||||
fn send_msg_cancellable<Out, Req: RequestCtap1<Output = Out> + RequestCtap2<Output = Out>>(
|
||||
&mut self,
|
||||
msg: &Req,
|
||||
@@ -178,6 +181,12 @@ impl<T: HIDDevice> FidoDeviceIO for T {
|
||||
keep_alive: &dyn Fn() -> bool,
|
||||
) -> Result<Req::Output, HIDError> {
|
||||
debug!("sending {:?} to {:?}", msg, self);
|
||||
#[cfg(test)]
|
||||
{
|
||||
if self.skip_serialization() {
|
||||
return self.send_ctap2_unserialized(msg);
|
||||
}
|
||||
}
|
||||
|
||||
let mut data = msg.wire_format()?;
|
||||
let mut buf: Vec<u8> = Vec::with_capacity(data.len() + 1);
|
||||
@@ -201,6 +210,12 @@ impl<T: HIDDevice> FidoDeviceIO for T {
|
||||
keep_alive: &dyn Fn() -> bool,
|
||||
) -> Result<Req::Output, HIDError> {
|
||||
debug!("sending {:?} to {:?}", msg, self);
|
||||
#[cfg(test)]
|
||||
{
|
||||
if self.skip_serialization() {
|
||||
return self.send_ctap1_unserialized(msg);
|
||||
}
|
||||
}
|
||||
let (data, add_info) = msg.ctap1_format()?;
|
||||
|
||||
while keep_alive() {
|
||||
|
||||
@@ -4,9 +4,13 @@
|
||||
use crate::consts::{Capability, HIDCmd, CID_BROADCAST};
|
||||
use crate::crypto::SharedSecret;
|
||||
use crate::ctap2::commands::get_info::AuthenticatorInfo;
|
||||
use crate::ctap2::commands::{CtapResponse, RequestCtap1, RequestCtap2};
|
||||
use crate::transport::device_selector::DeviceCommand;
|
||||
use crate::transport::TestDevice;
|
||||
use crate::transport::{hid::HIDDevice, FidoDevice, FidoProtocol, HIDError};
|
||||
use crate::u2ftypes::{U2FDeviceInfo, U2FHIDInitResp};
|
||||
use std::any::Any;
|
||||
use std::collections::VecDeque;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io::{self, Read, Write};
|
||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||
@@ -25,6 +29,9 @@ pub struct Device {
|
||||
pub sender: Option<Sender<DeviceCommand>>,
|
||||
pub receiver: Option<Receiver<DeviceCommand>>,
|
||||
pub protocol: FidoProtocol,
|
||||
skip_serialization: bool,
|
||||
pub upcoming_requests: VecDeque<Vec<u8>>,
|
||||
pub upcoming_responses: VecDeque<Result<Box<dyn Any>, HIDError>>,
|
||||
}
|
||||
|
||||
impl Device {
|
||||
@@ -44,11 +51,48 @@ impl Device {
|
||||
self.reads.push(read);
|
||||
}
|
||||
|
||||
pub fn add_upcoming_ctap2_request(&mut self, msg: &impl RequestCtap2) {
|
||||
self.upcoming_requests
|
||||
.push_back(msg.wire_format().expect("Failed to serialize CTAP request"));
|
||||
}
|
||||
|
||||
pub fn add_upcoming_ctap1_request(&mut self, msg: &impl RequestCtap1) {
|
||||
let (upcoming, _) = msg
|
||||
.ctap1_format()
|
||||
.expect("Failed to serialize CTAP request");
|
||||
self.upcoming_requests.push_back(upcoming);
|
||||
}
|
||||
|
||||
pub fn add_upcoming_ctap_response(&mut self, msg: impl CtapResponse) {
|
||||
self.upcoming_responses.push_back(Ok(Box::new(msg)));
|
||||
}
|
||||
|
||||
pub fn add_upcoming_ctap_error(&mut self, msg: HIDError) {
|
||||
self.upcoming_responses.push_back(Err(msg));
|
||||
}
|
||||
|
||||
pub fn create_channel(&mut self) {
|
||||
let (tx, rx) = channel();
|
||||
self.sender = Some(tx);
|
||||
self.receiver = Some(rx);
|
||||
}
|
||||
|
||||
pub fn new_skipping_serialization(id: &str) -> Result<Self, (HIDError, String)> {
|
||||
Ok(Device {
|
||||
id: id.to_string(),
|
||||
cid: CID_BROADCAST,
|
||||
reads: vec![],
|
||||
writes: vec![],
|
||||
dev_info: None,
|
||||
authenticator_info: None,
|
||||
sender: None,
|
||||
receiver: None,
|
||||
protocol: FidoProtocol::CTAP2,
|
||||
skip_serialization: true,
|
||||
upcoming_requests: VecDeque::new(),
|
||||
upcoming_responses: VecDeque::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for Device {
|
||||
@@ -124,6 +168,9 @@ impl HIDDevice for Device {
|
||||
sender: None,
|
||||
receiver: None,
|
||||
protocol: FidoProtocol::CTAP2,
|
||||
skip_serialization: false,
|
||||
upcoming_requests: VecDeque::new(),
|
||||
upcoming_responses: VecDeque::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -190,6 +237,52 @@ impl HIDDevice for Device {
|
||||
}
|
||||
}
|
||||
|
||||
impl TestDevice for Device {
|
||||
fn skip_serialization(&self) -> bool {
|
||||
self.skip_serialization
|
||||
}
|
||||
|
||||
fn send_ctap1_unserialized<Req: RequestCtap1>(
|
||||
&mut self,
|
||||
msg: &Req,
|
||||
) -> Result<Req::Output, HIDError> {
|
||||
let expected = self
|
||||
.upcoming_requests
|
||||
.pop_front()
|
||||
.expect("No expected CTAP1 command left");
|
||||
let (incoming, _) = msg.ctap1_format().expect("Can't serialize CTAP1 request");
|
||||
assert_eq!(expected, incoming);
|
||||
let response = self
|
||||
.upcoming_responses
|
||||
.pop_front()
|
||||
.expect("No response given!");
|
||||
response.map(|x| {
|
||||
*x.downcast()
|
||||
.expect("Failed to downcast given CTAP response")
|
||||
})
|
||||
}
|
||||
|
||||
fn send_ctap2_unserialized<Req: RequestCtap2>(
|
||||
&mut self,
|
||||
msg: &Req,
|
||||
) -> Result<Req::Output, HIDError> {
|
||||
let expected = self
|
||||
.upcoming_requests
|
||||
.pop_front()
|
||||
.expect("No expected CTAP2 command left");
|
||||
let incoming = msg.wire_format().expect("Can't serialize CTAP2 request");
|
||||
assert_eq!(expected, incoming);
|
||||
let response = self
|
||||
.upcoming_responses
|
||||
.pop_front()
|
||||
.expect("No response given!");
|
||||
response.map(|x| {
|
||||
*x.downcast()
|
||||
.expect("Failed to downcast given CTAP response")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FidoDevice for Device {
|
||||
fn pre_init(&mut self) -> Result<(), HIDError> {
|
||||
HIDDevice::pre_init(self)
|
||||
|
||||
@@ -112,6 +112,21 @@ pub trait FidoDeviceIO {
|
||||
) -> Result<Req::Output, HIDError>;
|
||||
}
|
||||
|
||||
pub trait TestDevice {
|
||||
#[cfg(test)]
|
||||
fn skip_serialization(&self) -> bool;
|
||||
#[cfg(test)]
|
||||
fn send_ctap1_unserialized<Req: RequestCtap1>(
|
||||
&mut self,
|
||||
msg: &Req,
|
||||
) -> Result<Req::Output, HIDError>;
|
||||
#[cfg(test)]
|
||||
fn send_ctap2_unserialized<Req: RequestCtap2>(
|
||||
&mut self,
|
||||
msg: &Req,
|
||||
) -> Result<Req::Output, HIDError>;
|
||||
}
|
||||
|
||||
pub trait FidoDevice: FidoDeviceIO
|
||||
where
|
||||
Self: Sized,
|
||||
|
||||
Reference in New Issue
Block a user