diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 1b4b96a..f2f2537 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -13,14 +13,17 @@ // limitations under the License. use super::data_formats::{ - extract_array, extract_byte_string, extract_map, extract_text_string, extract_unsigned, - ok_or_missing, ClientPinSubCommand, CoseKey, GetAssertionExtensions, GetAssertionOptions, - MakeCredentialExtensions, MakeCredentialOptions, PublicKeyCredentialDescriptor, - PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, + extract_array, extract_bool, extract_byte_string, extract_map, extract_text_string, + extract_unsigned, ok_or_missing, ClientPinSubCommand, CoseKey, GetAssertionExtensions, + GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, + PublicKeyCredentialUserEntity, }; +use super::key_material; use super::status_code::Ctap2StatusCode; use alloc::string::String; use alloc::vec::Vec; +use arrayref::array_ref; use cbor::destructure_cbor_map; use core::convert::TryFrom; @@ -41,6 +44,8 @@ pub enum Command { #[cfg(feature = "with_ctap2_1")] AuthenticatorSelection, // TODO(kaczmarczyck) implement FIDO 2.1 commands (see below consts) + // Vendor specific commands + AuthenticatorVendorConfigure(AuthenticatorVendorConfigureParameters), } impl From for Ctap2StatusCode { @@ -63,7 +68,8 @@ impl Command { const AUTHENTICATOR_CREDENTIAL_MANAGEMENT: u8 = 0xA0; const AUTHENTICATOR_SELECTION: u8 = 0xB0; const AUTHENTICATOR_CONFIG: u8 = 0xC0; - const AUTHENTICATOR_VENDOR_FIRST: u8 = 0x40; + const AUTHENTICATOR_VENDOR_CONFIGURE: u8 = 0x40; + const AUTHENTICATOR_VENDOR_FIRST_UNUSED: u8 = 0x41; const AUTHENTICATOR_VENDOR_LAST: u8 = 0xBF; pub fn deserialize(bytes: &[u8]) -> Result { @@ -109,6 +115,12 @@ impl Command { // Parameters are ignored. Ok(Command::AuthenticatorSelection) } + Command::AUTHENTICATOR_VENDOR_CONFIGURE => { + let decoded_cbor = cbor::read(&bytes[1..])?; + Ok(Command::AuthenticatorVendorConfigure( + AuthenticatorVendorConfigureParameters::try_from(decoded_cbor)?, + )) + } _ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND), } } @@ -372,6 +384,62 @@ impl TryFrom for AuthenticatorClientPinParameters { } } +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +pub struct AuthenticatorAttestationMaterial { + pub certificate: Vec, + pub private_key: [u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH], +} + +impl TryFrom for AuthenticatorAttestationMaterial { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + destructure_cbor_map! { + let { + 1 => certificate, + 2 => private_key, + } = extract_map(cbor_value)?; + } + let certificate = certificate.map(extract_byte_string).transpose()?.unwrap(); + let private_key = private_key.map(extract_byte_string).transpose()?.unwrap(); + if private_key.len() != key_material::ATTESTATION_PRIVATE_KEY_LENGTH { + return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR); + } + let private_key = array_ref!(private_key, 0, key_material::ATTESTATION_PRIVATE_KEY_LENGTH); + Ok(AuthenticatorAttestationMaterial { + certificate, + private_key: *private_key, + }) + } +} + +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +pub struct AuthenticatorVendorConfigureParameters { + pub lockdown: bool, + pub attestation_material: Option, +} + +impl TryFrom for AuthenticatorVendorConfigureParameters { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + destructure_cbor_map! { + let { + 1 => lockdown, + 2 => attestation_material, + } = extract_map(cbor_value)?; + } + let lockdown = lockdown.map_or(Ok(false), extract_bool)?; + let attestation_material = attestation_material + .map(AuthenticatorAttestationMaterial::try_from) + .transpose()?; + Ok(AuthenticatorVendorConfigureParameters { + lockdown, + attestation_material, + }) + } +} + #[cfg(test)] mod test { use super::super::data_formats::{ diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 4c5687a..50e0bbf 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -29,7 +29,7 @@ mod timed_permission; use self::command::MAX_CREDENTIAL_COUNT_IN_LIST; use self::command::{ AuthenticatorClientPinParameters, AuthenticatorGetAssertionParameters, - AuthenticatorMakeCredentialParameters, Command, + AuthenticatorMakeCredentialParameters, AuthenticatorVendorConfigureParameters, Command, }; #[cfg(feature = "with_ctap2_1")] use self::data_formats::AuthenticatorTransport; @@ -44,7 +44,7 @@ use self::pin_protocol_v1::PinPermission; use self::pin_protocol_v1::PinProtocolV1; use self::response::{ AuthenticatorGetAssertionResponse, AuthenticatorGetInfoResponse, - AuthenticatorMakeCredentialResponse, ResponseData, + AuthenticatorMakeCredentialResponse, AuthenticatorVendorResponse, ResponseData, }; use self::status_code::Ctap2StatusCode; use self::storage::PersistentStore; @@ -358,6 +358,10 @@ where #[cfg(feature = "with_ctap2_1")] Command::AuthenticatorSelection => self.process_selection(cid), // TODO(kaczmarczyck) implement FIDO 2.1 commands + // Vendor specific commands + Command::AuthenticatorVendorConfigure(params) => { + self.process_vendor_configure(params, cid) + } }; #[cfg(feature = "debug_ctap")] writeln!(&mut Console::new(), "Sending response: {:#?}", response).unwrap(); @@ -919,6 +923,63 @@ where Ok(ResponseData::AuthenticatorSelection) } + fn process_vendor_configure( + &mut self, + params: AuthenticatorVendorConfigureParameters, + cid: ChannelID, + ) -> Result { + (self.check_user_presence)(cid)?; + + // Sanity checks + let has_priv_key = self.persistent_store.attestation_private_key()?.is_some(); + let has_cert = self.persistent_store.attestation_certificate()?.is_some(); + + if params.attestation_material.is_some() { + let data = params.attestation_material.unwrap(); + if !has_cert { + self.persistent_store + .set_attestation_certificate(&data.certificate)?; + } + if !has_priv_key { + self.persistent_store + .set_attestation_private_key(&data.private_key)?; + } + }; + let has_priv_key = self.persistent_store.attestation_private_key()?.is_some(); + let has_cert = self.persistent_store.attestation_certificate()?.is_some(); + if params.lockdown { + // To avoid bricking the authenticator, we only allow lockdown + // to happen if both values are programmed or if both U2F/CTAP1 and + // batch attestation are disabled. + #[cfg(feature = "with_ctap1")] + let need_certificate = true; + #[cfg(not(feature = "with_ctap1"))] + let need_certificate = USE_BATCH_ATTESTATION; + + if (need_certificate && !(has_priv_key && has_cert)) + || libtock_drivers::crp::protect().is_err() + { + Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR) + } else { + Ok(ResponseData::AuthenticatorVendor( + AuthenticatorVendorResponse { + cert_programmed: has_cert, + pkey_programmed: has_priv_key, + lockdown_enabled: true, + }, + )) + } + } else { + Ok(ResponseData::AuthenticatorVendor( + AuthenticatorVendorResponse { + cert_programmed: has_cert, + pkey_programmed: has_priv_key, + lockdown_enabled: false, + }, + )) + } + } + pub fn generate_auth_data( &self, rp_id_hash: &[u8], @@ -941,6 +1002,7 @@ where #[cfg(test)] mod test { + use super::command::AuthenticatorAttestationMaterial; use super::data_formats::{ CoseKey, GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, @@ -2052,4 +2114,128 @@ mod test { last_counter = next_counter; } } + + #[test] + fn test_vendor_configure() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + // Nothing should be configured at the beginning + let response = ctap_state.process_vendor_configure( + AuthenticatorVendorConfigureParameters { + lockdown: false, + attestation_material: None, + }, + DUMMY_CHANNEL_ID, + ); + assert_eq!( + response, + Ok(ResponseData::AuthenticatorVendor( + AuthenticatorVendorResponse { + cert_programmed: false, + pkey_programmed: false, + lockdown_enabled: false + } + )) + ); + + // Inject dummy values + let dummy_key = [0x41u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; + let dummy_cert = [0xddu8; 20]; + let response = ctap_state.process_vendor_configure( + AuthenticatorVendorConfigureParameters { + lockdown: false, + attestation_material: Some(AuthenticatorAttestationMaterial { + certificate: dummy_cert.to_vec(), + private_key: dummy_key, + }), + }, + DUMMY_CHANNEL_ID, + ); + assert_eq!( + response, + Ok(ResponseData::AuthenticatorVendor( + AuthenticatorVendorResponse { + cert_programmed: true, + pkey_programmed: true, + lockdown_enabled: false + } + )) + ); + assert_eq!( + ctap_state + .persistent_store + .attestation_certificate() + .unwrap() + .unwrap(), + dummy_cert + ); + assert_eq!( + ctap_state + .persistent_store + .attestation_private_key() + .unwrap() + .unwrap(), + dummy_key + ); + + // Try to inject other dummy values and check that intial values are retained. + let other_dummy_key = [0x44u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; + let response = ctap_state.process_vendor_configure( + AuthenticatorVendorConfigureParameters { + lockdown: false, + attestation_material: Some(AuthenticatorAttestationMaterial { + certificate: dummy_cert.to_vec(), + private_key: other_dummy_key, + }), + }, + DUMMY_CHANNEL_ID, + ); + assert_eq!( + response, + Ok(ResponseData::AuthenticatorVendor( + AuthenticatorVendorResponse { + cert_programmed: true, + pkey_programmed: true, + lockdown_enabled: false + } + )) + ); + assert_eq!( + ctap_state + .persistent_store + .attestation_certificate() + .unwrap() + .unwrap(), + dummy_cert + ); + assert_eq!( + ctap_state + .persistent_store + .attestation_private_key() + .unwrap() + .unwrap(), + dummy_key + ); + + // Now try to lock the device + let response = ctap_state.process_vendor_configure( + AuthenticatorVendorConfigureParameters { + lockdown: true, + attestation_material: None, + }, + DUMMY_CHANNEL_ID, + ); + assert_eq!( + response, + Ok(ResponseData::AuthenticatorVendor( + AuthenticatorVendorResponse { + cert_programmed: true, + pkey_programmed: true, + lockdown_enabled: true + } + )) + ); + } } diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 47e1d54..674a1f5 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -34,6 +34,7 @@ pub enum ResponseData { AuthenticatorReset, #[cfg(feature = "with_ctap2_1")] AuthenticatorSelection, + AuthenticatorVendor(AuthenticatorVendorResponse), } impl From for Option { @@ -48,6 +49,7 @@ impl From for Option { ResponseData::AuthenticatorReset => None, #[cfg(feature = "with_ctap2_1")] ResponseData::AuthenticatorSelection => None, + ResponseData::AuthenticatorVendor(data) => Some(data.into()), } } } @@ -231,6 +233,30 @@ impl From for cbor::Value { } } +#[cfg_attr(test, derive(PartialEq))] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +pub struct AuthenticatorVendorResponse { + pub cert_programmed: bool, + pub pkey_programmed: bool, + pub lockdown_enabled: bool, +} + +impl From for cbor::Value { + fn from(vendor_response: AuthenticatorVendorResponse) -> Self { + let AuthenticatorVendorResponse { + cert_programmed, + pkey_programmed, + lockdown_enabled, + } = vendor_response; + + cbor_map_options! { + 1 => cert_programmed, + 2 => pkey_programmed, + 3 => lockdown_enabled, + } + } +} + #[cfg(test)] mod test { use super::super::data_formats::PackedAttestationStatement;