diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index 949151e..bb94a91 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -1,3 +1,17 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + use alloc::vec::Vec; use byteorder::{BigEndian, ByteOrder}; use core::convert::TryFrom; diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index 59ff409..22b4b90 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -291,7 +291,7 @@ impl Ctap1Command { let sk = crypto::ecdsa::SecKey::gensk(ctap_state.rng); let pk = sk.genpk(); let key_handle = ctap_state - .encrypt_key_handle(sk, &application, None) + .encrypt_key_handle(sk, &application) .map_err(|_| Ctap1StatusCode::SW_COMMAND_ABORTED)?; if key_handle.len() > 0xFF { // This is just being defensive with unreachable code. @@ -386,7 +386,7 @@ impl Ctap1Command { #[cfg(test)] mod test { - use super::super::{key_material, CREDENTIAL_ID_BASE_SIZE, USE_SIGNATURE_COUNTER}; + use super::super::{key_material, CREDENTIAL_ID_SIZE, USE_SIGNATURE_COUNTER}; use super::*; use crypto::rng256::ThreadRng256; use crypto::Hash256; @@ -426,12 +426,12 @@ mod test { 0x00, 0x00, 0x00, - 65 + CREDENTIAL_ID_BASE_SIZE as u8, + 65 + CREDENTIAL_ID_SIZE as u8, ]; let challenge = [0x0C; 32]; message.extend(&challenge); message.extend(application); - message.push(CREDENTIAL_ID_BASE_SIZE as u8); + message.push(CREDENTIAL_ID_SIZE as u8); message.extend(key_handle); message } @@ -471,15 +471,12 @@ mod test { let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE).unwrap(); assert_eq!(response[0], Ctap1Command::LEGACY_BYTE); - assert_eq!(response[66], CREDENTIAL_ID_BASE_SIZE as u8); + assert_eq!(response[66], CREDENTIAL_ID_SIZE as u8); assert!(ctap_state - .decrypt_credential_source( - response[67..67 + CREDENTIAL_ID_BASE_SIZE].to_vec(), - &application - ) + .decrypt_credential_source(response[67..67 + CREDENTIAL_ID_SIZE].to_vec(), &application) .unwrap() .is_some()); - const CERT_START: usize = 67 + CREDENTIAL_ID_BASE_SIZE; + const CERT_START: usize = 67 + CREDENTIAL_ID_SIZE; assert_eq!( &response[CERT_START..CERT_START + fake_cert.len()], &fake_cert[..] @@ -528,9 +525,7 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state - .encrypt_key_handle(sk, &application, None) - .unwrap(); + let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); @@ -546,9 +541,7 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state - .encrypt_key_handle(sk, &application, None) - .unwrap(); + let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); let application = [0x55; 32]; let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); @@ -565,9 +558,7 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state - .encrypt_key_handle(sk, &application, None) - .unwrap(); + let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); @@ -591,9 +582,7 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state - .encrypt_key_handle(sk, &application, None) - .unwrap(); + let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); message[0] = 0xEE; @@ -611,9 +600,7 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state - .encrypt_key_handle(sk, &application, None) - .unwrap(); + let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); message[1] = 0xEE; @@ -631,9 +618,7 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state - .encrypt_key_handle(sk, &application, None) - .unwrap(); + let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); message[2] = 0xEE; @@ -659,9 +644,7 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state - .encrypt_key_handle(sk, &application, None) - .unwrap(); + let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); let message = create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle); @@ -688,9 +671,7 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state - .encrypt_key_handle(sk, &application, None) - .unwrap(); + let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); let message = create_authenticate_message( &application, Ctap1Flags::DontEnforceUpAndSign, @@ -712,7 +693,7 @@ mod test { #[test] fn test_process_authenticate_bad_key_handle() { let application = [0x0A; 32]; - let key_handle = vec![0x00; CREDENTIAL_ID_BASE_SIZE]; + let key_handle = vec![0x00; CREDENTIAL_ID_SIZE]; let message = create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle); @@ -729,7 +710,7 @@ mod test { #[test] fn test_process_authenticate_without_up() { let application = [0x0A; 32]; - let key_handle = vec![0x00; CREDENTIAL_ID_BASE_SIZE]; + let key_handle = vec![0x00; CREDENTIAL_ID_SIZE]; let message = create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle); diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index e7419fd..a2b490d 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -498,7 +498,6 @@ pub struct PublicKeyCredentialSource { pub rp_id: String, pub user_handle: Vec, // not optional, but nullable pub user_display_name: Option, - pub cred_random: Option>, pub cred_protect_policy: Option, pub creation_order: u64, pub user_name: Option, @@ -513,14 +512,14 @@ enum PublicKeyCredentialSourceField { RpId = 2, UserHandle = 3, UserDisplayName = 4, - CredRandom = 5, CredProtectPolicy = 6, CreationOrder = 7, UserName = 8, UserIcon = 9, // When a field is removed, its tag should be reserved and not used for new fields. We document // those reserved tags below. - // Reserved tags: none. + // Reserved tags: + // - CredRandom = 5, } impl From for cbor::KeyType { @@ -539,7 +538,6 @@ impl From for cbor::Value { PublicKeyCredentialSourceField::RpId => Some(credential.rp_id), PublicKeyCredentialSourceField::UserHandle => Some(credential.user_handle), PublicKeyCredentialSourceField::UserDisplayName => credential.user_display_name, - PublicKeyCredentialSourceField::CredRandom => credential.cred_random, PublicKeyCredentialSourceField::CredProtectPolicy => credential.cred_protect_policy, PublicKeyCredentialSourceField::CreationOrder => credential.creation_order, PublicKeyCredentialSourceField::UserName => credential.user_name, @@ -559,7 +557,6 @@ impl TryFrom for PublicKeyCredentialSource { PublicKeyCredentialSourceField::RpId => rp_id, PublicKeyCredentialSourceField::UserHandle => user_handle, PublicKeyCredentialSourceField::UserDisplayName => user_display_name, - PublicKeyCredentialSourceField::CredRandom => cred_random, PublicKeyCredentialSourceField::CredProtectPolicy => cred_protect_policy, PublicKeyCredentialSourceField::CreationOrder => creation_order, PublicKeyCredentialSourceField::UserName => user_name, @@ -577,7 +574,6 @@ impl TryFrom for PublicKeyCredentialSource { let rp_id = extract_text_string(ok_or_missing(rp_id)?)?; let user_handle = extract_byte_string(ok_or_missing(user_handle)?)?; let user_display_name = user_display_name.map(extract_text_string).transpose()?; - let cred_random = cred_random.map(extract_byte_string).transpose()?; let cred_protect_policy = cred_protect_policy .map(CredentialProtectionPolicy::try_from) .transpose()?; @@ -601,7 +597,6 @@ impl TryFrom for PublicKeyCredentialSource { rp_id, user_handle, user_display_name, - cred_random, cred_protect_policy, creation_order, user_name, @@ -1373,7 +1368,6 @@ mod test { rp_id: "example.com".to_string(), user_handle: b"foo".to_vec(), user_display_name: None, - cred_random: None, cred_protect_policy: None, creation_order: 0, user_name: None, @@ -1395,16 +1389,6 @@ mod test { Ok(credential.clone()) ); - let credential = PublicKeyCredentialSource { - cred_random: Some(vec![0x00; 32]), - ..credential - }; - - assert_eq!( - PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), - Ok(credential.clone()) - ); - let credential = PublicKeyCredentialSource { cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationOptional), ..credential diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index d2aa269..4c5687a 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -86,10 +86,8 @@ pub const INITIAL_SIGNATURE_COUNTER: u32 = 1; // - 16 byte initialization vector for AES-256, // - 32 byte ECDSA private key for the credential, // - 32 byte relying party ID hashed with SHA256, -// - (optional) 32 byte for HMAC-secret, // - 32 byte HMAC-SHA256 over everything else. -pub const CREDENTIAL_ID_BASE_SIZE: usize = 112; -pub const CREDENTIAL_ID_MAX_SIZE: usize = CREDENTIAL_ID_BASE_SIZE + 32; +pub const CREDENTIAL_ID_SIZE: usize = 112; // Set this bit when checking user presence. const UP_FLAG: u8 = 0x01; // Set this bit when checking user verification. @@ -142,6 +140,7 @@ struct AssertionInput { client_data_hash: Vec, auth_data: Vec, hmac_secret_input: Option, + has_uv: bool, } struct AssertionState { @@ -231,7 +230,6 @@ where &mut self, private_key: crypto::ecdsa::SecKey, application: &[u8; 32], - cred_random: Option<&[u8; 32]>, ) -> Result, Ctap2StatusCode> { let master_keys = self.persistent_store.master_keys()?; let aes_enc_key = crypto::aes256::EncryptionKey::new(&master_keys.encryption); @@ -240,19 +238,14 @@ where let mut iv = [0; 16]; iv.copy_from_slice(&self.rng.gen_uniform_u8x32()[..16]); - let block_len = if cred_random.is_some() { 6 } else { 4 }; - let mut blocks = vec![[0u8; 16]; block_len]; + let mut blocks = [[0u8; 16]; 4]; blocks[0].copy_from_slice(&sk_bytes[..16]); blocks[1].copy_from_slice(&sk_bytes[16..]); blocks[2].copy_from_slice(&application[..16]); blocks[3].copy_from_slice(&application[16..]); - if let Some(cred_random) = cred_random { - blocks[4].copy_from_slice(&cred_random[..16]); - blocks[5].copy_from_slice(&cred_random[16..]); - } cbc_encrypt(&aes_enc_key, iv, &mut blocks); - let mut encrypted_id = Vec::with_capacity(16 * (block_len + 3)); + let mut encrypted_id = Vec::with_capacity(0x70); encrypted_id.extend(&iv); for b in &blocks { encrypted_id.extend(b); @@ -270,11 +263,9 @@ where credential_id: Vec, rp_id_hash: &[u8], ) -> Result, Ctap2StatusCode> { - let has_cred_random = match credential_id.len() { - CREDENTIAL_ID_BASE_SIZE => false, - CREDENTIAL_ID_MAX_SIZE => true, - _ => return Ok(None), - }; + if credential_id.len() != CREDENTIAL_ID_SIZE { + return Ok(None); + } let master_keys = self.persistent_store.master_keys()?; let payload_size = credential_id.len() - 32; if !verify_hmac_256::( @@ -288,9 +279,8 @@ where let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); let mut iv = [0; 16]; iv.copy_from_slice(&credential_id[..16]); - let block_len = if has_cred_random { 6 } else { 4 }; - let mut blocks = vec![[0u8; 16]; block_len]; - for i in 0..block_len { + let mut blocks = [[0u8; 16]; 4]; + for i in 0..4 { blocks[i].copy_from_slice(&credential_id[16 * (i + 1)..16 * (i + 2)]); } @@ -301,15 +291,6 @@ where decrypted_sk[16..].clone_from_slice(&blocks[1]); decrypted_rp_id_hash[..16].clone_from_slice(&blocks[2]); decrypted_rp_id_hash[16..].clone_from_slice(&blocks[3]); - let cred_random = if has_cred_random { - let mut decrypted_cred_random = [0; 32]; - decrypted_cred_random[..16].clone_from_slice(&blocks[4]); - decrypted_cred_random[16..].clone_from_slice(&blocks[5]); - Some(decrypted_cred_random.to_vec()) - } else { - None - }; - if rp_id_hash != decrypted_rp_id_hash { return Ok(None); } @@ -322,7 +303,6 @@ where rp_id: String::from(""), user_handle: vec![], user_display_name: None, - cred_random, cred_protect_policy: None, creation_order: 0, user_name: None, @@ -464,11 +444,6 @@ where (false, DEFAULT_CRED_PROTECT) }; - let cred_random = if use_hmac_extension { - Some(self.rng.gen_uniform_u8x32()) - } else { - None - }; let has_extension_output = use_hmac_extension || cred_protect_policy.is_some(); let rp_id = rp.rp_id; @@ -543,7 +518,6 @@ where user_display_name: user .user_display_name .map(|s| truncate_to_char_boundary(&s, 64).to_string()), - cred_random: cred_random.map(|c| c.to_vec()), cred_protect_policy, creation_order: self.persistent_store.new_creation_order()?, user_name: user @@ -556,7 +530,7 @@ where self.persistent_store.store_credential(credential_source)?; random_id } else { - self.encrypt_key_handle(sk.clone(), &rp_id_hash, cred_random.as_ref())? + self.encrypt_key_handle(sk.clone(), &rp_id_hash)? }; let mut auth_data = self.generate_auth_data(&rp_id_hash, flags)?; @@ -622,10 +596,23 @@ where )) } + // Generates a different per-credential secret for each UV mode. + // The computation is deterministic, and private_key expected to be unique. + fn generate_cred_random( + &mut self, + private_key: &crypto::ecdsa::SecKey, + has_uv: bool, + ) -> Result<[u8; 32], Ctap2StatusCode> { + let mut private_key_bytes = [0u8; 32]; + private_key.to_bytes(&mut private_key_bytes); + let key = self.persistent_store.cred_random_secret(has_uv)?; + Ok(hmac_256::(&key, &private_key_bytes)) + } + // Processes the input of a get_assertion operation for a given credential // and returns the correct Get(Next)Assertion response. fn assertion_response( - &self, + &mut self, credential: PublicKeyCredentialSource, assertion_input: AssertionInput, number_of_credentials: Option, @@ -634,13 +621,15 @@ where client_data_hash, mut auth_data, hmac_secret_input, + has_uv, } = assertion_input; // Process extensions. if let Some(hmac_secret_input) = hmac_secret_input { + let cred_random = self.generate_cred_random(&credential.private_key, has_uv)?; let encrypted_output = self .pin_protocol_v1 - .process_hmac_secret(hmac_secret_input, &credential.cred_random)?; + .process_hmac_secret(hmac_secret_input, &cred_random)?; let extensions_output = cbor_map! { "hmac-secret" => encrypted_output, }; @@ -807,6 +796,7 @@ where client_data_hash, auth_data: self.generate_auth_data(&rp_id_hash, flags)?, hmac_secret_input, + has_uv, }; let number_of_credentials = if applicable_credentials.is_empty() { None @@ -872,7 +862,7 @@ where max_credential_count_in_list: MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), // #TODO(106) update with version 2.1 of HMAC-secret #[cfg(feature = "with_ctap2_1")] - max_credential_id_length: Some(CREDENTIAL_ID_BASE_SIZE as u64 + 32), + max_credential_id_length: Some(CREDENTIAL_ID_SIZE as u64), #[cfg(feature = "with_ctap2_1")] transports: Some(vec![AuthenticatorTransport::Usb]), #[cfg(feature = "with_ctap2_1")] @@ -1010,7 +1000,7 @@ mod test { #[cfg(feature = "with_ctap2_1")] expected_response.extend( [ - 0x08, 0x18, 0x90, 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, + 0x08, 0x18, 0x70, 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, 0x6C, 0x67, 0x26, 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04, ] @@ -1141,7 +1131,7 @@ mod test { ]; expected_auth_data.push(INITIAL_SIGNATURE_COUNTER as u8); expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); - expected_auth_data.extend(&[0x00, CREDENTIAL_ID_BASE_SIZE as u8]); + expected_auth_data.extend(&[0x00, CREDENTIAL_ID_SIZE as u8]); assert_eq!( auth_data[0..expected_auth_data.len()], expected_auth_data[..] @@ -1186,7 +1176,6 @@ mod test { rp_id: String::from("example.com"), user_handle: vec![], user_display_name: None, - cred_random: None, cred_protect_policy: None, creation_order: 0, user_name: None, @@ -1291,7 +1280,7 @@ mod test { ]; expected_auth_data.push(INITIAL_SIGNATURE_COUNTER as u8); expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); - expected_auth_data.extend(&[0x00, CREDENTIAL_ID_MAX_SIZE as u8]); + expected_auth_data.extend(&[0x00, CREDENTIAL_ID_SIZE as u8]); assert_eq!( auth_data[0..expected_auth_data.len()], expected_auth_data[..] @@ -1488,8 +1477,8 @@ mod test { let auth_data = make_credential_response.auth_data; let offset = 37 + ctap_state.persistent_store.aaguid().unwrap().len(); assert_eq!(auth_data[offset], 0x00); - assert_eq!(auth_data[offset + 1] as usize, CREDENTIAL_ID_MAX_SIZE); - auth_data[offset + 2..offset + 2 + CREDENTIAL_ID_MAX_SIZE].to_vec() + assert_eq!(auth_data[offset + 1] as usize, CREDENTIAL_ID_SIZE); + auth_data[offset + 2..offset + 2 + CREDENTIAL_ID_SIZE].to_vec() } _ => panic!("Invalid response type"), }; @@ -1604,7 +1593,6 @@ mod test { rp_id: String::from("example.com"), user_handle: vec![0x1D], user_display_name: None, - cred_random: None, cred_protect_policy: Some( CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, ), @@ -1669,7 +1657,6 @@ mod test { rp_id: String::from("example.com"), user_handle: vec![0x1D], user_display_name: None, - cred_random: None, cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationRequired), creation_order: 0, user_name: None, @@ -1942,7 +1929,6 @@ mod test { rp_id: String::from("example.com"), user_handle: vec![], user_display_name: None, - cred_random: None, cred_protect_policy: None, creation_order: 0, user_name: None, @@ -2013,7 +1999,7 @@ mod test { // We are not testing the correctness of our SHA256 here, only if it is checked. let rp_id_hash = [0x55; 32]; let encrypted_id = ctap_state - .encrypt_key_handle(private_key.clone(), &rp_id_hash, None) + .encrypt_key_handle(private_key.clone(), &rp_id_hash) .unwrap(); let decrypted_source = ctap_state .decrypt_credential_source(encrypted_id, &rp_id_hash) @@ -2023,29 +2009,6 @@ mod test { assert_eq!(private_key, decrypted_source.private_key); } - #[test] - fn test_encrypt_decrypt_credential_with_cred_random() { - let mut rng = ThreadRng256 {}; - let user_immediately_present = |_| Ok(()); - let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - - // Usually, the relying party ID or its hash is provided by the client. - // We are not testing the correctness of our SHA256 here, only if it is checked. - let rp_id_hash = [0x55; 32]; - let cred_random = [0xC9; 32]; - let encrypted_id = ctap_state - .encrypt_key_handle(private_key.clone(), &rp_id_hash, Some(&cred_random)) - .unwrap(); - let decrypted_source = ctap_state - .decrypt_credential_source(encrypted_id, &rp_id_hash) - .unwrap() - .unwrap(); - - assert_eq!(private_key, decrypted_source.private_key); - assert_eq!(Some(cred_random.to_vec()), decrypted_source.cred_random); - } - #[test] fn test_encrypt_decrypt_bad_hmac() { let mut rng = ThreadRng256 {}; @@ -2056,30 +2019,7 @@ mod test { // Same as above. let rp_id_hash = [0x55; 32]; let encrypted_id = ctap_state - .encrypt_key_handle(private_key, &rp_id_hash, None) - .unwrap(); - for i in 0..encrypted_id.len() { - let mut modified_id = encrypted_id.clone(); - modified_id[i] ^= 0x01; - assert!(ctap_state - .decrypt_credential_source(modified_id, &rp_id_hash) - .unwrap() - .is_none()); - } - } - - #[test] - fn test_encrypt_decrypt_bad_hmac_with_cred_random() { - let mut rng = ThreadRng256 {}; - let user_immediately_present = |_| Ok(()); - let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - - // Same as above. - let rp_id_hash = [0x55; 32]; - let cred_random = [0xC9; 32]; - let encrypted_id = ctap_state - .encrypt_key_handle(private_key, &rp_id_hash, Some(&cred_random)) + .encrypt_key_handle(private_key, &rp_id_hash) .unwrap(); for i in 0..encrypted_id.len() { let mut modified_id = encrypted_id.clone(); diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 44aad71..410dac7 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -56,23 +56,16 @@ fn verify_pin_auth(hmac_key: &[u8], hmac_contents: &[u8], pin_auth: &[u8]) -> bo fn encrypt_hmac_secret_output( shared_secret: &[u8; 32], salt_enc: &[u8], - cred_random: &[u8], + cred_random: &[u8; 32], ) -> Result, 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 = [0u8; 16]; - let mut cred_random_secret = [0u8; 32]; - cred_random_secret.copy_from_slice(cred_random); - // With the if clause restriction above, block_len can only be 2 or 4. let block_len = salt_enc.len() / 16; let mut blocks = vec![[0u8; 16]; block_len]; @@ -84,7 +77,7 @@ fn encrypt_hmac_secret_output( let mut decrypted_salt1 = [0u8; 32]; decrypted_salt1[..16].copy_from_slice(&blocks[0]); decrypted_salt1[16..].copy_from_slice(&blocks[1]); - let output1 = hmac_256::(&cred_random_secret, &decrypted_salt1[..]); + let output1 = hmac_256::(&cred_random[..], &decrypted_salt1[..]); for i in 0..2 { blocks[i].copy_from_slice(&output1[16 * i..16 * (i + 1)]); } @@ -93,7 +86,7 @@ fn encrypt_hmac_secret_output( let mut decrypted_salt2 = [0u8; 32]; decrypted_salt2[..16].copy_from_slice(&blocks[2]); decrypted_salt2[16..].copy_from_slice(&blocks[3]); - let output2 = hmac_256::(&cred_random_secret, &decrypted_salt2[..]); + let output2 = hmac_256::(&cred_random[..], &decrypted_salt2[..]); for i in 0..2 { blocks[i + 2].copy_from_slice(&output2[16 * i..16 * (i + 1)]); } @@ -588,7 +581,7 @@ impl PinProtocolV1 { pub fn process_hmac_secret( &self, hmac_secret_input: GetAssertionHmacSecretInput, - cred_random: &Option>, + cred_random: &[u8; 32], ) -> Result, Ctap2StatusCode> { let GetAssertionHmacSecretInput { key_agreement, @@ -602,12 +595,7 @@ impl PinProtocolV1 { // Hard to tell what the correct error code here is. return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); } - - match cred_random { - Some(cr) => encrypt_hmac_secret_output(&shared_secret, &salt_enc[..], cr), - // This is the case if the credential was not created with HMAC-secret. - None => Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION), - } + encrypt_hmac_secret_output(&shared_secret, &salt_enc[..], cred_random) } #[cfg(feature = "with_ctap2_1")] @@ -1195,14 +1183,6 @@ mod test { let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random); assert_eq!(output.unwrap().len(), 64); - let salt_enc = [0x5E; 32]; - let cred_random = [0xC9; 33]; - let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random); - assert_eq!( - output, - Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION) - ); - let mut salt_enc = [0x00; 32]; let cred_random = [0xC9; 32]; diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index f48b70a..87c4c6c 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -138,6 +138,17 @@ impl PersistentStore { master_keys.extend_from_slice(&master_hmac_key); self.store.insert(key::MASTER_KEYS, &master_keys)?; } + + // Generate and store the CredRandom secrets if they are missing. + if self.store.find_handle(key::CRED_RANDOM_SECRET)?.is_none() { + let cred_random_with_uv = rng.gen_uniform_u8x32(); + let cred_random_without_uv = rng.gen_uniform_u8x32(); + let mut cred_random = Vec::with_capacity(64); + cred_random.extend_from_slice(&cred_random_without_uv); + cred_random.extend_from_slice(&cred_random_with_uv); + self.store.insert(key::CRED_RANDOM_SECRET, &cred_random)?; + } + // TODO(jmichel): remove this when vendor command is in place #[cfg(not(test))] self.load_attestation_data_from_firmware()?; @@ -335,6 +346,19 @@ impl PersistentStore { }) } + /// Returns the CredRandom secret. + pub fn cred_random_secret(&self, has_uv: bool) -> Result<[u8; 32], Ctap2StatusCode> { + let cred_random_secret = self + .store + .find(key::CRED_RANDOM_SECRET)? + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; + if cred_random_secret.len() != 64 { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + let offset = if has_uv { 32 } else { 0 }; + Ok(*array_ref![cred_random_secret, offset, 32]) + } + /// Returns the PIN hash if defined. pub fn pin_hash(&self) -> Result, Ctap2StatusCode> { let pin_hash = match self.store.find(key::PIN_HASH)? { @@ -657,7 +681,6 @@ mod test { rp_id: String::from(rp_id), user_handle, user_display_name: None, - cred_random: None, cred_protect_policy: None, creation_order: 0, user_name: None, @@ -814,7 +837,6 @@ mod test { rp_id: String::from("example.com"), user_handle: vec![0x00], user_display_name: None, - cred_random: None, cred_protect_policy: Some( CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, ), @@ -860,7 +882,6 @@ mod test { rp_id: String::from("example.com"), user_handle: vec![0x00], user_display_name: None, - cred_random: None, cred_protect_policy: None, creation_order: 0, user_name: None, @@ -882,7 +903,6 @@ mod test { rp_id: String::from("example.com"), user_handle: vec![0x00], user_display_name: None, - cred_random: None, cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationRequired), creation_order: 0, user_name: None, @@ -901,7 +921,7 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - // Master keys stay the same between resets. + // Master keys stay the same within the same CTAP reset cycle. let master_keys_1 = persistent_store.master_keys().unwrap(); let master_keys_2 = persistent_store.master_keys().unwrap(); assert_eq!(master_keys_2.encryption, master_keys_1.encryption); @@ -917,6 +937,28 @@ mod test { assert!(master_keys_3.hmac != master_hmac_key.as_slice()); } + #[test] + fn test_cred_random_secret() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + // CredRandom secrets stay the same within the same CTAP reset cycle. + let cred_random_with_uv_1 = persistent_store.cred_random_secret(true).unwrap(); + let cred_random_without_uv_1 = persistent_store.cred_random_secret(false).unwrap(); + let cred_random_with_uv_2 = persistent_store.cred_random_secret(true).unwrap(); + let cred_random_without_uv_2 = persistent_store.cred_random_secret(false).unwrap(); + assert_eq!(cred_random_with_uv_1, cred_random_with_uv_2); + assert_eq!(cred_random_without_uv_1, cred_random_without_uv_2); + + // CredRandom secrets change after reset. This test may fail if the random generator produces the + // same keys. + persistent_store.reset(&mut rng).unwrap(); + let cred_random_with_uv_3 = persistent_store.cred_random_secret(true).unwrap(); + let cred_random_without_uv_3 = persistent_store.cred_random_secret(false).unwrap(); + assert!(cred_random_with_uv_1 != cred_random_with_uv_3); + assert!(cred_random_without_uv_1 != cred_random_without_uv_3); + } + #[test] fn test_pin_hash() { let mut rng = ThreadRng256 {}; @@ -1084,7 +1126,6 @@ mod test { rp_id: String::from("example.com"), user_handle: vec![0x00], user_display_name: None, - cred_random: None, cred_protect_policy: None, creation_order: 0, user_name: None, diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index 6dab699..5c5b20e 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -88,6 +88,9 @@ make_partition! { /// board may configure `MAX_SUPPORTED_RESIDENTIAL_KEYS` depending on the storage size. CREDENTIALS = 1700..2000; + /// The secret of the CredRandom feature. + CRED_RANDOM_SECRET = 2041; + /// List of RP IDs allowed to read the minimum PIN length. #[cfg(feature = "with_ctap2_1")] _MIN_PIN_LENGTH_RP_IDS = 2042;