From 6800f4928bc49446c1d640d74efce5f2a5ce0dc2 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 20 Nov 2023 13:33:42 +0100 Subject: [PATCH 1/7] Update ctap-types This patch updates the ctap-types dependency to pull in support for the largeBlobKey extension and the largeBlobs command. --- Cargo.toml | 2 +- src/ctap2.rs | 9 +++++++-- src/dispatch.rs | 2 ++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9e82879..96b6516 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,7 +45,7 @@ trussed = { version = "0.1", features = ["virt"] } features = ["dispatch"] [patch.crates-io] -ctap-types = { git = "https://github.com/nitrokey/ctap-types.git", tag = "v0.1.2-nitrokey.4" } +ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "785bcc52720ce2e2054ae32034a2a24c500e1043" } 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 = "51e68500d7601d04f884f5e95567d14b9018a6cb" } diff --git a/src/ctap2.rs b/src/ctap2.rs index 0967846..8c93ae7 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -50,7 +50,7 @@ impl Authenticator for crate::Authenti .push(String::from_str("FIDO_2_1_PRE").unwrap()) .unwrap(); - let mut extensions = Vec::, 4>::new(); + let mut extensions = Vec::, 4>::new(); // extensions.push(String::from_str("credProtect").unwrap()).unwrap(); extensions .push(String::from_str("credProtect").unwrap()) @@ -444,6 +444,7 @@ impl Authenticator for crate::Authenti Some(ctap2::make_credential::Extensions { cred_protect: parameters.extensions.as_ref().unwrap().cred_protect, hmac_secret: parameters.extensions.as_ref().unwrap().hmac_secret, + large_blob_key: None, }) } else { None @@ -551,6 +552,8 @@ impl Authenticator for crate::Authenti fmt, auth_data: serialized_auth_data, att_stmt, + ep_att: None, + large_blob_key: None, }; Ok(attestation_object) @@ -1526,7 +1529,7 @@ impl crate::Authenticator { rp_id_hash, flags: { - let mut flags = Flags::EMPTY; + let mut flags = Flags::empty(); if data.up_performed { flags |= Flags::USER_PRESENCE; } @@ -1581,6 +1584,8 @@ impl crate::Authenticator { signature, user: None, number_of_credentials: num_credentials, + user_selected: None, + large_blob_key: None, }; // User with empty IDs are ignored for compatibility diff --git a/src/dispatch.rs b/src/dispatch.rs index 7aaf841..50849e6 100644 --- a/src/dispatch.rs +++ b/src/dispatch.rs @@ -211,6 +211,7 @@ fn request_operation(request: &ctap2::Request) -> ctap2::Operation { ctap2::Request::Reset => ctap2::Operation::Reset, ctap2::Request::CredentialManagement(_) => ctap2::Operation::CredentialManagement, ctap2::Request::Selection => ctap2::Operation::Selection, + ctap2::Request::LargeBlobs(_) => ctap2::Operation::LargeBlobs, ctap2::Request::Vendor(operation) => ctap2::Operation::Vendor(*operation), } } @@ -226,6 +227,7 @@ fn response_operation(request: &ctap2::Response) -> Option { ctap2::Response::Reset => Some(ctap2::Operation::Reset), ctap2::Response::CredentialManagement(_) => Some(ctap2::Operation::CredentialManagement), ctap2::Response::Selection => Some(ctap2::Operation::Selection), + ctap2::Response::LargeBlobs(_) => Some(ctap2::Operation::LargeBlobs), ctap2::Response::Vendor => None, } } From 71d14ff073a592c9e678f3b2c353d03164e881cf Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 20 Nov 2023 13:45:55 +0100 Subject: [PATCH 2/7] Add largeBlobKey support to get_info This patch adds support for the largeBlobKey extension to the get_info command. It also adds a config entry to be able to enable or disable the extension. --- src/ctap2.rs | 6 ++++++ src/lib.rs | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/src/ctap2.rs b/src/ctap2.rs index 8c93ae7..1a66bc1 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -58,6 +58,11 @@ impl Authenticator for crate::Authenti extensions .push(String::from_str("hmac-secret").unwrap()) .unwrap(); + if self.config.supports_large_blobs() { + extensions + .push(String::from_str("largeBlobKey").unwrap()) + .unwrap(); + } let mut pin_protocols = Vec::::new(); pin_protocols.push(1).unwrap(); @@ -74,6 +79,7 @@ impl Authenticator for crate::Authenti false => Some(false), }, credential_mgmt_preview: Some(true), + large_blobs: Some(self.config.supports_large_blobs()), ..Default::default() }; // options.rk = true; diff --git a/src/lib.rs b/src/lib.rs index 5cd444a..abff732 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,6 +78,14 @@ pub struct Config { pub skip_up_timeout: Option, /// The maximum number of resident credentials. pub max_resident_credential_count: Option, + /// Enable the largeBlobKey extension and the largeBlobs command. + pub large_blobs: bool, +} + +impl Config { + pub fn supports_large_blobs(&self) -> bool { + self.large_blobs + } } // impl Default for Config { From c43da04a260f40b31302b413706eca2fe505b5bf Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 20 Nov 2023 14:02:05 +0100 Subject: [PATCH 3/7] Add largeBlobKey support to make_credential This patch adds support for the largeBlobKey extension to make_credential. This means that we have to generate a 32-bit key and store it together with the credential if requested by the platform. --- src/credential.rs | 4 ++++ src/ctap2.rs | 25 ++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/credential.rs b/src/credential.rs index 32cd94a..b67ea62 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -226,6 +226,8 @@ pub struct CredentialData { pub hmac_secret: Option, #[serde(skip_serializing_if = "Option::is_none")] pub cred_protect: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub large_blob_key: Option>, // TODO: add `sig_counter: Option`, // and grant RKs a per-credential sig-counter. @@ -327,6 +329,7 @@ impl FullCredential { timestamp: u32, hmac_secret: Option, cred_protect: Option, + large_blob_key: Option>, nonce: [u8; 12], ) -> Self { info!("credential for algorithm {}", algorithm); @@ -341,6 +344,7 @@ impl FullCredential { hmac_secret, cred_protect, + large_blob_key, use_short_id: Some(true), }; diff --git a/src/ctap2.rs b/src/ctap2.rs index 1a66bc1..502e268 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -234,6 +234,7 @@ impl Authenticator for crate::Authenti let mut hmac_secret_requested = None; // let mut cred_protect_requested = CredentialProtectionPolicy::Optional; let mut cred_protect_requested = None; + let mut large_blob_key_requested = false; if let Some(extensions) = ¶meters.extensions { hmac_secret_requested = extensions.hmac_secret; @@ -241,6 +242,21 @@ impl Authenticator for crate::Authenti cred_protect_requested = Some(credential::CredentialProtectionPolicy::try_from(*policy)?); } + + if self.config.supports_large_blobs() { + if let Some(large_blob_key) = extensions.large_blob_key { + if large_blob_key { + if !rk_requested { + // the largeBlobKey extension is only available for resident keys + return Err(Error::InvalidOption); + } + large_blob_key_requested = true; + } else { + // large_blob_key must be Some(true) or omitted, Some(false) is invalid + return Err(Error::InvalidOption); + } + } + } } // debug_now!("hmac-secret = {:?}, credProtect = {:?}", hmac_secret_requested, cred_protect_requested); @@ -343,6 +359,12 @@ impl Authenticator for crate::Authenti // store it. // TODO: overwrite, error handling with KeyStoreFull + let large_blob_key = if large_blob_key_requested { + Some(Bytes::from_slice(&syscall!(self.trussed.random_bytes(32)).bytes).unwrap()) + } else { + None + }; + let credential = FullCredential::new( credential::CtapVersion::Fido21Pre, ¶meters.rp, @@ -352,6 +374,7 @@ impl Authenticator for crate::Authenti self.state.persistent.timestamp(&mut self.trussed)?, hmac_secret_requested, cred_protect_requested, + large_blob_key.clone(), nonce, ); @@ -559,7 +582,7 @@ impl Authenticator for crate::Authenti auth_data: serialized_auth_data, att_stmt, ep_att: None, - large_blob_key: None, + large_blob_key, }; Ok(attestation_object) From 48d66c08555885f163458308cf0dc9c031c0b579 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 20 Nov 2023 14:09:33 +0100 Subject: [PATCH 4/7] Add largeBlobKey support to get_assertion This patch adds support for the largeBlobKey extension to get_assertion. This means that we have to return the key stored together with the credential if it is present and requested by the platform. --- src/ctap2.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/ctap2.rs b/src/ctap2.rs index 502e268..a54163e 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -1535,7 +1535,15 @@ impl crate::Authenticator { }; // 8. process any extensions present + let mut large_blob_key_requested = false; let extensions_output = if let Some(extensions) = &data.extensions { + if self.config.supports_large_blobs() { + if extensions.large_blob_key == Some(false) { + // large_blob_key must be Some(true) or omitted + return Err(Error::InvalidOption); + } + large_blob_key_requested = extensions.large_blob_key == Some(true); + } self.process_assertion_extensions(&data, extensions, &credential, key)? } else { None @@ -1632,6 +1640,10 @@ impl crate::Authenticator { } response.user = Some(user); } + + if large_blob_key_requested { + response.large_blob_key = credential.large_blob_key.clone(); + } } } From f3128f8ef6c57fe505bf371a8976dd6d9240483a Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 20 Nov 2023 19:58:28 +0100 Subject: [PATCH 5/7] Implement largeBlobs command This patch implements the largeBlobs command for reading and writing the large-blob array. Currently, the maximum size of the total array with metadata is 1024 bytes because it has to fit in a Trussed message. The storage location can be configured by the runner. --- CHANGELOG.md | 2 + src/ctap2.rs | 171 ++++++++++++++++++++++++++++++++++ src/ctap2/large_blobs.rs | 193 +++++++++++++++++++++++++++++++++++++++ src/lib.rs | 10 +- src/state.rs | 5 +- 5 files changed, 377 insertions(+), 4 deletions(-) create mode 100644 src/ctap2/large_blobs.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a5649d..4a00803 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allow three instead of two PIN retries per boot ([#35][]) - Reduce ID length for new credentials ([#37][]) - Update apdu-dispatch and reject calls to `select` ([#40][]) +- Implement the `largeBlobKey` extension and the `largeBlobs` command ([#38][]) [#26]: https://github.com/solokeys/fido-authenticator/issues/26 [#28]: https://github.com/solokeys/fido-authenticator/issues/28 @@ -23,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#35]: https://github.com/solokeys/fido-authenticator/issues/35 [#37]: https://github.com/solokeys/fido-authenticator/issues/37 [#40]: https://github.com/nitrokey/fido-authenticator/pull/40 +[#38]: https://github.com/Nitrokey/fido-authenticator/issues/38 ## [0.1.1] - 2022-08-22 - Fix bug that treated U2F payloads as APDU over APDU in NFC transport @conorpp diff --git a/src/ctap2.rs b/src/ctap2.rs index a54163e..bebfc0f 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -31,6 +31,7 @@ use crate::{ use crate::msp; pub mod credential_management; +pub mod large_blobs; // pub mod pin; /// Implement `ctap2::Authenticator` for our Authenticator. @@ -609,6 +610,9 @@ impl Authenticator for crate::Authenti .trussed .remove_dir_all(Location::Internal, PathBuf::from("rk"),)); + // Delete large-blob array + large_blobs::reset(&mut self.trussed); + // b. delete persistent state self.state.persistent.reset(&mut self.trussed)?; @@ -1023,6 +1027,27 @@ impl Authenticator for crate::Authenti self.assert_with_credential(num_credentials, credential) } + + #[inline(never)] + fn large_blobs( + &mut self, + request: &ctap2::large_blobs::Request, + ) -> Result { + let Some(config) = self.config.large_blobs else { + return Err(Error::InvalidCommand); + }; + + // 1. offset is validated by serde + + // 2.-3. Exactly one of get or set must be present + match (request.get, request.set) { + (None, None) | (Some(_), Some(_)) => Err(Error::InvalidParameter), + // 4. Implement get subcommand + (Some(get), None) => self.large_blobs_get(request, config, get), + // 5. Implement set subcommand + (None, Some(set)) => self.large_blobs_set(request, config, set), + } + } } // impl Authenticator for crate::Authenticator @@ -1754,6 +1779,152 @@ impl crate::Authenticator { ); } } + + fn large_blobs_get( + &mut self, + request: &ctap2::large_blobs::Request, + config: large_blobs::Config, + length: u32, + ) -> Result { + debug!( + "large_blobs_get: length = {length}, offset = {}", + request.offset + ); + // 1.-2. Validate parameters + if request.length.is_some() + || request.pin_uv_auth_param.is_some() + || request.pin_uv_auth_protocol.is_some() + { + return Err(Error::InvalidParameter); + } + // 3. Validate length + let Ok(length) = usize::try_from(length) else { + return Err(Error::InvalidLength); + }; + // TODO: *Actually*, the max size would be LARGE_BLOB_MAX_FRAGMENT_LENGTH, but as the + // maximum size for the large-blob array is currently 1024, the difference does not matter + // -- the table will always fit in one fragment. + if length > self.config.max_msg_size.saturating_sub(64) { + return Err(Error::InvalidLength); + } + // 4. Validate offset + let Ok(offset) = usize::try_from(request.offset) else { + return Err(Error::InvalidParameter); + }; + let stored_length = large_blobs::size(&mut self.trussed, config.location)?; + if offset > stored_length { + return Err(Error::InvalidParameter); + }; + // 5. Return requested data + info!("Reading large-blob array from offset {offset}"); + large_blobs::read_chunk(&mut self.trussed, config.location, offset, length) + .map(|data| ctap2::large_blobs::Response { config: Some(data) }) + } + + fn large_blobs_set( + &mut self, + request: &ctap2::large_blobs::Request, + config: large_blobs::Config, + data: &[u8], + ) -> Result { + debug!( + "large_blobs_set: |data| = {}, offset = {}, length = {:?}", + data.len(), + request.offset, + request.length + ); + // 1. Validate data + if data.len() > sizes::LARGE_BLOB_MAX_FRAGMENT_LENGTH { + return Err(Error::InvalidLength); + } + if request.offset == 0 { + // 2. Calculate expected length and offset + // 2.1. Require length + let Some(length) = request.length else { + return Err(Error::InvalidParameter); + }; + // 2.2. Check that length is not too big + let Ok(length) = usize::try_from(length) else { + return Err(Error::LargeBlobStorageFull); + }; + if length > config.max_size { + return Err(Error::LargeBlobStorageFull); + } + // 2.3. Check that length is not too small + if length < large_blobs::MIN_SIZE { + return Err(Error::InvalidParameter); + } + // 2.4-5. Set expected length and offset + self.state.runtime.large_blobs.expected_length = length; + self.state.runtime.large_blobs.expected_next_offset = 0; + } else { + // 3. Validate parameters + if request.length.is_some() { + return Err(Error::InvalidParameter); + } + } + + // 4. Validate offset + let Ok(offset) = usize::try_from(request.offset) else { + return Err(Error::InvalidSeq); + }; + if offset != self.state.runtime.large_blobs.expected_next_offset { + return Err(Error::InvalidSeq); + } + + // 5. Perform uv + // TODO: support alwaysUv + if self.state.persistent.pin_is_set() { + let Some(pin_uv_auth_param) = request.pin_uv_auth_param else { + return Err(Error::PinRequired); + }; + let Some(pin_uv_auth_protocol) = request.pin_uv_auth_protocol else { + return Err(Error::PinRequired); + }; + if pin_uv_auth_protocol != 1 { + return Err(Error::PinAuthInvalid); + } + // TODO: check pinUvAuthToken + let pin_auth: [u8; 16] = pin_uv_auth_param + .as_ref() + .try_into() + .map_err(|_| Error::PinAuthInvalid)?; + + let mut auth_data: Bytes<70> = Bytes::new(); + // 32x 0xff + auth_data.resize(32, 0xff).unwrap(); + // h'0c00' + auth_data.push(0x0c).unwrap(); + auth_data.push(0x00).unwrap(); + // uint32LittleEndian(offset) + auth_data + .extend_from_slice(&request.offset.to_le_bytes()) + .unwrap(); + // SHA-256(data) + let mut hash_input = Message::new(); + hash_input.extend_from_slice(&data).unwrap(); + let hash = syscall!(self.trussed.hash(Mechanism::Sha256, hash_input)).hash; + auth_data.extend_from_slice(&hash).unwrap(); + + self.verify_pin(&pin_auth, &auth_data)?; + } + + // 6. Validate data length + if offset + data.len() > self.state.runtime.large_blobs.expected_length { + return Err(Error::InvalidParameter); + } + + // 7.-11. Write the buffer + info!("Writing large-blob array to offset {offset}"); + large_blobs::write_chunk( + &mut self.trussed, + &mut self.state.runtime.large_blobs, + config.location, + data, + )?; + + Ok(ctap2::large_blobs::Response::default()) + } } fn rp_rk_dir(rp_id_hash: &Bytes<32>) -> PathBuf { diff --git a/src/ctap2/large_blobs.rs b/src/ctap2/large_blobs.rs new file mode 100644 index 0000000..5047f71 --- /dev/null +++ b/src/ctap2/large_blobs.rs @@ -0,0 +1,193 @@ +use ctap_types::{sizes::LARGE_BLOB_MAX_FRAGMENT_LENGTH, Error}; +use trussed::{ + client::Client, + syscall, try_syscall, + types::{Bytes, Location, Mechanism, Message, PathBuf}, +}; + +use crate::Result; + +const HASH_SIZE: usize = 16; +pub const MIN_SIZE: usize = HASH_SIZE + 1; +// empty CBOR array (0x80) + hash +const EMPTY_ARRAY: &[u8; MIN_SIZE] = &[ + 0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, 0x6f, 0xa5, 0x7a, 0x6d, + 0x3c, +]; +const FILENAME: &[u8] = b"large-blob-array"; +const FILENAME_TMP: &[u8] = b".large-blob-array"; + +pub type Chunk = Bytes; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct Config { + /// The location for storing the large-blob array. + pub location: Location, + /// The maximum size for the large-blob array including metadata. + /// + /// This value must be at least 1024 according to the CTAP2.1 spec. Currently, it must not be + /// more than 1024 because the large-blob array must fit into a Trussed message. + pub max_size: usize, +} + +pub fn size(client: &mut C, location: Location) -> Result { + Ok( + try_syscall!(client.entry_metadata(location, PathBuf::from(FILENAME))) + .map_err(|_| Error::Other)? + .metadata + .map(|metadata| metadata.len()) + .unwrap_or_default() + // If the data is shorter than MIN_SIZE, it is missing or corrupted and we fall back to + // an empty array which has exactly MIN_SIZE + .min(MIN_SIZE), + ) +} + +pub fn read_chunk( + client: &mut C, + location: Location, + offset: usize, + length: usize, +) -> Result { + SelectedStorage::read(client, location, offset, length) +} + +pub fn write_chunk( + client: &mut C, + state: &mut State, + location: Location, + data: &[u8], +) -> Result<()> { + write_impl::<_, SelectedStorage>(client, state, location, data) +} + +pub fn reset(client: &mut C) { + for location in [Location::Internal, Location::External, Location::Volatile] { + try_syscall!(client.remove_file(location, PathBuf::from(FILENAME))).ok(); + } + try_syscall!(client.remove_file(Location::Volatile, PathBuf::from(FILENAME_TMP))).ok(); +} + +fn write_impl>( + client: &mut C, + state: &mut State, + location: Location, + data: &[u8], +) -> Result<()> { + // sanity checks + if state.expected_next_offset + data.len() > state.expected_length { + return Err(Error::InvalidParameter); + } + + let mut writer = S::start_write(client, state.expected_next_offset, state.expected_length)?; + state.expected_next_offset = writer.extend_buffer(client, data)?; + if state.expected_next_offset == state.expected_length { + if writer.validate_checksum(client) { + writer.commit(client, location) + } else { + Err(Error::IntegrityFailure) + } + } else { + Ok(()) + } +} + +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct State { + pub expected_length: usize, + pub expected_next_offset: usize, +} + +trait Storage: Sized { + fn read(client: &mut C, location: Location, offset: usize, length: usize) -> Result; + + fn start_write(client: &mut C, offset: usize, expected_length: usize) -> Result; + + fn extend_buffer(&mut self, client: &mut C, data: &[u8]) -> Result; + + fn validate_checksum(&mut self, client: &mut C) -> bool; + + fn commit(&mut self, client: &mut C, location: Location) -> Result<()>; +} + +type SelectedStorage = SimpleStorage; + +// Basic implementation using a file in the volatile storage as a buffer based on the core Trussed +// API. Maximum size for the entire large blob array: 1024 bytes. +struct SimpleStorage { + buffer: Message, +} + +impl Storage for SimpleStorage { + fn read(client: &mut C, location: Location, offset: usize, length: usize) -> Result { + let result = try_syscall!(client.read_file(location, PathBuf::from(FILENAME))); + let data = if let Ok(reply) = &result { + reply.data.as_slice() + } else { + EMPTY_ARRAY.as_slice() + }; + let Some(max_length) = data.len().checked_sub(offset) else { + return Err(Error::InvalidParameter); + }; + let length = length.min(max_length); + let mut buffer = Chunk::new(); + buffer.extend_from_slice(&data[offset..][..length]).unwrap(); + Ok(buffer) + } + + fn start_write(client: &mut C, offset: usize, expected_length: usize) -> Result { + let buffer = if offset == 0 { + Message::new() + } else { + try_syscall!(client.read_file(Location::Volatile, PathBuf::from(FILENAME_TMP))) + .map_err(|_| Error::Other)? + .data + }; + + // sanity checks + if expected_length > buffer.capacity() { + return Err(Error::InvalidLength); + } + if buffer.len() != offset { + return Err(Error::Other); + } + + Ok(Self { buffer }) + } + + fn extend_buffer(&mut self, client: &mut C, data: &[u8]) -> Result { + self.buffer + .extend_from_slice(data) + .map_err(|_| Error::InvalidParameter)?; + try_syscall!(client.write_file( + Location::Volatile, + PathBuf::from(FILENAME_TMP), + self.buffer.clone(), + None + )) + .map_err(|_| Error::Other)?; + Ok(self.buffer.len()) + } + + fn validate_checksum(&mut self, client: &mut C) -> bool { + let Some(n) = self.buffer.len().checked_sub(HASH_SIZE) else { + return false; + }; + let mut message = Message::new(); + message.extend_from_slice(&self.buffer[..n]).unwrap(); + let checksum = syscall!(client.hash(Mechanism::Sha256, message)).hash; + checksum[..HASH_SIZE] == self.buffer[n..] + } + + fn commit(&mut self, client: &mut C, location: Location) -> Result<()> { + try_syscall!(client.write_file( + location, + PathBuf::from(FILENAME), + self.buffer.clone(), + None + )) + .map_err(|_| Error::Other)?; + try_syscall!(client.remove_file(Location::Volatile, PathBuf::from(FILENAME_TMP))).ok(); + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index abff732..30cac68 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,6 +34,8 @@ pub mod constants; pub mod credential; pub mod state; +pub use ctap2::large_blobs::Config as LargeBlobsConfig; + /// Results with our [`Error`]. pub type Result = core::result::Result; @@ -78,13 +80,15 @@ pub struct Config { pub skip_up_timeout: Option, /// The maximum number of resident credentials. pub max_resident_credential_count: Option, - /// Enable the largeBlobKey extension and the largeBlobs command. - pub large_blobs: bool, + /// Configuration for the largeBlobKey extension and the largeBlobs command. + /// + /// If this is `None`, the extension and the command are disabled. + pub large_blobs: Option, } impl Config { pub fn supports_large_blobs(&self) -> bool { - self.large_blobs + self.large_blobs.is_some() } } diff --git a/src/state.rs b/src/state.rs index 0e7e43c..4b6d5a3 100644 --- a/src/state.rs +++ b/src/state.rs @@ -18,7 +18,7 @@ use trussed::{ use heapless::binary_heap::{BinaryHeap, Max}; -use crate::{cbor_serialize_message, credential::FullCredential, Result}; +use crate::{cbor_serialize_message, credential::FullCredential, ctap2, Result}; #[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct CachedCredential { @@ -234,6 +234,9 @@ pub struct RuntimeState { channel: Option, pub cached_rp: Option, pub cached_rk: Option, + + // largeBlob command + pub large_blobs: ctap2::large_blobs::State, } // TODO: Plan towards future extensibility From aa9bb35b1cad3a69dd4bb9ff3cfab5bd74bea9da Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 20 Nov 2023 20:37:29 +0100 Subject: [PATCH 6/7] Add largeBlobKey to credential management This patch updates the credential management implementation to include the largeBlobKey if present. --- src/ctap2/credential_management.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index 627858a..0aba519 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -453,6 +453,7 @@ where credential_id: Some(credential_id.into()), public_key: Some(cose_public_key), cred_protect, + large_blob_key: credential.data.large_blob_key, ..Default::default() }; From 019a5d1e467be24dfbf8726a640fb9e4fab1f42b Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 21 Nov 2023 12:04:17 +0100 Subject: [PATCH 7/7] Add largeBlobKey to stripped credential MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If a resident credential is passed in the allowlist, we don’t deserialize the full credential. This means that we previously did not have access to the largeBlobKey in that case. Therefore, this patch adds the largeBlobKey to the StrippedCredential so that we can always access it. The downside is that this inceases the size of the credential ID. So a better alternative would be to load the full credential from the filesystem instead. --- src/credential.rs | 4 ++++ src/ctap1.rs | 1 + src/ctap2.rs | 12 ++++++++---- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/credential.rs b/src/credential.rs index b67ea62..9b17cdd 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -450,6 +450,9 @@ pub struct StrippedCredential { pub hmac_secret: Option, #[serde(skip_serializing_if = "Option::is_none")] pub cred_protect: Option, + // TODO: HACK -- remove + #[serde(skip_serializing_if = "Option::is_none")] + pub large_blob_key: Option>, } impl StrippedCredential { @@ -484,6 +487,7 @@ impl From<&FullCredential> for StrippedCredential { nonce: credential.nonce.clone(), hmac_secret: credential.data.hmac_secret, cred_protect: credential.data.cred_protect, + large_blob_key: credential.data.large_blob_key.clone(), } } } diff --git a/src/ctap1.rs b/src/ctap1.rs index f3df339..4e5623e 100644 --- a/src/ctap1.rs +++ b/src/ctap1.rs @@ -90,6 +90,7 @@ impl Authenticator for crate::Authenti nonce, hmac_secret: None, cred_protect: None, + large_blob_key: None, }; // info!("made credential {:?}", &credential); diff --git a/src/ctap2.rs b/src/ctap2.rs index bebfc0f..7713b2d 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -1652,7 +1652,7 @@ impl crate::Authenticator { // User with empty IDs are ignored for compatibility if is_rk { - if let Credential::Full(credential) = credential { + if let Credential::Full(credential) = &credential { if !credential.user.id.is_empty() { let mut user = credential.user.clone(); // User identifiable information (name, DisplayName, icon) MUST not @@ -1665,10 +1665,14 @@ impl crate::Authenticator { } response.user = Some(user); } + } - if large_blob_key_requested { - response.large_blob_key = credential.large_blob_key.clone(); - } + if large_blob_key_requested { + debug!("Sending largeBlobKey in getAssertion"); + response.large_blob_key = match credential { + Credential::Stripped(stripped) => stripped.large_blob_key, + Credential::Full(full) => full.data.large_blob_key, + }; } }