Fixes credProtect checking in CTAP1 (#629)
We accidentally lost this check in #516. I refactored some of the filters for better style. The actual difference in logic is just one line in CTAP1 authenticate, everything else is style, a test and the order in which we convert and filter the credentials: ``` let credential_source = filter_listed_credential(credential_source, false) .ok_or(Ctap1StatusCode::SW_WRONG_DATA)?; ```
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use super::apdu::{Apdu, ApduStatusCode};
|
||||
use super::CtapState;
|
||||
use super::{filter_listed_credential, CtapState};
|
||||
use crate::api::attestation_store::{self, Attestation, AttestationStore};
|
||||
use crate::api::crypto::ecdsa::{self, SecretKey as _, Signature};
|
||||
use crate::api::crypto::EC_FIELD_SIZE;
|
||||
@@ -331,39 +331,37 @@ impl Ctap1Command {
|
||||
.key_store()
|
||||
.unwrap_credential(&key_handle, &application)
|
||||
.map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?;
|
||||
if let Some(credential_source) = credential_source {
|
||||
let ecdsa_key = credential_source
|
||||
.private_key
|
||||
.ecdsa_key(env)
|
||||
.map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?;
|
||||
if flags == Ctap1Flags::CheckOnly {
|
||||
return Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED);
|
||||
}
|
||||
ctap_state
|
||||
.increment_global_signature_counter(env)
|
||||
.map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?;
|
||||
let mut signature_data = ctap_state
|
||||
.generate_auth_data(
|
||||
env,
|
||||
&application,
|
||||
Ctap1Command::USER_PRESENCE_INDICATOR_BYTE,
|
||||
)
|
||||
.map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?;
|
||||
signature_data.extend(&challenge);
|
||||
let signature = ecdsa_key.sign(&signature_data);
|
||||
|
||||
let mut response = signature_data[application.len()..application.len() + 5].to_vec();
|
||||
response.extend(signature.to_der());
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(Ctap1StatusCode::SW_WRONG_DATA)
|
||||
let credential_source = filter_listed_credential(credential_source, false)
|
||||
.ok_or(Ctap1StatusCode::SW_WRONG_DATA)?;
|
||||
let ecdsa_key = credential_source
|
||||
.private_key
|
||||
.ecdsa_key(env)
|
||||
.map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?;
|
||||
if flags == Ctap1Flags::CheckOnly {
|
||||
return Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED);
|
||||
}
|
||||
ctap_state
|
||||
.increment_global_signature_counter(env)
|
||||
.map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?;
|
||||
let mut signature_data = ctap_state
|
||||
.generate_auth_data(
|
||||
env,
|
||||
&application,
|
||||
Ctap1Command::USER_PRESENCE_INDICATOR_BYTE,
|
||||
)
|
||||
.map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?;
|
||||
signature_data.extend(&challenge);
|
||||
let signature = ecdsa_key.sign(&signature_data);
|
||||
|
||||
let mut response = signature_data[application.len()..application.len() + 5].to_vec();
|
||||
response.extend(signature.to_der());
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::super::data_formats::SignatureAlgorithm;
|
||||
use super::super::data_formats::{CredentialProtectionPolicy, SignatureAlgorithm};
|
||||
use super::super::TOUCH_TIMEOUT_MS;
|
||||
use super::*;
|
||||
use crate::api::crypto::sha256::Sha256;
|
||||
@@ -712,4 +710,27 @@ mod test {
|
||||
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_authenticate_cred_protect() {
|
||||
let mut env = TestEnv::default();
|
||||
env.user_presence()
|
||||
.set(|| panic!("Unexpected user presence check in CTAP1"));
|
||||
let mut ctap_state = CtapState::new(&mut env);
|
||||
|
||||
let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
|
||||
let rp_id_hash = Sha::<TestEnv>::digest(b"example.com");
|
||||
let credential_source = CredentialSource {
|
||||
private_key,
|
||||
rp_id_hash,
|
||||
cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationRequired),
|
||||
cred_blob: None,
|
||||
};
|
||||
let key_handle = env.key_store().wrap_credential(credential_source).unwrap();
|
||||
let message =
|
||||
create_authenticate_message(&rp_id_hash, Ctap1Flags::DontEnforceUpAndSign, &key_handle);
|
||||
|
||||
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_DATA));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,28 +186,53 @@ pub fn cbor_write(value: cbor::Value, encoded_cbor: &mut Vec<u8>) -> Result<(),
|
||||
.map_err(|_e| Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)
|
||||
}
|
||||
|
||||
fn decrypt_credential_id<E: Env>(
|
||||
env: &mut E,
|
||||
/// Filters the credential from the option if credProtect criteria are not met.
|
||||
pub fn filter_listed_credential(
|
||||
credential: Option<CredentialSource>,
|
||||
has_uv: bool,
|
||||
) -> Option<CredentialSource> {
|
||||
credential.filter(|c| {
|
||||
has_uv
|
||||
|| !matches!(
|
||||
c.cred_protect_policy,
|
||||
Some(CredentialProtectionPolicy::UserVerificationRequired)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Filters the resident key from the option if credProtect criteria are not met.
|
||||
fn filter_listed_resident_credential(
|
||||
credential: Option<PublicKeyCredentialSource>,
|
||||
has_uv: bool,
|
||||
) -> Option<PublicKeyCredentialSource> {
|
||||
credential.filter(|c| {
|
||||
has_uv
|
||||
|| !matches!(
|
||||
c.cred_protect_policy,
|
||||
Some(CredentialProtectionPolicy::UserVerificationRequired)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Populates all matching fields in a `PublicKeyCredentialSource`.
|
||||
fn to_public_source(
|
||||
credential_id: Vec<u8>,
|
||||
rp_id_hash: &[u8],
|
||||
) -> Result<Option<PublicKeyCredentialSource>, Ctap2StatusCode> {
|
||||
let credential_source = env
|
||||
.key_store()
|
||||
.unwrap_credential(&credential_id, rp_id_hash)?;
|
||||
Ok(credential_source.map(|c| PublicKeyCredentialSource {
|
||||
credential_source: CredentialSource,
|
||||
) -> PublicKeyCredentialSource {
|
||||
PublicKeyCredentialSource {
|
||||
key_type: PublicKeyCredentialType::PublicKey,
|
||||
credential_id,
|
||||
private_key: c.private_key,
|
||||
private_key: credential_source.private_key,
|
||||
rp_id: String::new(),
|
||||
user_handle: Vec::new(),
|
||||
user_display_name: None,
|
||||
cred_protect_policy: c.cred_protect_policy,
|
||||
cred_protect_policy: credential_source.cred_protect_policy,
|
||||
creation_order: 0,
|
||||
user_name: None,
|
||||
user_icon: None,
|
||||
cred_blob: c.cred_blob,
|
||||
cred_blob: credential_source.cred_blob,
|
||||
large_blob_key: None,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// This function is adapted from https://doc.rust-lang.org/nightly/src/core/str/mod.rs.html#2110
|
||||
@@ -698,22 +723,6 @@ impl<E: Env> CtapState<E> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_cred_protect_for_listed_credential(
|
||||
&mut self,
|
||||
credential: &Option<PublicKeyCredentialSource>,
|
||||
has_uv: bool,
|
||||
) -> bool {
|
||||
if let Some(credential) = credential {
|
||||
has_uv
|
||||
|| !matches!(
|
||||
credential.cred_protect_policy,
|
||||
Some(CredentialProtectionPolicy::UserVerificationRequired),
|
||||
)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn process_make_credential(
|
||||
&mut self,
|
||||
env: &mut E,
|
||||
@@ -806,13 +815,18 @@ impl<E: Env> CtapState<E> {
|
||||
let rp_id_hash = Sha::<E>::digest(rp_id.as_bytes());
|
||||
if let Some(exclude_list) = exclude_list {
|
||||
for cred_desc in exclude_list {
|
||||
if self.check_cred_protect_for_listed_credential(
|
||||
&storage::find_credential(env, &rp_id, &cred_desc.key_id)?,
|
||||
if filter_listed_resident_credential(
|
||||
storage::find_credential(env, &rp_id, &cred_desc.key_id)?,
|
||||
has_uv,
|
||||
) || self.check_cred_protect_for_listed_credential(
|
||||
&decrypt_credential_id(env, cred_desc.key_id, &rp_id_hash)?,
|
||||
has_uv,
|
||||
) {
|
||||
)
|
||||
.is_some()
|
||||
|| filter_listed_credential(
|
||||
env.key_store()
|
||||
.unwrap_credential(&cred_desc.key_id, &rp_id_hash)?,
|
||||
has_uv,
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
// Perform this check, so bad actors can't brute force exclude_list
|
||||
// without user interaction.
|
||||
let _ = check_user_presence(env, channel);
|
||||
@@ -1087,13 +1101,20 @@ impl<E: Env> CtapState<E> {
|
||||
has_uv: bool,
|
||||
) -> Result<Option<PublicKeyCredentialSource>, Ctap2StatusCode> {
|
||||
for allowed_credential in allow_list {
|
||||
let credential = storage::find_credential(env, rp_id, &allowed_credential.key_id)?;
|
||||
if self.check_cred_protect_for_listed_credential(&credential, has_uv) {
|
||||
let credential = filter_listed_resident_credential(
|
||||
storage::find_credential(env, rp_id, &allowed_credential.key_id)?,
|
||||
has_uv,
|
||||
);
|
||||
if credential.is_some() {
|
||||
return Ok(credential);
|
||||
}
|
||||
let credential = decrypt_credential_id(env, allowed_credential.key_id, rp_id_hash)?;
|
||||
if self.check_cred_protect_for_listed_credential(&credential, has_uv) {
|
||||
return Ok(credential);
|
||||
let credential = filter_listed_credential(
|
||||
env.key_store()
|
||||
.unwrap_credential(&allowed_credential.key_id, &rp_id_hash)?,
|
||||
has_uv,
|
||||
);
|
||||
if credential.is_some() {
|
||||
return Ok(credential.map(|c| to_public_source(allowed_credential.key_id, c)));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
|
||||
Reference in New Issue
Block a user