adding HMAC-secret support

This commit is contained in:
Fabian Kaczmarczyck
2020-03-09 20:06:06 +01:00
parent 8b146440a5
commit 8d52e8aad7
3 changed files with 365 additions and 15 deletions

View File

@@ -220,6 +220,78 @@ impl TryFrom<&cbor::Value> for Extensions {
}
}
impl From<Extensions> for cbor::Value {
fn from(extensions: Extensions) -> Self {
cbor_map_btree!(extensions
.0
.iter()
.map(|(key, value)| (cbor_text!(key), value.clone()))
.collect())
}
}
impl Extensions {
#[cfg(test)]
pub fn new(extension_map: BTreeMap<String, cbor::Value>) -> Self {
Extensions(extension_map)
}
pub fn has_make_credential_hmac_secret(&self) -> Result<bool, Ctap2StatusCode> {
self.0
.get("hmac-secret")
.map(read_bool)
.unwrap_or(Ok(false))
}
pub fn get_assertion_hmac_secret(
&self,
) -> Option<Result<GetAssertionHmacSecretInput, Ctap2StatusCode>> {
self.0
.get("hmac-secret")
.map(GetAssertionHmacSecretInput::try_from)
}
}
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct GetAssertionHmacSecretInput {
pub key_agreement: CoseKey,
pub salt_enc: Vec<u8>,
pub salt_auth: Vec<u8>,
}
impl TryFrom<&cbor::Value> for GetAssertionHmacSecretInput {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: &cbor::Value) -> Result<Self, Ctap2StatusCode> {
let input_map = read_map(cbor_value)?;
let cose_key = read_map(ok_or_missing(input_map.get(&cbor_unsigned!(1)))?)?;
let salt_enc = read_byte_string(ok_or_missing(input_map.get(&cbor_unsigned!(2)))?)?;
let salt_auth = read_byte_string(ok_or_missing(input_map.get(&cbor_unsigned!(3)))?)?;
Ok(Self {
key_agreement: CoseKey(cose_key.clone()),
salt_enc,
salt_auth,
})
}
}
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct GetAssertionHmacSecretOutput(Vec<u8>);
impl From<GetAssertionHmacSecretOutput> for cbor::Value {
fn from(message: GetAssertionHmacSecretOutput) -> cbor::Value {
cbor_bytes!(message.0)
}
}
impl TryFrom<&cbor::Value> for GetAssertionHmacSecretOutput {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: &cbor::Value) -> Result<Self, Ctap2StatusCode> {
Ok(GetAssertionHmacSecretOutput(read_byte_string(cbor_value)?))
}
}
// Even though options are optional, we can use the default if not present.
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct MakeCredentialOptions {
@@ -314,6 +386,7 @@ pub struct PublicKeyCredentialSource {
pub rp_id: String,
pub user_handle: Vec<u8>, // not optional, but nullable
pub other_ui: Option<String>,
pub cred_random: Option<Vec<u8>>,
}
impl From<PublicKeyCredentialSource> for cbor::Value {
@@ -324,12 +397,17 @@ impl From<PublicKeyCredentialSource> for cbor::Value {
None => cbor_null!(),
Some(other_ui) => cbor_text!(other_ui),
};
let cred_random = match credential.cred_random {
None => cbor_null!(),
Some(cred_random) => cbor_bytes!(cred_random),
};
cbor_array! {
credential.credential_id,
private_key,
credential.rp_id,
credential.user_handle,
other_ui,
cred_random,
}
}
}
@@ -341,7 +419,7 @@ impl TryFrom<cbor::Value> for PublicKeyCredentialSource {
use cbor::{SimpleValue, Value};
let fields = read_array(&cbor_value)?;
if fields.len() != 5 {
if fields.len() != 6 {
return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR);
}
let credential_id = read_byte_string(&fields[0])?;
@@ -357,6 +435,10 @@ impl TryFrom<cbor::Value> for PublicKeyCredentialSource {
Value::Simple(SimpleValue::NullValue) => None,
cbor_value => Some(read_text_string(cbor_value)?),
};
let cred_random = match &fields[5] {
Value::Simple(SimpleValue::NullValue) => None,
cbor_value => Some(read_byte_string(cbor_value)?),
};
Ok(PublicKeyCredentialSource {
key_type: PublicKeyCredentialType::PublicKey,
credential_id,
@@ -364,6 +446,7 @@ impl TryFrom<cbor::Value> for PublicKeyCredentialSource {
rp_id,
user_handle,
other_ui,
cred_random,
})
}
}
@@ -993,6 +1076,7 @@ mod test {
rp_id: "example.com".to_string(),
user_handle: b"foo".to_vec(),
other_ui: None,
cred_random: None,
};
assert_eq!(
@@ -1005,6 +1089,16 @@ mod test {
..credential
};
assert_eq!(
PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())),
Ok(credential.clone())
);
let credential = PublicKeyCredentialSource {
cred_random: Some([0x00; 32].to_vec()),
..credential
};
assert_eq!(
PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())),
Ok(credential)

View File

@@ -28,9 +28,9 @@ use self::command::{
AuthenticatorMakeCredentialParameters, Command,
};
use self::data_formats::{
ClientPinSubCommand, CoseKey, PackedAttestationStatement, PublicKeyCredentialDescriptor,
PublicKeyCredentialSource, PublicKeyCredentialType, PublicKeyCredentialUserEntity,
SignatureAlgorithm,
ClientPinSubCommand, CoseKey, GetAssertionHmacSecretInput, PackedAttestationStatement,
PublicKeyCredentialDescriptor, PublicKeyCredentialSource, PublicKeyCredentialType,
PublicKeyCredentialUserEntity, SignatureAlgorithm,
};
use self::hid::ChannelID;
use self::key_material::{AAGUID, ATTESTATION_CERTIFICATE, ATTESTATION_PRIVATE_KEY};
@@ -84,6 +84,7 @@ pub const ENCRYPTED_CREDENTIAL_ID_SIZE: usize = 112;
const UP_FLAG: u8 = 0x01;
const UV_FLAG: u8 = 0x04;
const AT_FLAG: u8 = 0x40;
const ED_FLAG: u8 = 0x80;
pub const TOUCH_TIMEOUT_MS: isize = 30000;
#[cfg(feature = "with_ctap1")]
@@ -105,6 +106,63 @@ fn check_pin_auth(hmac_key: &[u8], hmac_contents: &[u8], pin_auth: &[u8]) -> boo
)
}
// Decrypts the HMAC secret salt(s) that were encrypted with the shared secret.
// The credRandom is used as a secret to HMAC those salts.
// The last step is to re-encrypt the outputs.
pub fn encrypt_hmac_secret_output(
shared_secret: &[u8; 32],
salt_enc: Vec<u8>,
cred_random: &[u8],
) -> Result<Vec<u8>, Ctap2StatusCode> {
if salt_enc.len() != 32 && salt_enc.len() != 64 {
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION);
}
if cred_random.len() != 32 {
// We are strict here. We need at least 32 byte, but expect exactly 32.
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION);
}
let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret);
let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key);
// The specification specifically asks for a zero IV.
let iv = [0; 16];
let mut cred_random_secret = [0; 32];
cred_random_secret.clone_from_slice(cred_random);
// Initialization of 4 blocks in any case makes this function more readable.
let mut blocks = [[0u8; 16]; 4];
let block_len = salt_enc.len() / 16;
for i in 0..block_len {
blocks[i].copy_from_slice(&salt_enc[16 * i..16 * (i + 1)]);
}
cbc_decrypt(&aes_dec_key, iv, &mut blocks[..block_len]);
let mut decrypted_salt1 = [0; 32];
decrypted_salt1[..16].clone_from_slice(&blocks[0]);
let output1 = hmac_256::<Sha256>(&cred_random_secret, &decrypted_salt1[..]);
decrypted_salt1[16..].clone_from_slice(&blocks[1]);
for i in 0..2 {
blocks[i].copy_from_slice(&output1[16 * i..16 * (i + 1)]);
}
if block_len == 4 {
let mut decrypted_salt2 = [0; 32];
decrypted_salt2[..16].clone_from_slice(&blocks[2]);
decrypted_salt2[16..].clone_from_slice(&blocks[3]);
let output2 = hmac_256::<Sha256>(&cred_random_secret, &decrypted_salt2[..]);
for i in 0..2 {
blocks[i + 2].copy_from_slice(&output2[16 * i..16 * (i + 1)]);
}
}
cbc_encrypt(&aes_enc_key, iv, &mut blocks[..block_len]);
let mut encrypted_output = Vec::with_capacity(salt_enc.len());
for b in &blocks[..block_len] {
encrypted_output.extend(b);
}
Ok(encrypted_output)
}
// This function is adapted from https://doc.rust-lang.org/nightly/src/core/str/mod.rs.html#2110
// (as of 2020-01-20) and truncates to "max" bytes, not breaking the encoding.
// We change the return value, since we don't need the bool.
@@ -261,6 +319,7 @@ where
rp_id: String::from(""),
user_handle: vec![],
other_ui: None,
cred_random: None,
})
}
@@ -324,10 +383,10 @@ where
user,
pub_key_cred_params,
exclude_list,
extensions,
options,
pin_uv_auth_param,
pin_uv_auth_protocol,
..
} = make_credential_params;
if let Some(auth_param) = &pin_uv_auth_param {
@@ -362,6 +421,22 @@ where
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM);
}
let use_hmac_extension = if let Some(extensions) = extensions {
extensions.has_make_credential_hmac_secret()?
} else {
false
};
if use_hmac_extension && !options.rk {
// The extension is actually supported, but we need resident keys.
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION);
}
let cred_random = if use_hmac_extension {
Some(self.rng.gen_uniform_u8x32().to_vec())
} else {
None
};
let ed_flag = if use_hmac_extension { ED_FLAG } else { 0 };
let rp_id = rp.rp_id;
if let Some(exclude_list) = exclude_list {
for cred_desc in exclude_list {
@@ -389,7 +464,7 @@ where
if !check_pin_auth(&self.pin_uv_auth_token, &client_data_hash, &pin_auth) {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID);
}
UP_FLAG | UV_FLAG | AT_FLAG
UP_FLAG | UV_FLAG | AT_FLAG | ed_flag
}
None => {
if self.persistent_store.pin_hash().is_some() {
@@ -398,7 +473,7 @@ where
if options.uv {
return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION);
}
UP_FLAG | AT_FLAG
UP_FLAG | AT_FLAG | ed_flag
}
};
@@ -421,6 +496,7 @@ where
other_ui: user
.user_display_name
.map(|s| truncate_to_char_boundary(&s, 64).to_string()),
cred_random,
};
self.persistent_store.store_credential(credential_source)?;
random_id
@@ -441,6 +517,14 @@ where
None => return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR),
};
auth_data.extend(cose_key);
if use_hmac_extension {
let extensions = cbor_map! {
"hmac-secret" => true,
};
if !cbor::write(extensions, &mut auth_data) {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR);
}
}
let mut signature_data = auth_data.clone();
signature_data.extend(client_data_hash);
@@ -481,10 +565,10 @@ where
rp_id,
client_data_hash,
allow_list,
extensions,
options,
pin_uv_auth_param,
pin_uv_auth_protocol,
..
} = get_assertion_params;
if let Some(auth_param) = &pin_uv_auth_param {
@@ -527,6 +611,16 @@ where
}
}
let get_assertion_hmac_secret_input = if let Some(extensions) = extensions {
extensions.get_assertion_hmac_secret().transpose()?
} else {
None
};
if get_assertion_hmac_secret_input.is_some() && !options.up {
// The extension is actually supported, but we need user presence.
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION);
}
// The user verification bit depends on the existance of PIN auth, whereas
// user presence is requested as an option.
let mut flags = match pin_uv_auth_param {
@@ -551,6 +645,9 @@ where
if options.up {
flags |= UP_FLAG;
}
if get_assertion_hmac_secret_input.is_some() {
flags |= ED_FLAG;
}
let rp_id_hash = Sha256::hash(rp_id.as_bytes());
let mut decrypted_credential = None;
@@ -590,7 +687,37 @@ where
self.increment_global_signature_counter();
let auth_data = self.generate_auth_data(&rp_id_hash, flags);
let mut auth_data = self.generate_auth_data(&rp_id_hash, flags);
// Process extensions.
if let Some(get_assertion_hmac_secret_input) = get_assertion_hmac_secret_input {
let GetAssertionHmacSecretInput {
key_agreement,
salt_enc,
salt_auth,
} = get_assertion_hmac_secret_input;
let pk: crypto::ecdh::PubKey = CoseKey::try_into(key_agreement)?;
let shared_secret = self.key_agreement_key.exchange_x_sha256(&pk);
// HMAC-secret does the same 16 byte truncated check.
if !check_pin_auth(&shared_secret, &salt_enc, &salt_auth) {
// Again, hard to tell what the correct error code here is.
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION);
}
let encrypted_output = if let Some(cred_random) = &credential.cred_random {
encrypt_hmac_secret_output(&shared_secret, salt_enc, cred_random)?
} else {
// This happens because the credential was not created with HMAC-secret.
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION);
};
let extensions = cbor_map! {
"hmac-secret" => encrypted_output,
};
if !cbor::write(extensions, &mut auth_data) {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR);
}
}
let mut signature_data = auth_data.clone();
signature_data.extend(client_data_hash);
let signature = credential
@@ -639,7 +766,7 @@ where
String::from(U2F_VERSION_STRING),
String::from(FIDO2_VERSION_STRING),
],
extensions: Some(vec![]),
extensions: Some(vec![String::from("hmac-secret")]),
aaguid: *AAGUID,
options: Some(options_map),
max_msg_size: Some(1024),
@@ -948,7 +1075,7 @@ where
#[cfg(test)]
mod test {
use super::data_formats::{
GetAssertionOptions, MakeCredentialOptions, PublicKeyCredentialRpEntity,
Extensions, GetAssertionOptions, MakeCredentialOptions, PublicKeyCredentialRpEntity,
PublicKeyCredentialUserEntity,
};
use super::*;
@@ -970,13 +1097,15 @@ mod test {
let mut expected_response = vec![0x00, 0xA6, 0x01];
// The difference here is a longer array of supported versions.
#[cfg(not(feature = "with_ctap1"))]
expected_response.extend(&[
0x81, 0x68, 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30, 0x02, 0x80, 0x03, 0x50,
]);
expected_response.extend(&[0x81, 0x68, 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30]);
#[cfg(feature = "with_ctap1")]
expected_response.extend(&[
0x82, 0x66, 0x55, 0x32, 0x46, 0x5F, 0x56, 0x32, 0x68, 0x46, 0x49, 0x44, 0x4F, 0x5F,
0x32, 0x5F, 0x30, 0x02, 0x80, 0x03, 0x50,
0x32, 0x5F, 0x30,
]);
expected_response.extend(&[
0x02, 0x81, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74,
0x03, 0x50,
]);
expected_response.extend(AAGUID);
expected_response.extend(&[
@@ -1130,6 +1259,7 @@ mod test {
rp_id: String::from("example.com"),
user_handle: vec![],
other_ui: None,
cred_random: None,
};
assert!(ctap_state
.persistent_store
@@ -1153,6 +1283,54 @@ mod test {
);
}
#[test]
fn test_process_make_credential_hmac_secret() {
let mut rng = ThreadRng256 {};
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present);
let mut extension_map = BTreeMap::new();
extension_map.insert("hmac-secret".to_string(), cbor_bool!(true));
let extensions = Some(Extensions::new(extension_map));
let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.extensions = extensions;
let make_credential_response =
ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID);
match make_credential_response.unwrap() {
ResponseData::AuthenticatorMakeCredential(make_credential_response) => {
let AuthenticatorMakeCredentialResponse {
fmt,
auth_data,
att_stmt,
} = make_credential_response;
// The expected response is split to only assert the non-random parts.
assert_eq!(fmt, "packed");
let mut expected_auth_data = vec![
0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80,
0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2,
0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0xC1, 0x00, 0x00, 0x00, 0x00,
];
expected_auth_data.extend(AAGUID);
expected_auth_data.extend(&[0x00, 0x20]);
assert_eq!(
auth_data[0..expected_auth_data.len()],
expected_auth_data[..]
);
let expected_extension_cbor = vec![
0xA1, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74,
0xF5,
];
assert_eq!(
auth_data[auth_data.len() - expected_extension_cbor.len()..auth_data.len()],
expected_extension_cbor[..]
);
assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64);
}
_ => panic!("Invalid response type"),
}
}
#[test]
fn test_process_make_credential_cancelled() {
let mut rng = ThreadRng256 {};
@@ -1216,6 +1394,53 @@ mod test {
}
}
#[test]
fn test_residential_process_get_assertion_hmac_secret() {
let mut rng = ThreadRng256 {};
let sk = crypto::ecdh::SecKey::gensk(&mut rng);
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present);
let mut extension_map = BTreeMap::new();
extension_map.insert("hmac-secret".to_string(), cbor_bool!(true));
let make_extensions = Some(Extensions::new(extension_map));
let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.extensions = make_extensions;
assert!(ctap_state
.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID)
.is_ok());
let pk = sk.genpk();
let hmac_secret_parameters = cbor_map! {
1 => cbor::Value::Map(CoseKey::from(pk).0),
2 => vec![0; 32],
3 => vec![0; 16],
};
let mut extension_map = BTreeMap::new();
extension_map.insert("hmac-secret".to_string(), hmac_secret_parameters);
let get_extensions = Some(Extensions::new(extension_map));
let get_assertion_params = AuthenticatorGetAssertionParameters {
rp_id: String::from("example.com"),
client_data_hash: vec![0xCD],
allow_list: None,
extensions: get_extensions,
options: GetAssertionOptions {
up: false,
uv: false,
},
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let get_assertion_response =
ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID);
assert_eq!(
get_assertion_response,
Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION)
);
}
#[test]
fn test_process_reset() {
let mut rng = ThreadRng256 {};
@@ -1231,6 +1456,7 @@ mod test {
rp_id: String::from("example.com"),
user_handle: vec![],
other_ui: None,
cred_random: None,
};
assert!(ctap_state
.persistent_store
@@ -1294,4 +1520,32 @@ mod test {
.is_none());
}
}
#[test]
fn test_encrypt_hmac_secret_output() {
let shared_secret = [0x55; 32];
let salt_enc = vec![0x5E; 32];
let cred_random = vec![0xC9; 32];
let output = encrypt_hmac_secret_output(&shared_secret, salt_enc, &cred_random);
assert_eq!(output.unwrap().len(), 32);
let salt_enc = vec![0x5E; 48];
let output = encrypt_hmac_secret_output(&shared_secret, salt_enc, &cred_random);
assert_eq!(
output,
Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION)
);
let salt_enc = vec![0x5E; 64];
let output = encrypt_hmac_secret_output(&shared_secret, salt_enc, &cred_random);
assert_eq!(output.unwrap().len(), 64);
let salt_enc = vec![0x5E; 32];
let cred_random = vec![0xC9; 33];
let output = encrypt_hmac_secret_output(&shared_secret, salt_enc, &cred_random);
assert_eq!(
output,
Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION)
);
}
}

View File

@@ -445,6 +445,7 @@ mod test {
rp_id: String::from(rp_id),
user_handle,
other_ui: None,
cred_random: None,
}
}
@@ -613,6 +614,7 @@ mod test {
rp_id: String::from("example.com"),
user_handle: vec![0x00],
other_ui: None,
cred_random: None,
};
assert_eq!(found_credential, Some(expected_credential));
}