Merge pull request #283 from kaczmarczyck/always-uv
Add the option alwaysUv
This commit is contained in:
@@ -128,6 +128,9 @@ a few things you can personalize:
|
||||
1. Implement enterprise attestation. This can be as easy as setting
|
||||
ENTERPRISE_ATTESTATION_MODE in `ctap/mod.rs`. If you want to use a different
|
||||
attestation type than batch attestation, you have to implement it first.
|
||||
1. If a certification (additional to FIDO's) requires that all requests are
|
||||
protected with user verification, set `ENFORCE_ALWAYS_UV` in
|
||||
`ctap/config_mod.rs` to `true`.
|
||||
|
||||
### 3D printed enclosure
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ pub enum ApduStatusCode {
|
||||
SW_WRONG_DATA = 0x6a_80,
|
||||
SW_WRONG_LENGTH = 0x67_00,
|
||||
SW_COND_USE_NOT_SATISFIED = 0x69_85,
|
||||
SW_COMMAND_NOT_ALLOWED = 0x69_86,
|
||||
SW_FILE_NOT_FOUND = 0x6a_82,
|
||||
SW_INCORRECT_P1P2 = 0x6a_86,
|
||||
/// Instruction code not supported or invalid
|
||||
|
||||
@@ -33,6 +33,14 @@ fn process_enable_enterprise_attestation(
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes the subcommand toggleAlwaysUv for AuthenticatorConfig.
|
||||
fn process_toggle_always_uv(
|
||||
persistent_store: &mut PersistentStore,
|
||||
) -> Result<ResponseData, Ctap2StatusCode> {
|
||||
persistent_store.toggle_always_uv()?;
|
||||
Ok(ResponseData::AuthenticatorConfig)
|
||||
}
|
||||
|
||||
/// Processes the subcommand setMinPINLength for AuthenticatorConfig.
|
||||
fn process_set_min_pin_length(
|
||||
persistent_store: &mut PersistentStore,
|
||||
@@ -78,7 +86,11 @@ pub fn process_config(
|
||||
pin_uv_auth_protocol,
|
||||
} = params;
|
||||
|
||||
if persistent_store.pin_hash()?.is_some() {
|
||||
let enforce_uv = match sub_command {
|
||||
ConfigSubCommand::ToggleAlwaysUv => false,
|
||||
_ => true,
|
||||
} && persistent_store.has_always_uv()?;
|
||||
if persistent_store.pin_hash()?.is_some() || enforce_uv {
|
||||
// TODO(kaczmarczyck) The error code is specified inconsistently with other commands.
|
||||
check_pin_uv_auth_protocol(pin_uv_auth_protocol)
|
||||
.map_err(|_| Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?;
|
||||
@@ -100,6 +112,7 @@ pub fn process_config(
|
||||
ConfigSubCommand::EnableEnterpriseAttestation => {
|
||||
process_enable_enterprise_attestation(persistent_store)
|
||||
}
|
||||
ConfigSubCommand::ToggleAlwaysUv => process_toggle_always_uv(persistent_store),
|
||||
ConfigSubCommand::SetMinPinLength => {
|
||||
if let Some(ConfigSubCommandParams::SetMinPinLength(params)) = sub_command_params {
|
||||
process_set_min_pin_length(persistent_store, params)
|
||||
@@ -114,6 +127,7 @@ pub fn process_config(
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::ctap::ENFORCE_ALWAYS_UV;
|
||||
use crypto::rng256::ThreadRng256;
|
||||
|
||||
#[test]
|
||||
@@ -144,6 +158,87 @@ mod test {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_toggle_always_uv() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let mut persistent_store = PersistentStore::new(&mut rng);
|
||||
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
|
||||
let pin_uv_auth_token = [0x55; 32];
|
||||
let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token);
|
||||
|
||||
let config_params = AuthenticatorConfigParameters {
|
||||
sub_command: ConfigSubCommand::ToggleAlwaysUv,
|
||||
sub_command_params: None,
|
||||
pin_uv_auth_param: None,
|
||||
pin_uv_auth_protocol: None,
|
||||
};
|
||||
let config_response =
|
||||
process_config(&mut persistent_store, &mut pin_protocol_v1, config_params);
|
||||
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
|
||||
assert!(persistent_store.has_always_uv().unwrap());
|
||||
|
||||
let config_params = AuthenticatorConfigParameters {
|
||||
sub_command: ConfigSubCommand::ToggleAlwaysUv,
|
||||
sub_command_params: None,
|
||||
pin_uv_auth_param: None,
|
||||
pin_uv_auth_protocol: None,
|
||||
};
|
||||
let config_response =
|
||||
process_config(&mut persistent_store, &mut pin_protocol_v1, config_params);
|
||||
if ENFORCE_ALWAYS_UV {
|
||||
assert_eq!(
|
||||
config_response,
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED)
|
||||
);
|
||||
} else {
|
||||
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
|
||||
assert!(!persistent_store.has_always_uv().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_toggle_always_uv_with_pin() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let mut persistent_store = PersistentStore::new(&mut rng);
|
||||
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
|
||||
let pin_uv_auth_token = [0x55; 32];
|
||||
let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token);
|
||||
persistent_store.set_pin(&[0x88; 16], 4).unwrap();
|
||||
|
||||
let pin_uv_auth_param = Some(vec![
|
||||
0x99, 0xBA, 0x0A, 0x57, 0x9D, 0x95, 0x5A, 0x44, 0xE3, 0x77, 0xCF, 0x95, 0x51, 0x3F,
|
||||
0xFD, 0xBE,
|
||||
]);
|
||||
let config_params = AuthenticatorConfigParameters {
|
||||
sub_command: ConfigSubCommand::ToggleAlwaysUv,
|
||||
sub_command_params: None,
|
||||
pin_uv_auth_param: pin_uv_auth_param.clone(),
|
||||
pin_uv_auth_protocol: Some(1),
|
||||
};
|
||||
let config_response =
|
||||
process_config(&mut persistent_store, &mut pin_protocol_v1, config_params);
|
||||
if ENFORCE_ALWAYS_UV {
|
||||
assert_eq!(
|
||||
config_response,
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED)
|
||||
);
|
||||
return;
|
||||
}
|
||||
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
|
||||
assert!(persistent_store.has_always_uv().unwrap());
|
||||
|
||||
let config_params = AuthenticatorConfigParameters {
|
||||
sub_command: ConfigSubCommand::ToggleAlwaysUv,
|
||||
sub_command_params: None,
|
||||
pin_uv_auth_param,
|
||||
pin_uv_auth_protocol: Some(1),
|
||||
};
|
||||
let config_response =
|
||||
process_config(&mut persistent_store, &mut pin_protocol_v1, config_params);
|
||||
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
|
||||
assert!(!persistent_store.has_always_uv().unwrap());
|
||||
}
|
||||
|
||||
fn create_min_pin_config_params(
|
||||
min_pin_length: u8,
|
||||
min_pin_length_rp_ids: Option<Vec<String>>,
|
||||
|
||||
@@ -189,6 +189,12 @@ impl Ctap1Command {
|
||||
R: Rng256,
|
||||
CheckUserPresence: Fn(ChannelID) -> Result<(), Ctap2StatusCode>,
|
||||
{
|
||||
if !ctap_state
|
||||
.allows_ctap1()
|
||||
.map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?
|
||||
{
|
||||
return Err(Ctap1StatusCode::SW_COMMAND_NOT_ALLOWED);
|
||||
}
|
||||
let command = U2fCommand::try_from(message)?;
|
||||
match command {
|
||||
U2fCommand::Register {
|
||||
@@ -398,6 +404,21 @@ mod test {
|
||||
message
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_allowed() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
|
||||
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE);
|
||||
ctap_state.persistent_store.toggle_always_uv().unwrap();
|
||||
|
||||
let application = [0x0A; 32];
|
||||
let message = create_register_message(&application);
|
||||
ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE);
|
||||
ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE);
|
||||
let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE);
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_COMMAND_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_register() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
|
||||
@@ -177,7 +177,7 @@ impl CtapHid {
|
||||
match message.cmd {
|
||||
// CTAP specification (version 20190130) section 8.1.9.1.1
|
||||
CtapHid::COMMAND_MSG => {
|
||||
// If we don't have CTAP1 backward compatibilty, this command in invalid.
|
||||
// If we don't have CTAP1 backward compatibilty, this command is invalid.
|
||||
#[cfg(not(feature = "with_ctap1"))]
|
||||
return CtapHid::error_message(cid, CtapHid::ERR_INVALID_CMD);
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ impl LargeBlobs {
|
||||
if offset != self.expected_next_offset {
|
||||
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_SEQ);
|
||||
}
|
||||
if persistent_store.pin_hash()?.is_some() {
|
||||
if persistent_store.pin_hash()?.is_some() || persistent_store.has_always_uv()? {
|
||||
let pin_uv_auth_param =
|
||||
pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?;
|
||||
// TODO(kaczmarczyck) Error codes for PIN protocol differ across commands.
|
||||
|
||||
108
src/ctap/mod.rs
108
src/ctap/mod.rs
@@ -147,6 +147,9 @@ pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa
|
||||
const DEFAULT_CRED_PROTECT: Option<CredentialProtectionPolicy> = None;
|
||||
// Maximum size stored with the credBlob extension. Must be at least 32.
|
||||
const MAX_CRED_BLOB_LENGTH: usize = 32;
|
||||
// Enforce the alwaysUv option. With this constant set to true, commands require
|
||||
// a PIN to be set up. alwaysUv can not be disabled by commands.
|
||||
pub const ENFORCE_ALWAYS_UV: bool = false;
|
||||
|
||||
// Checks the PIN protocol parameter against all supported versions.
|
||||
pub fn check_pin_uv_auth_protocol(
|
||||
@@ -364,6 +367,14 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Returns whether CTAP1 commands are currently supported.
|
||||
// If alwaysUv is enabled and the authenticator does not support internal UV,
|
||||
// CTAP1 needs to be disabled.
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
pub fn allows_ctap1(&self) -> Result<bool, Ctap2StatusCode> {
|
||||
Ok(!self.persistent_store.has_always_uv()?)
|
||||
}
|
||||
|
||||
// Encrypts the private key and relying party ID hash into a credential ID. Other
|
||||
// information, such as a user name, are not stored, because encrypted credential IDs
|
||||
// are used for credentials stored server-side. Also, we want the key handle to be
|
||||
@@ -685,7 +696,11 @@ where
|
||||
UP_FLAG | UV_FLAG | AT_FLAG | ed_flag
|
||||
}
|
||||
None => {
|
||||
if self.persistent_store.pin_hash()?.is_some() {
|
||||
if self.persistent_store.has_always_uv()? {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED);
|
||||
}
|
||||
// Corresponds to makeCredUvNotRqd set to true.
|
||||
if options.rk && self.persistent_store.pin_hash()?.is_some() {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED);
|
||||
}
|
||||
if options.uv {
|
||||
@@ -972,8 +987,10 @@ where
|
||||
UV_FLAG
|
||||
}
|
||||
None => {
|
||||
if self.persistent_store.has_always_uv()? {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED);
|
||||
}
|
||||
if options.uv {
|
||||
// The specification (inconsistently) wants CTAP2_ERR_UNSUPPORTED_OPTION.
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION);
|
||||
}
|
||||
0x00
|
||||
@@ -1062,6 +1079,18 @@ where
|
||||
}
|
||||
|
||||
fn process_get_info(&self) -> Result<ResponseData, Ctap2StatusCode> {
|
||||
let has_always_uv = self.persistent_store.has_always_uv()?;
|
||||
#[cfg_attr(not(feature = "with_ctap1"), allow(unused_mut))]
|
||||
let mut versions = vec![
|
||||
String::from(FIDO2_VERSION_STRING),
|
||||
String::from(FIDO2_1_VERSION_STRING),
|
||||
];
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
{
|
||||
if !has_always_uv {
|
||||
versions.insert(0, String::from(U2F_VERSION_STRING))
|
||||
}
|
||||
}
|
||||
let mut options_map = BTreeMap::new();
|
||||
options_map.insert(String::from("rk"), true);
|
||||
options_map.insert(
|
||||
@@ -1080,15 +1109,11 @@ where
|
||||
options_map.insert(String::from("authnrCfg"), true);
|
||||
options_map.insert(String::from("credMgmt"), true);
|
||||
options_map.insert(String::from("setMinPINLength"), true);
|
||||
options_map.insert(String::from("makeCredUvNotRqd"), true);
|
||||
options_map.insert(String::from("makeCredUvNotRqd"), !has_always_uv);
|
||||
options_map.insert(String::from("alwaysUv"), has_always_uv);
|
||||
Ok(ResponseData::AuthenticatorGetInfo(
|
||||
AuthenticatorGetInfoResponse {
|
||||
versions: vec![
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
String::from(U2F_VERSION_STRING),
|
||||
String::from(FIDO2_VERSION_STRING),
|
||||
String::from(FIDO2_1_VERSION_STRING),
|
||||
],
|
||||
versions,
|
||||
extensions: Some(vec![
|
||||
String::from("hmac-secret"),
|
||||
String::from("credProtect"),
|
||||
@@ -1340,6 +1365,7 @@ mod test {
|
||||
"credMgmt" => true,
|
||||
"setMinPINLength" => true,
|
||||
"makeCredUvNotRqd" => true,
|
||||
"alwaysUv" => false,
|
||||
},
|
||||
0x05 => MAX_MSG_SIZE as u64,
|
||||
0x06 => cbor_array_vec![vec![1]],
|
||||
@@ -1781,6 +1807,70 @@ mod test {
|
||||
assert_eq!(stored_credential.large_blob_key.unwrap(), large_blob_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_resident_process_make_credential_with_pin() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let user_immediately_present = |_| Ok(());
|
||||
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
|
||||
ctap_state.persistent_store.set_pin(&[0x88; 16], 4).unwrap();
|
||||
|
||||
let mut make_credential_params = create_minimal_make_credential_parameters();
|
||||
make_credential_params.options.rk = false;
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID);
|
||||
|
||||
check_make_response(
|
||||
make_credential_response,
|
||||
0x41,
|
||||
&ctap_state.persistent_store.aaguid().unwrap(),
|
||||
0x70,
|
||||
&[],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resident_process_make_credential_with_pin() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let user_immediately_present = |_| Ok(());
|
||||
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
|
||||
ctap_state.persistent_store.set_pin(&[0x88; 16], 4).unwrap();
|
||||
|
||||
let make_credential_params = create_minimal_make_credential_parameters();
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID);
|
||||
assert_eq!(
|
||||
make_credential_response,
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_make_credential_with_pin_always_uv() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let user_immediately_present = |_| Ok(());
|
||||
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
|
||||
|
||||
ctap_state.persistent_store.toggle_always_uv().unwrap();
|
||||
let make_credential_params = create_minimal_make_credential_parameters();
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID);
|
||||
assert_eq!(
|
||||
make_credential_response,
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)
|
||||
);
|
||||
|
||||
ctap_state.persistent_store.set_pin(&[0x88; 16], 4).unwrap();
|
||||
let mut make_credential_params = create_minimal_make_credential_parameters();
|
||||
make_credential_params.pin_uv_auth_param = Some(vec![0xA4; 16]);
|
||||
make_credential_params.pin_uv_auth_protocol = Some(1);
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID);
|
||||
assert_eq!(
|
||||
make_credential_response,
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_make_credential_cancelled() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
|
||||
@@ -18,10 +18,10 @@ use crate::ctap::data_formats::{
|
||||
extract_array, extract_text_string, CredentialProtectionPolicy, PublicKeyCredentialSource,
|
||||
PublicKeyCredentialUserEntity,
|
||||
};
|
||||
use crate::ctap::key_material;
|
||||
use crate::ctap::pin_protocol_v1::PIN_AUTH_LENGTH;
|
||||
use crate::ctap::status_code::Ctap2StatusCode;
|
||||
use crate::ctap::INITIAL_SIGNATURE_COUNTER;
|
||||
use crate::ctap::{key_material, ENFORCE_ALWAYS_UV};
|
||||
use crate::embedded_flash::{new_storage, Storage};
|
||||
use alloc::string::String;
|
||||
use alloc::vec;
|
||||
@@ -633,6 +633,30 @@ impl PersistentStore {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns whether alwaysUv is enabled.
|
||||
pub fn has_always_uv(&self) -> Result<bool, Ctap2StatusCode> {
|
||||
if ENFORCE_ALWAYS_UV {
|
||||
return Ok(true);
|
||||
}
|
||||
match self.store.find(key::ALWAYS_UV)? {
|
||||
None => Ok(false),
|
||||
Some(value) if value.is_empty() => Ok(true),
|
||||
_ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR),
|
||||
}
|
||||
}
|
||||
|
||||
/// Enables alwaysUv, when disabled, and vice versa.
|
||||
pub fn toggle_always_uv(&mut self) -> Result<(), Ctap2StatusCode> {
|
||||
if ENFORCE_ALWAYS_UV {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED);
|
||||
}
|
||||
if self.has_always_uv()? {
|
||||
Ok(self.store.remove(key::ALWAYS_UV)?)
|
||||
} else {
|
||||
Ok(self.store.insert(key::ALWAYS_UV, &[])?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<persistent_store::StoreError> for Ctap2StatusCode {
|
||||
@@ -1344,6 +1368,26 @@ mod test {
|
||||
assert!(!persistent_store.enterprise_attestation().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_always_uv() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let mut persistent_store = PersistentStore::new(&mut rng);
|
||||
|
||||
if ENFORCE_ALWAYS_UV {
|
||||
assert!(persistent_store.has_always_uv().unwrap());
|
||||
assert_eq!(
|
||||
persistent_store.toggle_always_uv(),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED)
|
||||
);
|
||||
} else {
|
||||
assert!(!persistent_store.has_always_uv().unwrap());
|
||||
assert_eq!(persistent_store.toggle_always_uv(), Ok(()));
|
||||
assert!(persistent_store.has_always_uv().unwrap());
|
||||
assert_eq!(persistent_store.toggle_always_uv(), Ok(()));
|
||||
assert!(!persistent_store.has_always_uv().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_deserialize_credential() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
|
||||
@@ -93,6 +93,9 @@ make_partition! {
|
||||
/// The stored large blob can be too big for one key, so it has to be sharded.
|
||||
LARGE_BLOB_SHARDS = 2000..2004;
|
||||
|
||||
/// If this entry exists and is empty, alwaysUv is enabled.
|
||||
ALWAYS_UV = 2038;
|
||||
|
||||
/// If this entry exists and is empty, enterprise attestation is enabled.
|
||||
ENTERPRISE_ATTESTATION = 2039;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user