diff --git a/CHANGELOG.md b/CHANGELOG.md index 677d635..0d1539e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Implement the `largeBlobKey` extension and the `largeBlobs` command ([#38][]) - Fix error type for third invalid PIN entry ([#60][]) - Fix error type for cancelled user presence ([#61][]) -- Extract PIN protocol implementation into separate module ([#62][]) +- PIN protocol changes: + - Extract PIN protocol implementation into separate module ([#62][]) + - Implement PIN protocol 2 ([#63][]) + - Implement PIN token permissions ([#63][]) [#26]: https://github.com/solokeys/fido-authenticator/issues/26 [#28]: https://github.com/solokeys/fido-authenticator/issues/28 @@ -31,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#60]: https://github.com/Nitrokey/fido-authenticator/pull/60 [#61]: https://github.com/Nitrokey/fido-authenticator/pull/61 [#62]: https://github.com/Nitrokey/fido-authenticator/pull/62 +[#63]: https://github.com/Nitrokey/fido-authenticator/pull/63 ## [0.1.1] - 2022-08-22 - Fix bug that treated U2F payloads as APDU over APDU in NFC transport @conorpp diff --git a/Cargo.toml b/Cargo.toml index 1270ab7..daf1090 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ serde_cbor = { version = "0.11.0", default-features = false } serde-indexed = "0.1.0" sha2 = { version = "0.10", default-features = false } trussed = "0.1" +trussed-hkdf = { version = "0.1.0" } trussed-staging = { version = "0.1.0", default-features = false, optional = true } apdu-dispatch = { version = "0.1", optional = true } @@ -56,11 +57,13 @@ usbd-ctaphid = "0.1.0" features = ["dispatch"] [patch.crates-io] -ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "7d4ad69e64ad308944c012aef5b9cfd7654d9be8" } +ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "4846817d9cd44604121680a19d46f3264973a3ce" } ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } apdu-dispatch = { git = "https://github.com/trussed-dev/apdu-dispatch.git", rev = "915fc237103fcecc29d0f0b73391f19abf6576de" } -trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "b1781805a2e33615d2d00b8bec80c0b1f5870ca1" } -trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging", rev = "3b9594d93f89a5e760fe78fa5a96f125dfdcd470" } +littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "ebd27e49ca321089d01d8c9b169c4aeb58ceeeca" } serde-indexed = { git = "https://github.com/sosthene-nitrokey/serde-indexed.git", rev = "5005d23cb4ee8622e62188ea0f9466146f851f0d" } +trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "cff2e663841b6a68d3a8ce12647d57b2b6fbc36c" } +trussed-hkdf = { git = "https://github.com/Nitrokey/trussed-hkdf-backend.git", tag = "v0.1.0" } +trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging", rev = "3b9594d93f89a5e760fe78fa5a96f125dfdcd470" } trussed-usbip = { git = "https://github.com/Nitrokey/pc-usbip-runner.git", tag = "v0.0.1-nitrokey.1" } usbd-ctaphid = { git = "https://github.com/Nitrokey/usbd-ctaphid.git", tag = "v0.1.0-nitrokey.2" } diff --git a/src/ctap1.rs b/src/ctap1.rs index 4e5623e..be12d47 100644 --- a/src/ctap1.rs +++ b/src/ctap1.rs @@ -63,7 +63,7 @@ impl Authenticator for crate::Authenti let wrapped_key = syscall!(self .trussed - .wrap_key_chacha8poly1305(wrapping_key, private_key, &[])) + .wrap_key_chacha8poly1305(wrapping_key, private_key, &[], None)) .wrapped_key; // debug!("wrapped_key = {:?}", &wrapped_key); diff --git a/src/ctap2.rs b/src/ctap2.rs index 70d8cb3..62ba5a9 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -1,7 +1,7 @@ //! The `ctap_types::ctap2::Authenticator` implementation. use ctap_types::{ - ctap2::{self, Authenticator, VendorOperation}, + ctap2::{self, client_pin::Permissions, Authenticator, VendorOperation}, heapless::{String, Vec}, heapless_bytes::Bytes, sizes, Error, @@ -68,8 +68,10 @@ impl Authenticator for crate::Authenti .unwrap(); } - let mut pin_protocols = Vec::::new(); - pin_protocols.push(1).unwrap(); + let mut pin_protocols = Vec::::new(); + for pin_protocol in self.pin_protocols() { + pin_protocols.push(u8::from(*pin_protocol)).unwrap(); + } let options = ctap2::get_info::CtapOptions { ep: None, @@ -84,6 +86,7 @@ impl Authenticator for crate::Authenti }, credential_mgmt_preview: Some(true), large_blobs: Some(self.config.supports_large_blobs()), + pin_uv_auth_token: Some(true), ..Default::default() }; // options.rk = true; @@ -170,8 +173,10 @@ impl Authenticator for crate::Authenti let uv_performed = self.pin_prechecks( ¶meters.options, parameters.pin_auth.map(AsRef::as_ref), - ¶meters.pin_protocol, + parameters.pin_protocol, parameters.client_data_hash.as_ref(), + Permissions::MAKE_CREDENTIAL, + ¶meters.rp.id, )?; // 5. "persist credProtect value for this credential" @@ -332,11 +337,13 @@ impl Authenticator for crate::Authenti false => { // WrappedKey version let wrapping_key = self.state.persistent.key_wrapping_key(&mut self.trussed)?; - let wrapped_key = - syscall!(self - .trussed - .wrap_key_chacha8poly1305(wrapping_key, private_key, &[])) - .wrapped_key; + let wrapped_key = syscall!(self.trussed.wrap_key_chacha8poly1305( + wrapping_key, + private_key, + &[], + None + )) + .wrapped_key; // 32B key, 12B nonce, 16B tag + some info on algorithm (P256/Ed25519) // Turns out it's size 92 (enum serialization not optimized yet...) @@ -639,32 +646,20 @@ impl Authenticator for crate::Authenti debug_now!("CTAP2.PIN..."); // info_now!("{:?}", parameters); - // TODO: Handle pin protocol V2 - if parameters.pin_protocol != 1 { - return Err(Error::InvalidParameter); - } + let pin_protocol = self.parse_pin_protocol(parameters.pin_protocol)?; + let mut response = ctap2::client_pin::Response::default(); - Ok(match parameters.sub_command { + match parameters.sub_command { Subcommand::GetRetries => { debug_now!("CTAP2.Pin.GetRetries"); - ctap2::client_pin::Response { - key_agreement: None, - pin_token: None, - retries: Some(self.state.persistent.retries()), - } + response.retries = Some(self.state.persistent.retries()); } Subcommand::GetKeyAgreement => { debug_now!("CTAP2.Pin.GetKeyAgreement"); - let key_agreement = self.pin_protocol().key_agreement_key(); - - ctap2::client_pin::Response { - key_agreement: Some(key_agreement), - pin_token: None, - retries: None, - } + response.key_agreement = Some(self.pin_protocol(pin_protocol).key_agreement_key()); } Subcommand::SetPin => { @@ -695,14 +690,15 @@ impl Authenticator for crate::Authenti } // 3. generate shared secret - let shared_secret = self.pin_protocol().shared_secret(platform_kek)?; + let mut pin_protocol = self.pin_protocol(pin_protocol); + let shared_secret = pin_protocol.shared_secret(platform_kek)?; // TODO: there are moar early returns!! // - implement Drop? // - do garbage collection outside of this? // 4. verify pinAuth - shared_secret.verify_pin_auth(&mut self.trussed, new_pin_enc, pin_auth)?; + pin_protocol.verify_pin_auth(&shared_secret, new_pin_enc, pin_auth)?; // 5. decrypt and verify new PIN let new_pin = self.decrypt_pin_check_length(&shared_secret, new_pin_enc)?; @@ -714,12 +710,6 @@ impl Authenticator for crate::Authenti self.state .reset_retries(&mut self.trussed) .map_err(|_| Error::Other)?; - - ctap2::client_pin::Response { - key_agreement: None, - pin_token: None, - retries: None, - } } Subcommand::ChangePin => { @@ -755,7 +745,8 @@ impl Authenticator for crate::Authenti self.state.pin_blocked()?; // 3. generate shared secret - let shared_secret = self.pin_protocol().shared_secret(platform_kek)?; + let mut pin_protocol_impl = self.pin_protocol(pin_protocol); + let shared_secret = pin_protocol_impl.shared_secret(platform_kek)?; // 4. verify pinAuth let mut data = MediumData::new(); @@ -763,13 +754,17 @@ impl Authenticator for crate::Authenti .map_err(|_| Error::InvalidParameter)?; data.extend_from_slice(pin_hash_enc) .map_err(|_| Error::InvalidParameter)?; - shared_secret.verify_pin_auth(&mut self.trussed, &data, pin_auth)?; + pin_protocol_impl.verify_pin_auth(&shared_secret, &data, pin_auth)?; // 5. decrement retries self.state.decrement_retries(&mut self.trussed)?; // 6. decrypt pinHashEnc, compare with stored - self.decrypt_pin_hash_and_maybe_escalate(&shared_secret, pin_hash_enc)?; + self.decrypt_pin_hash_and_maybe_escalate( + pin_protocol, + &shared_secret, + pin_hash_enc, + )?; // 7. reset retries self.state.reset_retries(&mut self.trussed)?; @@ -782,75 +777,165 @@ impl Authenticator for crate::Authenti // 9. store hashed PIN self.hash_store_pin(&new_pin)?; - self.pin_protocol().reset_pin_token(); - - ctap2::client_pin::Response { - key_agreement: None, - pin_token: None, - retries: None, + for pin_protocol in self.pin_protocols() { + self.pin_protocol(*pin_protocol).reset_pin_token(); } } + // § 6.5.5.7.1 No 4 Subcommand::GetPinToken => { debug_now!("CTAP2.Pin.GetPinToken"); - // 1. check mandatory parameters - let platform_kek = match parameters.key_agreement.as_ref() { - Some(key) => key, - None => { - return Err(Error::MissingParameter); - } - }; - let pin_hash_enc = match parameters.pin_hash_enc.as_ref() { - Some(hash) => hash, - None => { - return Err(Error::MissingParameter); - } - }; + // 1. Check mandatory parameters + let key_agreement = parameters + .key_agreement + .as_ref() + .ok_or(Error::MissingParameter)?; + let pin_hash_enc = parameters + .pin_hash_enc + .as_ref() + .ok_or(Error::MissingParameter)?; - // 2. fail if no retries left + // 2. Check PIN protocol + let pin_protocol = self.parse_pin_protocol(parameters.pin_protocol)?; + + // 3. + 4. Check invalid parameters + if parameters.permissions.is_some() || parameters.rp_id.is_some() { + return Err(Error::InvalidParameter); + } + + // 5. Check PIN retries self.state.pin_blocked()?; - // 3. generate shared secret - let shared_secret = self.pin_protocol().shared_secret(platform_kek)?; + // 6. Obtain shared secret + let shared_secret = self + .pin_protocol(pin_protocol) + .shared_secret(key_agreement)?; - // 4. decrement retires + // 7. Request user consent using display -- skipped + + // 8. Decrement PIN retries self.state.decrement_retries(&mut self.trussed)?; - // 5. decrypt and verify pinHashEnc - self.decrypt_pin_hash_and_maybe_escalate(&shared_secret, pin_hash_enc)?; + // 9. Check PIN + self.decrypt_pin_hash_and_maybe_escalate( + pin_protocol, + &shared_secret, + pin_hash_enc, + )?; - // 6. reset retries + // 10. Reset PIN retries self.state.reset_retries(&mut self.trussed)?; - // 7. return encrypted pinToken - debug_now!("wrapping pin token"); - // info_now!("exists? {}", syscall!(self.trussed.exists(shared_secret)).exists); - let pin_token_enc = self - .pin_protocol() - .reset_and_encrypt_pin_token(&shared_secret)?; + // 11. Check forcePINChange -- skipped + + // 12. Reset all PIN tokens + for pin_protocol in self.pin_protocols() { + self.pin_protocol(*pin_protocol).reset_pin_token(); + } + + // 13. Call beginUsingPinUvAuthToken + let mut pin_protocol = self.pin_protocol(pin_protocol); + pin_protocol.begin_using_pin_token(false); + + // 14. Assign the default permissions + let mut permissions = Permissions::empty(); + permissions.insert(Permissions::MAKE_CREDENTIAL); + permissions.insert(Permissions::GET_ASSERTION); + pin_protocol.restrict_pin_token(permissions, None); + + // 15. Return PIN token + response.pin_token = Some(pin_protocol.encrypt_pin_token(&shared_secret)?); shared_secret.delete(&mut self.trussed); + } + + // § 6.5.5.7.2 No 4 + Subcommand::GetPinUvAuthTokenUsingPinWithPermissions => { + debug_now!("CTAP2.Pin.GetPinUvAuthTokenUsingPinWithPermissions"); - // ble... - if pin_token_enc.len() != 16 { - return Err(Error::Other); + // 1. Check mandatory parameters + let key_agreement = parameters + .key_agreement + .as_ref() + .ok_or(Error::MissingParameter)?; + let pin_hash_enc = parameters + .pin_hash_enc + .as_ref() + .ok_or(Error::MissingParameter)?; + let permissions = parameters.permissions.ok_or(Error::MissingParameter)?; + + // 2. Check PIN protocol + let pin_protocol = self.parse_pin_protocol(parameters.pin_protocol)?; + + // 3. Check that permissions are not empty + let permissions = Permissions::from_bits_truncate(permissions); + if permissions.is_empty() { + return Err(Error::InvalidParameter); + } + + // 4. Check that all requested permissions are supported + let mut unauthorized_permissions = Permissions::empty(); + unauthorized_permissions.insert(Permissions::BIO_ENROLLMENT); + if !self.config.supports_large_blobs() { + unauthorized_permissions.insert(Permissions::LARGE_BLOB_WRITE); + } + unauthorized_permissions.insert(Permissions::AUTHENTICATOR_CONFIGURATION); + if permissions.intersects(unauthorized_permissions) { + return Err(Error::UnauthorizedPermission); } - ctap2::client_pin::Response { - key_agreement: None, - pin_token: Some(pin_token_enc), - retries: None, + // 5. Check PIN retries + self.state.pin_blocked()?; + + // 6. Obtain shared secret + let shared_secret = self + .pin_protocol(pin_protocol) + .shared_secret(key_agreement)?; + + // 7. Request user consent using display -- skipped + + // 8. Decrement PIN retries + self.state.decrement_retries(&mut self.trussed)?; + + // 9. Check PIN + self.decrypt_pin_hash_and_maybe_escalate( + pin_protocol, + &shared_secret, + pin_hash_enc, + )?; + + // 10. Reset PIN retries + self.state.reset_retries(&mut self.trussed)?; + + // 11. Check forcePINChange -- skipped + + // 12. Reset all PIN tokens + for pin_protocol in self.pin_protocols() { + self.pin_protocol(*pin_protocol).reset_pin_token(); } + + // 13. Call beginUsingPinUvAuthToken + let mut pin_protocol = self.pin_protocol(pin_protocol); + pin_protocol.begin_using_pin_token(false); + + // 14. Assign the requested permissions + // 15. Assign the requested RP id + pin_protocol.restrict_pin_token(permissions, parameters.rp_id.clone()); + + // 16. Return PIN token + response.pin_token = Some(pin_protocol.encrypt_pin_token(&shared_secret)?); + + shared_secret.delete(&mut self.trussed); } - Subcommand::GetPinUvAuthTokenUsingUvWithPermissions - | Subcommand::GetUVRetries - | Subcommand::GetPinUvAuthTokenUsingPinWithPermissions => { + Subcommand::GetPinUvAuthTokenUsingUvWithPermissions | Subcommand::GetUVRetries => { // todo!("not implemented yet") return Err(Error::InvalidParameter); } - }) + } + + Ok(response) } #[inline(never)] @@ -862,7 +947,8 @@ impl Authenticator for crate::Authenti use ctap2::credential_management::Subcommand; // TODO: I see "failed pinauth" output, but then still continuation... - self.verify_pin_auth_using_token(parameters)?; + // TODO: determine rp_id + self.verify_pin_auth_using_token(parameters, None)?; let mut cred_mgmt = cm::CredentialManagement::new(self); let sub_parameters = ¶meters.sub_command_params; @@ -902,6 +988,9 @@ impl Authenticator for crate::Authenti .ok_or(Error::MissingParameter)?, ) } + + // 0x7 + Subcommand::UpdateUserInformation => Err(Error::InvalidParameter), } } @@ -929,8 +1018,10 @@ impl Authenticator for crate::Authenti let uv_performed = match self.pin_prechecks( ¶meters.options, parameters.pin_auth.map(AsRef::as_ref), - ¶meters.pin_protocol, + parameters.pin_protocol, parameters.client_data_hash.as_ref(), + Permissions::GET_ASSERTION, + ¶meters.rp_id, ) { Ok(b) => b, Err(Error::PinRequired) => { @@ -1034,9 +1125,25 @@ impl Authenticator for crate::Authenti // impl Authenticator for crate::Authenticator impl crate::Authenticator { - fn pin_protocol(&mut self) -> PinProtocol<'_, T> { + fn parse_pin_protocol(&self, version: impl TryInto) -> Result { + if let Ok(version) = version.try_into() { + for pin_protocol in self.pin_protocols() { + if u8::from(*pin_protocol) == version { + return Ok(*pin_protocol); + } + } + } + Err(Error::InvalidParameter) + } + + // This is the single source of truth for the supported PIN protocols. + fn pin_protocols(&self) -> &'static [PinProtocolVersion] { + &[PinProtocolVersion::V2, PinProtocolVersion::V1] + } + + fn pin_protocol(&mut self, pin_protocol: PinProtocolVersion) -> PinProtocol<'_, T> { let state = self.state.runtime.pin_protocol(&mut self.trussed); - PinProtocol::new(&mut self.trussed, state, PinProtocolVersion::V1) + PinProtocol::new(&mut self.trussed, state, pin_protocol) } #[inline(never)] @@ -1152,8 +1259,9 @@ impl crate::Authenticator { fn decrypt_pin_hash_and_maybe_escalate( &mut self, + pin_protocol: PinProtocolVersion, shared_secret: &SharedSecret, - pin_hash_enc: &Bytes<64>, + pin_hash_enc: &Bytes<80>, ) -> Result<()> { let pin_hash = shared_secret .decrypt(&mut self.trussed, pin_hash_enc) @@ -1168,7 +1276,7 @@ impl crate::Authenticator { if pin_hash != stored_pin_hash { // I) generate new KEK - self.pin_protocol().regenerate(); + self.pin_protocol(pin_protocol).regenerate(); self.state.pin_blocked()?; return Err(Error::PinInvalid); } @@ -1217,20 +1325,11 @@ impl crate::Authenticator { Ok(pin) } - // fn verify_pin(&mut self, pin_auth: &Bytes<16>, client_data_hash: &Bytes<32>) -> bool { - fn verify_pin(&mut self, pin_auth: &[u8; 16], data: &[u8]) -> Result<()> { - let pin_verified = self.pin_protocol().verify_pin_token(data, pin_auth); - if pin_verified { - Ok(()) - } else { - Err(Error::PinAuthInvalid) - } - } - // fn verify_pin_auth_using_token(&mut self, data: &[u8], pin_auth: &Bytes<16>) fn verify_pin_auth_using_token( &mut self, parameters: &ctap2::credential_management::Request, + rp_id: Option<&str>, ) -> Result<()> { // info_now!("CM params: {:?}", parameters); use ctap2::credential_management::Subcommand; @@ -1245,9 +1344,7 @@ impl crate::Authenticator { // .sub_command_params.as_ref().ok_or(Error::MissingParameter)? .pin_protocol .ok_or(Error::MissingParameter)?; - if pin_protocol != 1 { - return Err(Error::InvalidParameter); - } + let pin_protocol = self.parse_pin_protocol(pin_protocol)?; // check pinAuth let mut data: Bytes<{ sizes::MAX_CREDENTIAL_ID_LENGTH_PLUS_256 }> = @@ -1274,8 +1371,11 @@ impl crate::Authenticator { .as_ref() .ok_or(Error::MissingParameter)?; - if self.pin_protocol().verify_pin_token(&data[..len], pin_auth) { + let mut pin_protocol = self.pin_protocol(pin_protocol); + if let Ok(pin_token) = pin_protocol.verify_pin_token(&data[..len], pin_auth) { info_now!("passed pinauth"); + pin_token.require_permissions(Permissions::CREDENTIAL_MANAGEMENT)?; + pin_token.require_valid_for_rp_id(rp_id)?; Ok(()) } else { info_now!("failed pinauth!"); @@ -1295,6 +1395,9 @@ impl crate::Authenticator { // of already checked CredMgmt subcommands Subcommand::EnumerateRpsGetNextRp | Subcommand::EnumerateCredentialsGetNextCredential => Ok(()), + + // not implemented + Subcommand::UpdateUserInformation => Err(Error::InvalidParameter), } } @@ -1303,8 +1406,10 @@ impl crate::Authenticator { &mut self, options: &Option, pin_auth: Option<&[u8]>, - pin_protocol: &Option, + pin_protocol: Option, data: &[u8], + permissions: Permissions, + rp_id: &str, ) -> Result { // 1. pinAuth zero length -> wait for user touch, then // return PinNotSet if not set, PinInvalid if set @@ -1324,9 +1429,13 @@ impl crate::Authenticator { } // 2. check PIN protocol is 1 if pinAuth was sent - if pin_auth.is_some() && pin_protocol != &Some(1) { - return Err(Error::PinAuthInvalid); - } + let pin_protocol = if pin_auth.is_some() { + let pin_protocol = pin_protocol.ok_or(Error::MissingParameter)?; + let pin_protocol = self.parse_pin_protocol(pin_protocol)?; + Some(pin_protocol) + } else { + None + }; // 3. if no PIN is set (we have no other form of UV), // and platform sent `uv` or `pinAuth`, return InvalidOption @@ -1348,20 +1457,16 @@ impl crate::Authenticator { if self.state.persistent.pin_is_set() { // let mut uv_performed = false; if let Some(pin_auth) = pin_auth { - if pin_auth.len() != 16 { - return Err(Error::InvalidParameter); - } // seems a bit redundant to check here in light of 2. // I guess the CTAP spec writers aren't implementers :D - if let Some(1) = pin_protocol { + if let Some(pin_protocol) = pin_protocol { // 5. if pinAuth is present and pinProtocol = 1, verify // success --> set uv = 1 // error --> PinAuthInvalid - self.verify_pin( - // unwrap panic ruled out above - pin_auth.try_into().unwrap(), - data, - )?; + let mut pin_protocol = self.pin_protocol(pin_protocol); + let pin_token = pin_protocol.verify_pin_token(data, pin_auth)?; + pin_token.require_permissions(permissions)?; + pin_token.require_valid_for_rp_id(Some(rp_id))?; return Ok(true); } else { @@ -1410,11 +1515,11 @@ impl crate::Authenticator { credential_key: KeyId, ) -> Result> { if let Some(hmac_secret) = &extensions.hmac_secret { - if let Some(pin_protocol) = hmac_secret.pin_protocol { - if pin_protocol != 1 { - return Err(Error::InvalidParameter); - } - } + let pin_protocol = hmac_secret + .pin_protocol + .map(|i| self.parse_pin_protocol(i)) + .transpose()? + .unwrap_or(PinProtocolVersion::V1); // We derive credRandom as an hmac of the existing private key. // UV is used as input data since credRandom should depend UV @@ -1428,16 +1533,17 @@ impl crate::Authenticator { .key; // Verify the auth tag, which uses the same process as the pinAuth - let shared_secret = self - .pin_protocol() - .shared_secret(&hmac_secret.key_agreement)?; - shared_secret.verify_pin_auth( - &mut self.trussed, + let mut pin_protocol = self.pin_protocol(pin_protocol); + let shared_secret = pin_protocol.shared_secret(&hmac_secret.key_agreement)?; + pin_protocol.verify_pin_auth( + &shared_secret, &hmac_secret.salt_enc, &hmac_secret.salt_auth, )?; - if hmac_secret.salt_enc.len() != 32 && hmac_secret.salt_enc.len() != 64 { + if hmac_secret.salt_enc.len() != 32 + && (hmac_secret.salt_enc.len() != 64 || hmac_secret.salt_enc.len() == 80) + { debug_now!("invalid hmac-secret length"); return Err(Error::InvalidLength); } @@ -1839,6 +1945,7 @@ impl crate::Authenticator { if pin_uv_auth_protocol != 1 { return Err(Error::PinAuthInvalid); } + let pin_protocol = self.parse_pin_protocol(pin_uv_auth_protocol)?; // TODO: check pinUvAuthToken let pin_auth: [u8; 16] = pin_uv_auth_param .as_ref() @@ -1858,7 +1965,9 @@ impl crate::Authenticator { // SHA-256(data) auth_data.extend_from_slice(&Sha256::digest(data)).unwrap(); - self.verify_pin(&pin_auth, &auth_data)?; + let mut pin_protocol = self.pin_protocol(pin_protocol); + let pin_token = pin_protocol.verify_pin_token(&pin_auth, &auth_data)?; + pin_token.require_permissions(Permissions::LARGE_BLOB_WRITE)?; } // 6. Validate data length diff --git a/src/ctap2/pin.rs b/src/ctap2/pin.rs index 71fd52a..0ef9cd1 100644 --- a/src/ctap2/pin.rs +++ b/src/ctap2/pin.rs @@ -1,17 +1,83 @@ use crate::{cbor_serialize_message, TrussedRequirements}; -use ctap_types::{cose::EcdhEsHkdf256PublicKey, Error, Result}; +use core::mem; +use ctap_types::{cose::EcdhEsHkdf256PublicKey, ctap2::client_pin::Permissions, Error, Result}; use trussed::{ cbor_deserialize, - client::{Aes256Cbc, CryptoClient, HmacSha256, P256}, + client::{CryptoClient, HmacSha256, P256}, syscall, try_syscall, - types::{Bytes, KeyId, KeySerialization, Location, Mechanism, StorageAttributes}, + types::{ + Bytes, KeyId, KeySerialization, Location, Mechanism, Message, ShortData, StorageAttributes, + String, + }, }; +use trussed_hkdf::{KeyOrData, OkmId}; -const PIN_TOKEN_LENGTH: usize = 16; +// PIN protocol 1 supports 16 or 32 bytes, PIN protocol 2 requires 32 bytes. +const PIN_TOKEN_LENGTH: usize = 32; #[derive(Clone, Copy, Debug)] pub enum PinProtocolVersion { V1, + V2, +} + +impl From for u8 { + fn from(version: PinProtocolVersion) -> Self { + match version { + PinProtocolVersion::V1 => 1, + PinProtocolVersion::V2 => 2, + } + } +} + +#[derive(Debug)] +pub struct PinToken { + key_id: KeyId, + state: PinTokenState, +} + +impl PinToken { + fn generate(trussed: &mut T) -> PinToken { + let key_id = + syscall!(trussed.generate_secret_key(PIN_TOKEN_LENGTH, Location::Volatile)).key; + Self::new(key_id) + } + + fn new(key_id: KeyId) -> Self { + Self { + key_id, + state: Default::default(), + } + } + + fn delete(self, trussed: &mut T) { + syscall!(trussed.delete(self.key_id)); + } + + pub fn require_permissions(&self, permissions: Permissions) -> Result<()> { + if self.state.permissions.contains(permissions) { + Ok(()) + } else { + Err(Error::PinAuthInvalid) + } + } + + pub fn require_valid_for_rp_id(&self, rp_id: Option<&str>) -> Result<()> { + if self.state.rp_id.is_none() || self.state.rp_id.as_deref() == rp_id { + Ok(()) + } else { + Err(Error::PinAuthInvalid) + } + } +} + +#[derive(Debug, Default)] +struct PinTokenState { + permissions: Permissions, + rp_id: Option>, + is_user_present: bool, + is_user_verified: bool, + is_in_use: bool, } #[derive(Debug)] @@ -19,10 +85,12 @@ pub struct PinProtocolState { key_agreement_key: KeyId, // only used to delete the old shared secret from VFS when generating a new one. ideally, the // SharedSecret struct would clean up after itself. - shared_secret: Option, + shared_secret: Option, // for protocol version 1 - pin_token_v1: KeyId, + pin_token_v1: PinToken, + // for protocol version 2 + pin_token_v2: PinToken, } impl PinProtocolState { @@ -31,15 +99,17 @@ impl PinProtocolState { Self { key_agreement_key: generate_key_agreement_key(trussed), shared_secret: None, - pin_token_v1: generate_pin_token(trussed), + pin_token_v1: PinToken::generate(trussed), + pin_token_v2: PinToken::generate(trussed), } } pub fn reset(self, trussed: &mut T) { - syscall!(trussed.delete(self.pin_token_v1)); + self.pin_token_v1.delete(trussed); + self.pin_token_v2.delete(trussed); syscall!(trussed.delete(self.key_agreement_key)); if let Some(shared_secret) = self.shared_secret { - syscall!(trussed.delete(shared_secret)); + shared_secret.delete(trussed); } } } @@ -64,31 +134,48 @@ impl<'a, T: TrussedRequirements> PinProtocol<'a, T> { } } - fn pin_token(&self) -> KeyId { + fn pin_token(&self) -> &PinToken { match self.version { - PinProtocolVersion::V1 => self.state.pin_token_v1, + PinProtocolVersion::V1 => &self.state.pin_token_v1, + PinProtocolVersion::V2 => &self.state.pin_token_v2, } } - fn set_pin_token(&mut self, pin_token: KeyId) { + fn pin_token_mut(&mut self) -> &mut PinToken { match self.version { - PinProtocolVersion::V1 => self.state.pin_token_v1 = pin_token, + PinProtocolVersion::V1 => &mut self.state.pin_token_v1, + PinProtocolVersion::V2 => &mut self.state.pin_token_v2, } } pub fn regenerate(&mut self) { syscall!(self.trussed.delete(self.state.key_agreement_key)); if let Some(shared_secret) = self.state.shared_secret.take() { - syscall!(self.trussed.delete(shared_secret)); + shared_secret.delete(self.trussed); } self.state.key_agreement_key = generate_key_agreement_key(self.trussed); } // in spec: resetPinUvAuthToken() pub fn reset_pin_token(&mut self) { - syscall!(self.trussed.delete(self.pin_token())); - let pin_token = generate_pin_token(self.trussed); - self.set_pin_token(pin_token); + let new = PinToken::generate(self.trussed); + mem::replace(self.pin_token_mut(), new).delete(self.trussed); + } + + pub fn restrict_pin_token(&mut self, permissions: Permissions, rp_id: Option>) { + let pin_token = self.pin_token_mut(); + pin_token.state.permissions = permissions; + pin_token.state.rp_id = rp_id; + } + + // in spec: beginUsingPinUvAuthToken(userIsPresent) + pub fn begin_using_pin_token(&mut self, is_user_present: bool) { + let pin_token = self.pin_token_mut(); + pin_token.state.is_user_present = is_user_present; + pin_token.state.is_user_verified = true; + // TODO: set initial usage time limit + // TODO: start and observe usage timer + pin_token.state.is_in_use = true; } // in spec: getPublicKey @@ -109,28 +196,44 @@ impl<'a, T: TrussedRequirements> PinProtocol<'a, T> { cose_key } - // in spec: verify(pinUvAuthToken, ...) #[must_use] - pub fn verify_pin_token(&mut self, data: &[u8], signature: &[u8]) -> bool { - // TODO: check if pin token is in use - verify(self.trussed, self.pin_token(), data, signature) + fn verify(&mut self, key: KeyId, data: &[u8], signature: &[u8]) -> bool { + let actual_signature = syscall!(self.trussed.sign_hmacsha256(key, data)).signature; + let expected_signature = match self.version { + PinProtocolVersion::V1 => &actual_signature[..16], + PinProtocolVersion::V2 => &actual_signature, + }; + expected_signature == signature + } + + // in spec: verify(pinUvAuthToken, ...) + pub fn verify_pin_token(&mut self, data: &[u8], signature: &[u8]) -> Result<&PinToken> { + let pin_token = self.pin_token(); + if pin_token.state.is_in_use && self.verify(pin_token.key_id, data, signature) { + Ok(self.pin_token()) + } else { + Err(Error::PinAuthInvalid) + } } - // in spec: resetPinUvAuthToken() + encrypt(..., pinUvAuthToken) - pub fn reset_and_encrypt_pin_token( + // in spec: verify(shared secret, ...) + pub fn verify_pin_auth( &mut self, shared_secret: &SharedSecret, - ) -> Result> { - self.reset_pin_token(); - self.encrypt_pin_token(shared_secret) + data: &[u8], + pin_auth: &[u8], + ) -> Result<()> { + let key_id = shared_secret.hmac_key_id(); + if self.verify(key_id, data, pin_auth) { + Ok(()) + } else { + Err(Error::PinAuthInvalid) + } } // in spec: encrypt(..., pinUvAuthToken) - fn encrypt_pin_token(&mut self, shared_secret: &SharedSecret) -> Result> { - let token = syscall!(self - .trussed - .wrap_key_aes256cbc(shared_secret.key_id, self.pin_token())) - .wrapped_key; + pub fn encrypt_pin_token(&mut self, shared_secret: &SharedSecret) -> Result> { + let token = shared_secret.wrap(self.trussed, self.pin_token().key_id); Bytes::from_slice(&token).map_err(|_| Error::Other) } @@ -162,78 +265,167 @@ impl<'a, T: TrussedRequirements> PinProtocol<'a, T> { syscall!(self.trussed.delete(peer_key)); let pre_shared_secret = result.ok()?.shared_secret; - if let Some(shared_secret) = self.state.shared_secret { - syscall!(self.trussed.delete(shared_secret)); + if let Some(shared_secret) = self.state.shared_secret.take() { + shared_secret.delete(self.trussed); } let shared_secret = self.kdf(pre_shared_secret); - self.state.shared_secret = Some(shared_secret); syscall!(self.trussed.delete(pre_shared_secret)); - Some(SharedSecret { - key_id: shared_secret, - }) + let shared_secret = shared_secret?; + self.state.shared_secret = Some(shared_secret.clone()); + Some(shared_secret) + } + + fn kdf(&mut self, input: KeyId) -> Option { + match self.version { + PinProtocolVersion::V1 => self.kdf_v1(input), + PinProtocolVersion::V2 => self.kdf_v2(input), + } } - fn kdf(&mut self, input: KeyId) -> KeyId { - syscall!(self.trussed.derive_key( + // PIN protocol 1: derive a single key using SHA-256 + fn kdf_v1(&mut self, input: KeyId) -> Option { + let key_id = syscall!(self.trussed.derive_key( Mechanism::Sha256, input, None, StorageAttributes::new().set_persistence(Location::Volatile) )) - .key + .key; + Some(SharedSecret::V1 { key_id }) + } + + // PIN protocol 2: derive two keys using HKDF-SHA-256 + // In the spec, the keys are concatenated and the relevant part is selected during the key + // operations. For simplicity, we store two separate keys instead. + fn kdf_v2(&mut self, input: KeyId) -> Option { + fn hkdf(trussed: &mut T, okm: OkmId, info: &[u8]) -> Option { + let info = Message::from_slice(info).ok()?; + try_syscall!(trussed.hkdf_expand(okm, info, 32, Location::Volatile)) + .ok() + .map(|reply| reply.key) + } + + // salt: 0x00 * 32 => None + let okm = try_syscall!(self.trussed.hkdf_extract( + KeyOrData::Key(input), + None, + Location::Volatile + )) + .ok()? + .okm; + let hmac_key_id = hkdf(self.trussed, okm, b"CTAP2 HMAC key"); + let aes_key_id = hkdf(self.trussed, okm, b"CTAP2 AES key"); + + syscall!(self.trussed.delete(okm.0)); + + Some(SharedSecret::V2 { + hmac_key_id: hmac_key_id?, + aes_key_id: aes_key_id?, + }) } } -pub struct SharedSecret { - key_id: KeyId, +#[derive(Clone, Debug)] +pub enum SharedSecret { + V1 { + key_id: KeyId, + }, + V2 { + hmac_key_id: KeyId, + aes_key_id: KeyId, + }, } impl SharedSecret { - pub fn verify_pin_auth( - &self, - trussed: &mut T, - data: &[u8], - pin_auth: &Bytes<16>, - ) -> Result<()> { - if verify(trussed, self.key_id, data, pin_auth) { - Ok(()) - } else { - Err(Error::PinAuthInvalid) + fn aes_key_id(&self) -> KeyId { + match self { + Self::V1 { key_id } => *key_id, + Self::V2 { aes_key_id, .. } => *aes_key_id, + } + } + + fn hmac_key_id(&self) -> KeyId { + match self { + Self::V1 { key_id } => *key_id, + Self::V2 { hmac_key_id, .. } => *hmac_key_id, + } + } + + fn generate_iv(&self, trussed: &mut T) -> ShortData { + match self { + Self::V1 { .. } => ShortData::from_slice(&[0; 16]).unwrap(), + Self::V2 { .. } => syscall!(trussed.random_bytes(16)) + .bytes + .try_convert_into() + .unwrap(), } } #[must_use] pub fn encrypt(&self, trussed: &mut T, data: &[u8]) -> Bytes<1024> { - syscall!(trussed.encrypt(Mechanism::Aes256Cbc, self.key_id, data, b"", None)).ciphertext + let key_id = self.aes_key_id(); + let iv = self.generate_iv(trussed); + let mut ciphertext = + syscall!(trussed.encrypt(Mechanism::Aes256Cbc, key_id, data, &[], Some(iv.clone()))) + .ciphertext; + if matches!(self, Self::V2 { .. }) { + ciphertext.insert_slice_at(&iv, 0).unwrap(); + } + ciphertext } #[must_use] - pub fn decrypt(&self, trussed: &mut T, data: &[u8]) -> Option> { - decrypt(trussed, self.key_id, data) + fn wrap(&self, trussed: &mut T, key: KeyId) -> Bytes<1024> { + let wrapping_key = self.aes_key_id(); + let iv = self.generate_iv(trussed); + let mut wrapped_key = syscall!(trussed.wrap_key( + Mechanism::Aes256Cbc, + wrapping_key, + key, + &[], + Some(iv.clone()) + )) + .wrapped_key; + if matches!(self, Self::V2 { .. }) { + wrapped_key.insert_slice_at(&iv, 0).unwrap(); + } + wrapped_key } - pub fn delete(self, trussed: &mut T) { - syscall!(trussed.delete(self.key_id)); + #[must_use] + pub fn decrypt(&self, trussed: &mut T, data: &[u8]) -> Option> { + let key_id = self.aes_key_id(); + let (iv, data) = match self { + Self::V1 { .. } => (Default::default(), data), + Self::V2 { .. } => { + if data.len() < 16 { + return None; + } + data.split_at(16) + } + }; + try_syscall!(trussed.decrypt(Mechanism::Aes256Cbc, key_id, data, iv, b"", b"")) + .ok() + .and_then(|response| response.plaintext) } -} - -#[must_use] -fn verify(trussed: &mut T, key: KeyId, data: &[u8], signature: &[u8]) -> bool { - let actual_signature = syscall!(trussed.sign_hmacsha256(key, data)).signature; - &actual_signature[..16] == signature -} -#[must_use] -fn decrypt(trussed: &mut T, key: KeyId, data: &[u8]) -> Option> { - try_syscall!(trussed.decrypt_aes256cbc(key, data)) - .ok() - .and_then(|response| response.plaintext) -} - -fn generate_pin_token(trussed: &mut T) -> KeyId { - syscall!(trussed.generate_secret_key(PIN_TOKEN_LENGTH, Location::Volatile)).key + pub fn delete(self, trussed: &mut T) { + match self { + Self::V1 { key_id } => { + syscall!(trussed.delete(key_id)); + } + Self::V2 { + hmac_key_id, + aes_key_id, + } => { + for key_id in [hmac_key_id, aes_key_id] { + syscall!(trussed.delete(key_id)); + } + } + } + } } fn generate_key_agreement_key(trussed: &mut T) -> KeyId { diff --git a/src/lib.rs b/src/lib.rs index 8f46acb..2351556 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,7 @@ generate_macros!(); use core::time::Duration; use trussed::{client, syscall, types::Message, Client as TrussedClient}; +use trussed_hkdf::HkdfClient; use ctap_types::heapless_bytes::Bytes; @@ -55,6 +56,7 @@ pub trait TrussedRequirements: + client::Sha256 + client::HmacSha256 + client::Ed255 // + client::Totp + + HkdfClient + ExtensionRequirements { } @@ -67,6 +69,7 @@ impl TrussedRequirements for T where + client::Sha256 + client::HmacSha256 + client::Ed255 // + client::Totp + + HkdfClient + ExtensionRequirements { }