diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index 726634c..e96ce7a 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -248,6 +248,62 @@ mod test { ); } + #[test] + fn test_process_set_min_pin_length_force_pin_change_implicit() { + 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(); + // Increase min PIN, force PIN change. + let min_pin_length = 6; + let mut config_params = create_min_pin_config_params(min_pin_length, None); + let pin_uv_auth_param = Some(vec![ + 0x81, 0x37, 0x37, 0xF3, 0xD8, 0x69, 0xBD, 0x74, 0xFE, 0x88, 0x30, 0x8C, 0xC4, 0x2E, + 0xA8, 0xC8, + ]); + config_params.pin_uv_auth_param = pin_uv_auth_param; + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); + assert_eq!(persistent_store.has_force_pin_change(), Ok(true)); + } + + #[test] + fn test_process_set_min_pin_length_force_pin_change_explicit() { + 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![ + 0xE3, 0x74, 0xF4, 0x27, 0xBE, 0x7D, 0x40, 0xB5, 0x71, 0xB6, 0xB4, 0x1A, 0xD2, 0xC1, + 0x53, 0xD7, + ]); + let set_min_pin_length_params = SetMinPinLengthParams { + new_min_pin_length: Some(persistent_store.min_pin_length().unwrap()), + min_pin_length_rp_ids: None, + force_change_pin: Some(true), + }; + let config_params = AuthenticatorConfigParameters { + sub_command: ConfigSubCommand::SetMinPinLength, + sub_command_params: Some(ConfigSubCommandParams::SetMinPinLength( + set_min_pin_length_params, + )), + 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_eq!(persistent_store.has_force_pin_change(), Ok(true)); + } + #[test] fn test_process_config_vendor_prototype() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 6ffd108..8a4479a 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -1006,6 +1006,10 @@ where ); options_map.insert(String::from("credMgmt"), true); options_map.insert(String::from("setMinPINLength"), true); + options_map.insert( + String::from("forcePINChange"), + self.persistent_store.has_force_pin_change()?, + ); Ok(ResponseData::AuthenticatorGetInfo( AuthenticatorGetInfoResponse { versions: vec![ @@ -1256,13 +1260,15 @@ mod test { expected_response.extend(&ctap_state.persistent_store.aaguid().unwrap()); expected_response.extend( [ - 0x04, 0xA5, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x68, 0x63, 0x72, 0x65, + 0x04, 0xA6, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x68, 0x63, 0x72, 0x65, 0x64, 0x4D, 0x67, 0x6D, 0x74, 0xF5, 0x69, 0x63, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x50, - 0x69, 0x6E, 0xF4, 0x6F, 0x73, 0x65, 0x74, 0x4D, 0x69, 0x6E, 0x50, 0x49, 0x4E, 0x4C, - 0x65, 0x6E, 0x67, 0x74, 0x68, 0xF5, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, 0x01, 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, 0x0F, 0x18, 0x20, 0x10, 0x08, 0x14, 0x18, 0x96, + 0x69, 0x6E, 0xF4, 0x6E, 0x66, 0x6F, 0x72, 0x63, 0x65, 0x50, 0x49, 0x4E, 0x43, 0x68, + 0x61, 0x6E, 0x67, 0x65, 0xF4, 0x6F, 0x73, 0x65, 0x74, 0x4D, 0x69, 0x6E, 0x50, 0x49, + 0x4E, 0x4C, 0x65, 0x6E, 0x67, 0x74, 0x68, 0xF5, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, + 0x01, 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, 0x0F, 0x18, 0x20, 0x10, 0x08, 0x14, + 0x18, 0x96, ] .iter(), ); diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 0e46573..ae1b1df 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -328,6 +328,9 @@ impl PinProtocolV1 { let token_encryption_key = crypto::aes256::EncryptionKey::new(&shared_secret); let pin_decryption_key = crypto::aes256::DecryptionKey::new(&token_encryption_key); self.verify_pin_hash_enc(rng, persistent_store, &pin_decryption_key, pin_hash_enc)?; + if persistent_store.has_force_pin_change()? { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); + } // Assuming PIN_TOKEN_LENGTH % block_size == 0 here. let iv = [0u8; 16]; @@ -821,6 +824,28 @@ mod test { ); } + #[test] + fn test_process_get_pin_token_force_pin_change() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + set_standard_pin(&mut persistent_store); + assert_eq!(persistent_store.force_pin_change(), Ok(())); + let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let pk = pin_protocol_v1.key_agreement_key.genpk(); + let shared_secret = pin_protocol_v1.key_agreement_key.exchange_x_sha256(&pk); + let key_agreement = CoseKey::from(pk); + let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); + assert_eq!( + pin_protocol_v1.process_get_pin_token( + &mut rng, + &mut persistent_store, + key_agreement, + pin_hash_enc + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID), + ); + } + #[test] fn test_process_get_pin_uv_auth_token_using_pin_with_permissions() { let mut rng = ThreadRng256 {}; @@ -885,6 +910,30 @@ mod test { ); } + #[test] + fn test_process_get_pin_token_force_pin_change_force_pin_change() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + set_standard_pin(&mut persistent_store); + assert_eq!(persistent_store.force_pin_change(), Ok(())); + let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let pk = pin_protocol_v1.key_agreement_key.genpk(); + let shared_secret = pin_protocol_v1.key_agreement_key.exchange_x_sha256(&pk); + let key_agreement = CoseKey::from(pk); + let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); + assert_eq!( + pin_protocol_v1.process_get_pin_uv_auth_token_using_pin_with_permissions( + &mut rng, + &mut persistent_store, + key_agreement, + pin_hash_enc, + 0x03, + Some(String::from("example.com")), + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID), + ); + } + #[test] fn test_process() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index ffc5dd6..74c11a6 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -384,7 +384,9 @@ impl PersistentStore { let mut pin_properties = [0; 1 + PIN_AUTH_LENGTH]; pin_properties[0] = pin_code_point_length; pin_properties[1..].clone_from_slice(pin_hash); - Ok(self.store.insert(key::PIN_PROPERTIES, &pin_properties)?) + self.store.insert(key::PIN_PROPERTIES, &pin_properties)?; + // If this second transaction fails, you are forced to retry. + Ok(self.store.remove(key::FORCE_PIN_CHANGE)?) } /// Returns the number of remaining PIN retries. @@ -541,9 +543,18 @@ impl PersistentStore { Ok(()) } + /// Returns whether the PIN needs to be changed before its next usage. + pub fn has_force_pin_change(&self) -> Result { + match self.store.find(key::FORCE_PIN_CHANGE)? { + None => Ok(false), + Some(value) if value.len() == 1 && value[0] == 1 => Ok(true), + _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), + } + } + + /// Marks the PIN as outdated with respect to the new PIN policy. pub fn force_pin_change(&mut self) -> Result<(), Ctap2StatusCode> { - // TODO(kaczmarczyck) implement storage logic - Ok(()) + Ok(self.store.insert(key::FORCE_PIN_CHANGE, &[1])?) } } @@ -1148,6 +1159,18 @@ mod test { } } + #[test] + fn test_force_pin_change() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + assert!(!persistent_store.has_force_pin_change().unwrap()); + assert_eq!(persistent_store.force_pin_change(), Ok(())); + assert!(persistent_store.has_force_pin_change().unwrap()); + assert_eq!(persistent_store.set_pin(&[0x88; 16], 8), Ok(())); + assert!(!persistent_store.has_force_pin_change().unwrap()); + } + #[test] fn test_serialize_deserialize_credential() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index c6e46e2..1c0e21e 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -88,6 +88,9 @@ make_partition! { /// board may configure `MAX_SUPPORTED_RESIDENT_KEYS` depending on the storage size. CREDENTIALS = 1700..2000; + /// If this entry exists and equals 1, the PIN needs to be changed. + FORCE_PIN_CHANGE = 2040; + /// The secret of the CredRandom feature. CRED_RANDOM_SECRET = 2041;