diff --git a/src/ctap/client_pin.rs b/src/ctap/client_pin.rs index a580b1b..82e7904 100644 --- a/src/ctap/client_pin.rs +++ b/src/ctap/client_pin.rs @@ -20,6 +20,7 @@ use super::pin_protocol::{verify_pin_uv_auth_token, PinProtocol, SharedSecret}; use super::response::{AuthenticatorClientPinResponse, ResponseData}; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; +use super::token_state::PinUvAuthTokenState; use alloc::boxed::Box; use alloc::str; use alloc::string::String; @@ -30,6 +31,7 @@ use crypto::sha256::Sha256; use crypto::Hash256; #[cfg(test)] use enum_iterator::IntoEnumIterator; +use libtock_drivers::timer::ClockValue; use subtle::ConstantTimeEq; /// The prefix length of the PIN hash that is stored and compared. @@ -104,8 +106,7 @@ pub struct ClientPin { pin_protocol_v1: PinProtocol, pin_protocol_v2: PinProtocol, consecutive_pin_mismatches: u8, - permissions: u8, - permissions_rp_id: Option, + pin_uv_auth_token_state: PinUvAuthTokenState, } impl ClientPin { @@ -114,8 +115,7 @@ impl ClientPin { pin_protocol_v1: PinProtocol::new(rng), pin_protocol_v2: PinProtocol::new(rng), consecutive_pin_mismatches: 0, - permissions: 0, - permissions_rp_id: None, + pin_uv_auth_token_state: PinUvAuthTokenState::new(), } } @@ -290,6 +290,7 @@ impl ClientPin { rng: &mut impl Rng256, persistent_store: &mut PersistentStore, client_pin_params: AuthenticatorClientPinParameters, + now: ClockValue, ) -> Result { let AuthenticatorClientPinParameters { pin_uv_auth_protocol, @@ -314,14 +315,17 @@ impl ClientPin { if persistent_store.has_force_pin_change()? { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); } - let pin_token = shared_secret.encrypt( rng, self.get_pin_protocol(pin_uv_auth_protocol) .get_pin_uv_auth_token(), )?; - self.permissions = 0x03; - self.permissions_rp_id = None; + + self.pin_protocol_v1.reset_pin_uv_auth_token(rng); + self.pin_protocol_v2.reset_pin_uv_auth_token(rng); + self.pin_uv_auth_token_state + .begin_using_pin_uv_auth_token(now); + self.pin_uv_auth_token_state.set_default_permissions(); Ok(AuthenticatorClientPinResponse { key_agreement: None, @@ -350,6 +354,7 @@ impl ClientPin { rng: &mut impl Rng256, persistent_store: &mut PersistentStore, mut client_pin_params: AuthenticatorClientPinParameters, + now: ClockValue, ) -> Result { let permissions = ok_or_missing(client_pin_params.permissions)?; // Mutating client_pin_params is just an optimization to move it into @@ -364,10 +369,10 @@ impl ClientPin { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } - let response = self.process_get_pin_token(rng, persistent_store, client_pin_params)?; - - self.permissions = permissions; - self.permissions_rp_id = permissions_rp_id; + let response = self.process_get_pin_token(rng, persistent_store, client_pin_params, now)?; + self.pin_uv_auth_token_state.set_permissions(permissions); + self.pin_uv_auth_token_state + .set_permissions_rp_id(permissions_rp_id); Ok(response) } @@ -378,6 +383,7 @@ impl ClientPin { rng: &mut impl Rng256, persistent_store: &mut PersistentStore, client_pin_params: AuthenticatorClientPinParameters, + now: ClockValue, ) -> Result { let response = match client_pin_params.sub_command { ClientPinSubCommand::GetPinRetries => { @@ -395,7 +401,7 @@ impl ClientPin { None } ClientPinSubCommand::GetPinToken => { - Some(self.process_get_pin_token(rng, persistent_store, client_pin_params)?) + Some(self.process_get_pin_token(rng, persistent_store, client_pin_params, now)?) } ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions => Some( self.process_get_pin_uv_auth_token_using_uv_with_permissions(client_pin_params)?, @@ -406,6 +412,7 @@ impl ClientPin { rng, persistent_store, client_pin_params, + now, )?, ), }; @@ -419,6 +426,9 @@ impl ClientPin { pin_uv_auth_param: &[u8], pin_uv_auth_protocol: PinUvAuthProtocol, ) -> Result<(), Ctap2StatusCode> { + if !self.pin_uv_auth_token_state.is_in_use() { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); + } verify_pin_uv_auth_token( self.get_pin_protocol(pin_uv_auth_protocol) .get_pin_uv_auth_token(), @@ -435,8 +445,7 @@ impl ClientPin { self.pin_protocol_v2.regenerate(rng); self.pin_protocol_v2.reset_pin_uv_auth_token(rng); self.consecutive_pin_mismatches = 0; - self.permissions = 0; - self.permissions_rp_id = None; + self.pin_uv_auth_token_state.stop_using_pin_uv_auth_token(); } /// Verifies, computes and encrypts the HMAC-secret outputs. @@ -476,30 +485,43 @@ impl ClientPin { shared_secret.encrypt(rng, &output) } - /// Check if the required command's token permission is granted. - pub fn has_permission(&self, permission: PinPermission) -> Result<(), Ctap2StatusCode> { - // Relies on the fact that all permissions are represented by powers of two. - if permission as u8 & self.permissions != 0 { + /// Consumes flags and permissions related to the pinUvAuthToken. + pub fn clear_token_flags(&mut self) { + self.pin_uv_auth_token_state.clear_user_verified_flag(); + self.pin_uv_auth_token_state + .clear_pin_uv_auth_token_permissions_except_lbw(); + } + + /// Updates the running timers, triggers timeout events. + pub fn update_timeouts(&mut self, now: ClockValue) { + self.pin_uv_auth_token_state + .pin_uv_auth_token_usage_timer_observer(now); + } + + /// Checks if user verification is cached for use of the pinUvAuthToken. + pub fn check_user_verified_flag(&mut self) -> Result<(), Ctap2StatusCode> { + if self.pin_uv_auth_token_state.get_user_verified_flag_value() { Ok(()) } else { Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) } } + /// Check if the required command's token permission is granted. + pub fn has_permission(&self, permission: PinPermission) -> Result<(), Ctap2StatusCode> { + self.pin_uv_auth_token_state.has_permission(permission) + } + /// Check if no RP ID is associated with the token permission. pub fn has_no_rp_id_permission(&self) -> Result<(), Ctap2StatusCode> { - if self.permissions_rp_id.is_some() { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - Ok(()) + self.pin_uv_auth_token_state.has_no_permissions_rp_id() } /// Check if no or the passed RP ID is associated with the token permission. pub fn has_no_or_rp_id_permission(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { - match &self.permissions_rp_id { - Some(p) if rp_id != p => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), - _ => Ok(()), - } + self.pin_uv_auth_token_state + .has_no_permissions_rp_id() + .or_else(|_| self.pin_uv_auth_token_state.has_permissions_rp_id(rp_id)) } /// Check if no RP ID is associated with the token permission, or it matches the hash. @@ -507,26 +529,28 @@ impl ClientPin { &self, rp_id_hash: &[u8], ) -> Result<(), Ctap2StatusCode> { - match &self.permissions_rp_id { - Some(p) if rp_id_hash != Sha256::hash(p.as_bytes()) => { - Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) - } - _ => Ok(()), - } + self.pin_uv_auth_token_state + .has_no_permissions_rp_id() + .or_else(|_| { + self.pin_uv_auth_token_state + .has_permissions_rp_id_hash(rp_id_hash) + }) } /// Check if the passed RP ID is associated with the token permission. /// /// If no RP ID is associated, associate the passed RP ID as a side effect. pub fn ensure_rp_id_permission(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { - match &self.permissions_rp_id { - Some(p) if rp_id != p => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), - None => { - self.permissions_rp_id = Some(String::from(rp_id)); - Ok(()) - } - _ => Ok(()), + if self + .pin_uv_auth_token_state + .has_no_permissions_rp_id() + .is_ok() + { + self.pin_uv_auth_token_state + .set_permissions_rp_id(Some(String::from(rp_id))); + return Ok(()); } + self.pin_uv_auth_token_state.has_permissions_rp_id(rp_id) } #[cfg(test)] @@ -541,12 +565,16 @@ impl ClientPin { PinUvAuthProtocol::V1 => (key_agreement_key, crypto::ecdh::SecKey::gensk(&mut rng)), PinUvAuthProtocol::V2 => (crypto::ecdh::SecKey::gensk(&mut rng), key_agreement_key), }; + let mut pin_uv_auth_token_state = PinUvAuthTokenState::new(); + pin_uv_auth_token_state.set_permissions(0xFF); + const CLOCK_FREQUENCY_HZ: usize = 32768; + const DUMMY_CLOCK_VALUE: ClockValue = ClockValue::new(0, CLOCK_FREQUENCY_HZ); + pin_uv_auth_token_state.begin_using_pin_uv_auth_token(DUMMY_CLOCK_VALUE); ClientPin { pin_protocol_v1: PinProtocol::new_test(key_agreement_key_v1, pin_uv_auth_token), pin_protocol_v2: PinProtocol::new_test(key_agreement_key_v2, pin_uv_auth_token), consecutive_pin_mismatches: 0, - permissions: 0xFF, - permissions_rp_id: None, + pin_uv_auth_token_state, } } } @@ -557,6 +585,10 @@ mod test { use super::*; use alloc::vec; use crypto::rng256::ThreadRng256; + use libtock_drivers::timer::Duration; + + const CLOCK_FREQUENCY_HZ: usize = 32768; + const DUMMY_CLOCK_VALUE: ClockValue = ClockValue::new(0, CLOCK_FREQUENCY_HZ); /// Stores a PIN hash corresponding to the dummy PIN "1234". fn set_standard_pin(persistent_store: &mut PersistentStore) { @@ -782,7 +814,7 @@ mod test { retries: Some(persistent_store.pin_retries().unwrap() as u64), }); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, params), + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), Ok(ResponseData::AuthenticatorClientPin(expected_response)) ); } @@ -810,7 +842,7 @@ mod test { retries: None, }); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, params), + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), Ok(ResponseData::AuthenticatorClientPin(expected_response)) ); } @@ -831,7 +863,7 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, params), + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), Ok(ResponseData::AuthenticatorClientPin(None)) ); } @@ -865,14 +897,24 @@ mod test { let pin_uv_auth_param = shared_secret.authenticate(&auth_param_data); params.pin_uv_auth_param = Some(pin_uv_auth_param); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, params.clone()), + client_pin.process_command( + &mut rng, + &mut persistent_store, + params.clone(), + DUMMY_CLOCK_VALUE + ), Ok(ResponseData::AuthenticatorClientPin(None)) ); let mut bad_params = params.clone(); bad_params.pin_hash_enc = Some(vec![0xEE; 16]); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, bad_params), + client_pin.process_command( + &mut rng, + &mut persistent_store, + bad_params, + DUMMY_CLOCK_VALUE + ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); @@ -880,7 +922,7 @@ mod test { persistent_store.decr_pin_retries().unwrap(); } assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, params), + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED) ); } @@ -905,13 +947,41 @@ mod test { set_standard_pin(&mut persistent_store); assert!(client_pin - .process_command(&mut rng, &mut persistent_store, params.clone()) + .process_command( + &mut rng, + &mut persistent_store, + params.clone(), + DUMMY_CLOCK_VALUE + ) .is_ok()); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(PinPermission::MakeCredential), + Ok(()) + ); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(PinPermission::GetAssertion), + Ok(()) + ); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_no_permissions_rp_id(), + Ok(()) + ); let mut bad_params = params; bad_params.pin_hash_enc = Some(vec![0xEE; 16]); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, bad_params), + client_pin.process_command( + &mut rng, + &mut persistent_store, + bad_params, + DUMMY_CLOCK_VALUE + ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) ); } @@ -937,7 +1007,7 @@ mod test { assert_eq!(persistent_store.force_pin_change(), Ok(())); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, params), + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID), ); } @@ -964,32 +1034,65 @@ mod test { set_standard_pin(&mut persistent_store); assert!(client_pin - .process_command(&mut rng, &mut persistent_store, params.clone()) + .process_command( + &mut rng, + &mut persistent_store, + params.clone(), + DUMMY_CLOCK_VALUE + ) .is_ok()); - assert_eq!(client_pin.permissions, 0x03); assert_eq!( - client_pin.permissions_rp_id, - Some(String::from("example.com")) + client_pin + .pin_uv_auth_token_state + .has_permission(PinPermission::MakeCredential), + Ok(()) + ); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(PinPermission::GetAssertion), + Ok(()) + ); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permissions_rp_id("example.com"), + Ok(()) ); let mut bad_params = params.clone(); bad_params.permissions = Some(0x00); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, bad_params), + client_pin.process_command( + &mut rng, + &mut persistent_store, + bad_params, + DUMMY_CLOCK_VALUE + ), Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) ); let mut bad_params = params.clone(); bad_params.permissions_rp_id = None; assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, bad_params), + client_pin.process_command( + &mut rng, + &mut persistent_store, + bad_params, + DUMMY_CLOCK_VALUE + ), Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) ); let mut bad_params = params; bad_params.pin_hash_enc = Some(vec![0xEE; 16]); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, bad_params), + client_pin.process_command( + &mut rng, + &mut persistent_store, + bad_params, + DUMMY_CLOCK_VALUE + ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) ); } @@ -1017,7 +1120,7 @@ mod test { assert_eq!(persistent_store.force_pin_change(), Ok(())); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, params), + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) ); } @@ -1275,14 +1378,21 @@ mod test { fn test_has_permission() { let mut rng = ThreadRng256 {}; let mut client_pin = ClientPin::new(&mut rng); - client_pin.permissions = 0x7F; - for permission in PinPermission::into_enum_iter() { - assert_eq!(client_pin.has_permission(permission), Ok(())); - } - client_pin.permissions = 0x00; + client_pin.pin_uv_auth_token_state.set_permissions(0x7F); for permission in PinPermission::into_enum_iter() { assert_eq!( - client_pin.has_permission(permission), + client_pin + .pin_uv_auth_token_state + .has_permission(permission), + Ok(()) + ); + } + client_pin.pin_uv_auth_token_state.set_permissions(0x00); + for permission in PinPermission::into_enum_iter() { + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(permission), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } @@ -1293,8 +1403,9 @@ mod test { let mut rng = ThreadRng256 {}; let mut client_pin = ClientPin::new(&mut rng); assert_eq!(client_pin.has_no_rp_id_permission(), Ok(())); - assert_eq!(client_pin.permissions_rp_id, None); - client_pin.permissions_rp_id = Some("example.com".to_string()); + client_pin + .pin_uv_auth_token_state + .set_permissions_rp_id(Some("example.com".to_string())); assert_eq!( client_pin.has_no_rp_id_permission(), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) @@ -1306,8 +1417,9 @@ mod test { let mut rng = ThreadRng256 {}; let mut client_pin = ClientPin::new(&mut rng); assert_eq!(client_pin.has_no_or_rp_id_permission("example.com"), Ok(())); - assert_eq!(client_pin.permissions_rp_id, None); - client_pin.permissions_rp_id = Some("example.com".to_string()); + client_pin + .pin_uv_auth_token_state + .set_permissions_rp_id(Some("example.com".to_string())); assert_eq!(client_pin.has_no_or_rp_id_permission("example.com"), Ok(())); assert_eq!( client_pin.has_no_or_rp_id_permission("another.example.com"), @@ -1324,8 +1436,9 @@ mod test { client_pin.has_no_or_rp_id_hash_permission(&rp_id_hash), Ok(()) ); - assert_eq!(client_pin.permissions_rp_id, None); - client_pin.permissions_rp_id = Some("example.com".to_string()); + client_pin + .pin_uv_auth_token_state + .set_permissions_rp_id(Some("example.com".to_string())); assert_eq!( client_pin.has_no_or_rp_id_hash_permission(&rp_id_hash), Ok(()) @@ -1342,12 +1455,14 @@ mod test { let mut client_pin = ClientPin::new(&mut rng); assert_eq!(client_pin.ensure_rp_id_permission("example.com"), Ok(())); assert_eq!( - client_pin.permissions_rp_id, - Some(String::from("example.com")) + client_pin + .pin_uv_auth_token_state + .has_permissions_rp_id("example.com"), + Ok(()) ); assert_eq!(client_pin.ensure_rp_id_permission("example.com"), Ok(())); assert_eq!( - client_pin.ensure_rp_id_permission("counter-example.com"), + client_pin.ensure_rp_id_permission("another.example.com"), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } @@ -1355,8 +1470,11 @@ mod test { #[test] fn test_verify_pin_uv_auth_token() { let mut rng = ThreadRng256 {}; - let client_pin = ClientPin::new(&mut rng); + let mut client_pin = ClientPin::new(&mut rng); let message = [0xAA]; + client_pin + .pin_uv_auth_token_state + .begin_using_pin_uv_auth_token(DUMMY_CLOCK_VALUE); let pin_uv_auth_token_v1 = client_pin .get_pin_protocol(PinUvAuthProtocol::V1) @@ -1423,6 +1541,28 @@ mod test { ); } + #[test] + fn test_verify_pin_uv_auth_token_not_in_use() { + let mut rng = ThreadRng256 {}; + let client_pin = ClientPin::new(&mut rng); + let message = [0xAA]; + + let pin_uv_auth_token_v1 = client_pin + .get_pin_protocol(PinUvAuthProtocol::V1) + .get_pin_uv_auth_token(); + let pin_uv_auth_param_v1 = + authenticate_pin_uv_auth_token(&pin_uv_auth_token_v1, &message, PinUvAuthProtocol::V1); + + assert_eq!( + client_pin.verify_pin_uv_auth_token( + &message, + &pin_uv_auth_param_v1, + PinUvAuthProtocol::V1 + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + #[test] fn test_reset() { let mut rng = ThreadRng256 {}; @@ -1431,8 +1571,10 @@ mod test { let public_key_v2 = client_pin.pin_protocol_v2.get_public_key(); let token_v1 = *client_pin.pin_protocol_v1.get_pin_uv_auth_token(); let token_v2 = *client_pin.pin_protocol_v2.get_pin_uv_auth_token(); - client_pin.permissions = 0xFF; - client_pin.permissions_rp_id = Some(String::from("example.com")); + client_pin.pin_uv_auth_token_state.set_permissions(0xFF); + client_pin + .pin_uv_auth_token_state + .set_permissions_rp_id(Some(String::from("example.com"))); client_pin.reset(&mut rng); assert_ne!(public_key_v1, client_pin.pin_protocol_v1.get_public_key()); assert_ne!(public_key_v2, client_pin.pin_protocol_v2.get_public_key()); @@ -1452,4 +1594,94 @@ mod test { } assert_eq!(client_pin.has_no_rp_id_permission(), Ok(())); } + + #[test] + fn test_update_timeouts() { + let (mut client_pin, mut params) = create_client_pin_and_parameters( + PinUvAuthProtocol::V2, + ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions, + ); + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + set_standard_pin(&mut persistent_store); + params.permissions = Some(0xFF); + + assert!(client_pin + .process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE) + .is_ok()); + for permission in PinPermission::into_enum_iter() { + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(permission), + Ok(()) + ); + } + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permissions_rp_id("example.com"), + Ok(()) + ); + + let timeout = DUMMY_CLOCK_VALUE.wrapping_add(Duration::from_ms(30001)); + client_pin.update_timeouts(timeout); + for permission in PinPermission::into_enum_iter() { + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(permission), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permissions_rp_id("example.com"), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_clear_token_flags() { + let (mut client_pin, mut params) = create_client_pin_and_parameters( + PinUvAuthProtocol::V2, + ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions, + ); + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + set_standard_pin(&mut persistent_store); + params.permissions = Some(0xFF); + + assert!(client_pin + .process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE) + .is_ok()); + for permission in PinPermission::into_enum_iter() { + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(permission), + Ok(()) + ); + } + assert_eq!(client_pin.check_user_verified_flag(), Ok(())); + + client_pin.clear_token_flags(); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(PinPermission::CredentialManagement), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(PinPermission::LargeBlobWrite), + Ok(()) + ); + assert_eq!( + client_pin.check_user_verified_flag(), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } } diff --git a/src/ctap/command.rs b/src/ctap/command.rs index c0ecb2d..4616b02 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -143,7 +143,7 @@ impl Command { } } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct AuthenticatorMakeCredentialParameters { pub client_data_hash: Vec, pub rp: PublicKeyCredentialRpEntity, diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 84fe721..1721e62 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -148,7 +148,7 @@ impl TryFrom for PublicKeyCredentialType { } // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialparameters -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct PublicKeyCredentialParameter { pub cred_type: PublicKeyCredentialType, pub alg: SignatureAlgorithm, @@ -387,7 +387,7 @@ impl TryFrom for GetAssertionHmacSecretInput { } // Even though options are optional, we can use the default if not present. -#[derive(Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct MakeCredentialOptions { pub rk: bool, pub uv: bool, @@ -484,7 +484,7 @@ impl From for cbor::Value { } } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub enum SignatureAlgorithm { ES256 = ES256_ALGORITHM as isize, // This is the default for all numbers not covered above. diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 83db5a9..46d805b 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -29,6 +29,7 @@ pub mod response; pub mod status_code; mod storage; mod timed_permission; +mod token_state; use self::client_pin::{ClientPin, PinPermission}; use self::command::{ @@ -301,11 +302,12 @@ where } } - pub fn update_command_permission(&mut self, now: ClockValue) { + pub fn update_timeouts(&mut self, now: ClockValue) { // Ignore the result, just update. let _ = self .stateful_command_permission .check_command_permission(now); + self.client_pin.update_timeouts(now); } pub fn increment_global_signature_counter(&mut self) -> Result<(), Ctap2StatusCode> { @@ -465,6 +467,7 @@ where self.rng, &mut self.persistent_store, params, + now, ), Command::AuthenticatorReset => self.process_reset(cid, now), Command::AuthenticatorCredentialManagement(params) => { @@ -641,6 +644,10 @@ where )?; self.client_pin .has_permission(PinPermission::MakeCredential)?; + self.client_pin.check_user_verified_flag()?; + // Checking for the correct permissions_rp_id is specified earlier. + // Error codes are identical though, so the implementation can be identical with + // GetAssertion. self.client_pin.ensure_rp_id_permission(&rp_id)?; UP_FLAG | UV_FLAG | AT_FLAG | ed_flag } @@ -660,6 +667,7 @@ where }; (self.check_user_presence)(cid)?; + self.client_pin.clear_token_flags(); let sk = crypto::ecdsa::SecKey::gensk(self.rng); let pk = sk.genpk(); @@ -932,6 +940,10 @@ where )?; self.client_pin .has_permission(PinPermission::GetAssertion)?; + // Checking for the UV flag is specified earlier for GetAssertion. + // Error codes are identical though, so the implementation can be identical with + // MakeCredential. + self.client_pin.check_user_verified_flag()?; self.client_pin.ensure_rp_id_permission(&rp_id)?; UV_FLAG } @@ -987,6 +999,7 @@ where // For CTAP 2.1, it was moved to a later protocol step. if options.up { (self.check_user_presence)(cid)?; + self.client_pin.clear_token_flags(); } let credential = credential.ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; @@ -1777,7 +1790,7 @@ mod test { make_credential_params.pin_uv_auth_param = Some(pin_uv_auth_param); make_credential_params.pin_uv_auth_protocol = Some(pin_uv_auth_protocol); let make_credential_response = - ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + ctap_state.process_make_credential(make_credential_params.clone(), DUMMY_CHANNEL_ID); check_make_response( make_credential_response, @@ -1786,6 +1799,13 @@ mod test { 0x20, &[], ); + + 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] @@ -2088,6 +2108,7 @@ mod test { ctap_state.rng, &mut ctap_state.persistent_store, client_pin_params, + DUMMY_CLOCK_VALUE, ); let get_assertion_params = get_assertion_hmac_secret_params( key_agreement_key, @@ -2145,6 +2166,7 @@ mod test { ctap_state.rng, &mut ctap_state.persistent_store, client_pin_params, + DUMMY_CLOCK_VALUE, ); let get_assertion_params = get_assertion_hmac_secret_params( key_agreement_key, @@ -2423,7 +2445,6 @@ mod test { let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - ctap_state.client_pin = client_pin; let mut make_credential_params = create_minimal_make_credential_parameters(); let user1 = PublicKeyCredentialUserEntity { @@ -2448,6 +2469,7 @@ mod test { .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) .is_ok()); + ctap_state.client_pin = client_pin; // The PIN length is outside of the test scope and most likely incorrect. ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); let client_data_hash = vec![0xCD]; diff --git a/src/ctap/token_state.rs b/src/ctap/token_state.rs new file mode 100644 index 0000000..fea0f3c --- /dev/null +++ b/src/ctap/token_state.rs @@ -0,0 +1,277 @@ +// Copyright 2021 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 crate::ctap::client_pin::PinPermission; +use crate::ctap::status_code::Ctap2StatusCode; +use crate::ctap::timed_permission::TimedPermission; +use alloc::string::String; +use crypto::sha256::Sha256; +use crypto::Hash256; +use libtock_drivers::timer::{ClockValue, Duration}; + +/// Timeout for auth tokens. +/// +/// This usage time limit is correct for USB, BLE, and internal. +/// NFC only allows 19.8 seconds. +/// TODO(#15) multiplex over transports, add NFC +const INITIAL_USAGE_TIME_LIMIT: Duration = Duration::from_ms(30000); + +/// Implements pinUvAuthToken state from section 6.5.2.1. +/// +/// The userPresent flag is omitted as the only way to set it to true is +/// built-in user verification. Therefore, we never cache user presence. +/// +/// This implementation does not use a rolling timer. +pub struct PinUvAuthTokenState { + // Relies on the fact that all permissions are represented by powers of two. + permissions_set: u8, + permissions_rp_id: Option, + usage_timer: TimedPermission, + user_verified: bool, + in_use: bool, +} + +impl PinUvAuthTokenState { + /// Creates a pinUvAuthToken state without permissions. + pub fn new() -> PinUvAuthTokenState { + PinUvAuthTokenState { + permissions_set: 0, + permissions_rp_id: None, + usage_timer: TimedPermission::waiting(), + user_verified: false, + in_use: false, + } + } + + /// Returns whether the pinUvAuthToken is active. + pub fn is_in_use(&self) -> bool { + self.in_use + } + + /// Checks if the permission is granted. + pub fn has_permission(&self, permission: PinPermission) -> Result<(), Ctap2StatusCode> { + if permission as u8 & self.permissions_set != 0 { + Ok(()) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + } + } + + /// Checks if there is no associated permissions RPID. + pub fn has_no_permissions_rp_id(&self) -> Result<(), Ctap2StatusCode> { + if self.permissions_rp_id.is_some() { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); + } + Ok(()) + } + + /// Checks if the permissions RPID is associated. + pub fn has_permissions_rp_id(&self, rp_id: &str) -> Result<(), Ctap2StatusCode> { + match &self.permissions_rp_id { + Some(p) if rp_id == p => Ok(()), + _ => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), + } + } + + /// Checks if the permissions RPID's association matches the hash. + pub fn has_permissions_rp_id_hash(&self, rp_id_hash: &[u8]) -> Result<(), Ctap2StatusCode> { + match &self.permissions_rp_id { + Some(p) if rp_id_hash == Sha256::hash(p.as_bytes()) => Ok(()), + _ => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), + } + } + + /// Sets the permissions, represented as bits in a byte. + pub fn set_permissions(&mut self, permissions: u8) { + self.permissions_set = permissions; + } + + /// Sets the permissions RPID. + pub fn set_permissions_rp_id(&mut self, permissions_rp_id: Option) { + self.permissions_rp_id = permissions_rp_id; + } + + /// Sets the default permissions. + /// + /// Allows MakeCredential and GetAssertion, without specifying a RP ID. + pub fn set_default_permissions(&mut self) { + self.set_permissions(0x03); + self.set_permissions_rp_id(None); + } + + /// Starts the timer for pinUvAuthToken usage. + pub fn begin_using_pin_uv_auth_token(&mut self, now: ClockValue) { + self.user_verified = true; + self.usage_timer = TimedPermission::granted(now, INITIAL_USAGE_TIME_LIMIT); + self.in_use = true; + } + + /// Updates the usage timer, and disables the pinUvAuthToken on timeout. + pub fn pin_uv_auth_token_usage_timer_observer(&mut self, now: ClockValue) { + if !self.in_use { + return; + } + self.usage_timer = self.usage_timer.check_expiration(now); + if !self.usage_timer.is_granted(now) { + self.stop_using_pin_uv_auth_token(); + } + } + + /// Returns whether the user is verified. + pub fn get_user_verified_flag_value(&self) -> bool { + self.in_use && self.user_verified + } + + /// Consumes the user verification. + pub fn clear_user_verified_flag(&mut self) { + self.user_verified = false; + } + + /// Clears all permissions except Large Blob Write. + pub fn clear_pin_uv_auth_token_permissions_except_lbw(&mut self) { + self.permissions_set &= PinPermission::LargeBlobWrite as u8; + } + + /// Resets to the initial state. + pub fn stop_using_pin_uv_auth_token(&mut self) { + self.permissions_rp_id = None; + self.permissions_set = 0; + self.usage_timer = TimedPermission::waiting(); + self.user_verified = false; + self.in_use = false; + } +} + +#[cfg(test)] +mod test { + use super::*; + use enum_iterator::IntoEnumIterator; + + const CLOCK_FREQUENCY_HZ: usize = 32768; + const START_CLOCK_VALUE: ClockValue = ClockValue::new(0, CLOCK_FREQUENCY_HZ); + const SMALL_DURATION: Duration = Duration::from_ms(100); + + #[test] + fn test_observer() { + let mut token_state = PinUvAuthTokenState::new(); + let mut now = START_CLOCK_VALUE; + token_state.begin_using_pin_uv_auth_token(now); + assert!(token_state.is_in_use()); + now = now.wrapping_add(SMALL_DURATION); + token_state.pin_uv_auth_token_usage_timer_observer(now); + assert!(token_state.is_in_use()); + now = now.wrapping_add(INITIAL_USAGE_TIME_LIMIT); + token_state.pin_uv_auth_token_usage_timer_observer(now); + assert!(!token_state.is_in_use()); + } + + #[test] + fn test_stop() { + let mut token_state = PinUvAuthTokenState::new(); + token_state.begin_using_pin_uv_auth_token(START_CLOCK_VALUE); + assert!(token_state.is_in_use()); + token_state.stop_using_pin_uv_auth_token(); + assert!(!token_state.is_in_use()); + } + + #[test] + fn test_permissions() { + let mut token_state = PinUvAuthTokenState::new(); + token_state.set_permissions(0xFF); + for permission in PinPermission::into_enum_iter() { + assert_eq!(token_state.has_permission(permission), Ok(())); + } + token_state.clear_pin_uv_auth_token_permissions_except_lbw(); + assert_eq!( + token_state.has_permission(PinPermission::CredentialManagement), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + token_state.has_permission(PinPermission::LargeBlobWrite), + Ok(()) + ); + token_state.stop_using_pin_uv_auth_token(); + for permission in PinPermission::into_enum_iter() { + assert_eq!( + token_state.has_permission(permission), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + } + + #[test] + fn test_permissions_rp_id_none() { + let mut token_state = PinUvAuthTokenState::new(); + let example_hash = Sha256::hash(b"example.com"); + token_state.set_permissions_rp_id(None); + assert_eq!(token_state.has_no_permissions_rp_id(), Ok(())); + assert_eq!( + token_state.has_permissions_rp_id("example.com"), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + token_state.has_permissions_rp_id_hash(&example_hash), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_permissions_rp_id_some() { + let mut token_state = PinUvAuthTokenState::new(); + let example_hash = Sha256::hash(b"example.com"); + token_state.set_permissions_rp_id(Some(String::from("example.com"))); + + assert_eq!( + token_state.has_no_permissions_rp_id(), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!(token_state.has_permissions_rp_id("example.com"), Ok(())); + assert_eq!( + token_state.has_permissions_rp_id("another.example.com"), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + token_state.has_permissions_rp_id_hash(&example_hash), + Ok(()) + ); + assert_eq!( + token_state.has_permissions_rp_id_hash(&[0x1D; 32]), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + + token_state.stop_using_pin_uv_auth_token(); + assert_eq!( + token_state.has_permissions_rp_id("example.com"), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + token_state.has_permissions_rp_id_hash(&example_hash), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_user_verified_flag() { + let mut token_state = PinUvAuthTokenState::new(); + assert!(!token_state.get_user_verified_flag_value()); + token_state.begin_using_pin_uv_auth_token(START_CLOCK_VALUE); + assert!(token_state.get_user_verified_flag_value()); + token_state.clear_user_verified_flag(); + assert!(!token_state.get_user_verified_flag_value()); + token_state.begin_using_pin_uv_auth_token(START_CLOCK_VALUE); + assert!(token_state.get_user_verified_flag_value()); + token_state.stop_using_pin_uv_auth_token(); + assert!(!token_state.get_user_verified_flag_value()); + } +} diff --git a/src/main.rs b/src/main.rs index 2ca12a8..202a1ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -120,8 +120,8 @@ fn main() { } // These calls are making sure that even for long inactivity, wrapping clock values - // never randomly wink or grant user presence for U2F. - ctap_state.update_command_permission(now); + // don't cause problems with timers. + ctap_state.update_timeouts(now); ctap_hid.wink_permission = ctap_hid.wink_permission.check_expiration(now); if has_packet {