Skip to content

Commit

Permalink
Implement largeBlobs command
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
robin-nitrokey committed Nov 28, 2023
1 parent 48d66c0 commit f3128f8
Show file tree
Hide file tree
Showing 5 changed files with 377 additions and 4 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
171 changes: 171 additions & 0 deletions src/ctap2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -609,6 +610,9 @@ impl<UP: UserPresence, T: TrussedRequirements> 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)?;

Expand Down Expand Up @@ -1023,6 +1027,27 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti

self.assert_with_credential(num_credentials, credential)
}

#[inline(never)]
fn large_blobs(
&mut self,
request: &ctap2::large_blobs::Request,
) -> Result<ctap2::large_blobs::Response> {
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<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenticator<UP, T>
Expand Down Expand Up @@ -1754,6 +1779,152 @@ impl<UP: UserPresence, T: TrussedRequirements> crate::Authenticator<UP, T> {
);
}
}

fn large_blobs_get(
&mut self,
request: &ctap2::large_blobs::Request,
config: large_blobs::Config,
length: u32,
) -> Result<ctap2::large_blobs::Response> {
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<ctap2::large_blobs::Response> {
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 {
Expand Down
Loading

0 comments on commit f3128f8

Please sign in to comment.