Use streaming API for large blobs

This patch adds an implementation of the large-blob storage using the
streaming API, making it possible to write data that is larger than 1024
bytes, the Trussed message size.
This commit is contained in:
Robin Krahl
2023-11-28 20:35:23 +01:00
parent a4fff2f4bb
commit c3ef7126b7
5 changed files with 261 additions and 38 deletions
+9 -3
View File
@@ -11,14 +11,16 @@ description = "FIDO authenticator Trussed app"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
ctap-types = "0.1.0"
ctap-types = { version = "0.1.0", features = ["large-blobs"] }
delog = "0.1.0"
heapless = "0.7"
interchange = "0.2.0"
serde = { version = "1.0", default-features = false }
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-staging = { version = "0.1.0", default-features = false, optional = true }
apdu-dispatch = { version = "0.1", optional = true }
ctaphid-dispatch = { version = "0.1", optional = true }
@@ -29,6 +31,9 @@ dispatch = ["apdu-dispatch", "ctaphid-dispatch", "iso7816"]
disable-reset-time-window = []
enable-fido-pre = []
# enables support for a large-blob array longer than 1024 bytes
chunked = ["trussed-staging/chunked"]
log-all = []
log-none = []
log-info = []
@@ -45,8 +50,9 @@ trussed = { version = "0.1", features = ["virt"] }
features = ["dispatch"]
[patch.crates-io]
ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "785bcc52720ce2e2054ae32034a2a24c500e1043" }
ctap-types = { git = "https://github.com/Nitrokey/ctap-types.git", branch = "msg-size" }
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" }
trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "b1781805a2e33615d2d00b8bec80c0b1f5870ca1" }
trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging", rev = "3b9594d93f89a5e760fe78fa5a96f125dfdcd470" }
serde-indexed = { git = "https://github.com/sosthene-nitrokey/serde-indexed.git", rev = "5005d23cb4ee8622e62188ea0f9466146f851f0d" }
+7 -9
View File
@@ -6,6 +6,7 @@ use ctap_types::{
heapless_bytes::Bytes,
sizes, Error,
};
use sha2::{Digest as _, Sha256};
use trussed::{
syscall, try_syscall,
@@ -1799,24 +1800,24 @@ impl<UP: UserPresence, T: TrussedRequirements> crate::Authenticator<UP, T> {
|| request.pin_uv_auth_param.is_some()
|| request.pin_uv_auth_protocol.is_some()
{
error!("length/pin set");
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 {
error!("offset too large");
return Err(Error::InvalidParameter);
};
let stored_length = large_blobs::size(&mut self.trussed, config.location)?;
if offset > stored_length {
error!("offset: {offset}, stored_length: {stored_length}");
return Err(Error::InvalidParameter);
};
// 5. Return requested data
@@ -1838,7 +1839,7 @@ impl<UP: UserPresence, T: TrussedRequirements> crate::Authenticator<UP, T> {
request.length
);
// 1. Validate data
if data.len() > sizes::LARGE_BLOB_MAX_FRAGMENT_LENGTH {
if data.len() > self.config.max_msg_size.saturating_sub(64) {
return Err(Error::InvalidLength);
}
if request.offset == 0 {
@@ -1851,7 +1852,7 @@ impl<UP: UserPresence, T: TrussedRequirements> crate::Authenticator<UP, T> {
let Ok(length) = usize::try_from(length) else {
return Err(Error::LargeBlobStorageFull);
};
if length > config.max_size {
if length > config.max_size() {
return Err(Error::LargeBlobStorageFull);
}
// 2.3. Check that length is not too small
@@ -1905,10 +1906,7 @@ impl<UP: UserPresence, T: TrussedRequirements> crate::Authenticator<UP, T> {
.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();
auth_data.extend_from_slice(&Sha256::digest(&data)).unwrap();
self.verify_pin(&pin_auth, &auth_data)?;
}
+227 -22
View File
@@ -1,11 +1,12 @@
use ctap_types::{sizes::LARGE_BLOB_MAX_FRAGMENT_LENGTH, Error};
use trussed::{
client::Client,
config::MAX_MESSAGE_LENGTH,
syscall, try_syscall,
types::{Bytes, Location, Mechanism, Message, PathBuf},
};
use crate::Result;
use crate::{Result, TrussedRequirements};
const HASH_SIZE: usize = 16;
pub const MIN_SIZE: usize = HASH_SIZE + 1;
@@ -25,12 +26,29 @@ pub struct Config {
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.
/// This value must be at least 1024 according to the CTAP2.1 spec. Without the chunking
/// extension, it cannot be larger than 1024 because the large-blob array must fit into a
/// Trussed message. Therefore, this setting is only available if the chunked feature is
/// enabled.
#[cfg(feature = "chunked")]
pub max_size: usize,
}
pub fn size<C: Client>(client: &mut C, location: Location) -> Result<usize> {
impl Config {
pub fn max_size(&self) -> usize {
#[cfg(feature = "chunked")]
{
self.max_size
}
#[cfg(not(feature = "chunked"))]
{
MAX_MESSAGE_LENGTH
}
}
}
pub fn size<C: TrussedRequirements>(client: &mut C, location: Location) -> Result<usize> {
Ok(
try_syscall!(client.entry_metadata(location, PathBuf::from(FILENAME)))
.map_err(|_| Error::Other)?
@@ -39,11 +57,11 @@ pub fn size<C: Client>(client: &mut C, location: Location) -> Result<usize> {
.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),
.max(MIN_SIZE),
)
}
pub fn read_chunk<C: Client>(
pub fn read_chunk<C: TrussedRequirements>(
client: &mut C,
location: Location,
offset: usize,
@@ -52,7 +70,7 @@ pub fn read_chunk<C: Client>(
SelectedStorage::read(client, location, offset, length)
}
pub fn write_chunk<C: Client>(
pub fn write_chunk<C: TrussedRequirements>(
client: &mut C,
state: &mut State,
location: Location,
@@ -61,7 +79,7 @@ pub fn write_chunk<C: Client>(
write_impl::<_, SelectedStorage>(client, state, location, data)
}
pub fn reset<C: Client>(client: &mut C) {
pub fn reset<C: TrussedRequirements>(client: &mut C) {
for location in [Location::Internal, Location::External, Location::Volatile] {
try_syscall!(client.remove_file(location, PathBuf::from(FILENAME))).ok();
}
@@ -79,12 +97,18 @@ fn write_impl<C, S: Storage<C>>(
return Err(Error::InvalidParameter);
}
let mut writer = S::start_write(client, state.expected_next_offset, state.expected_length)?;
let mut writer = S::start_write(
client,
location,
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)
if writer.validate_checksum(client)? {
writer.commit(client)
} else {
writer.abort(client)?;
Err(Error::IntegrityFailure)
}
} else {
@@ -92,7 +116,7 @@ fn write_impl<C, S: Storage<C>>(
}
}
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
#[derive(Clone, Debug, Default)]
pub struct State {
pub expected_length: usize,
pub expected_next_offset: usize,
@@ -101,20 +125,34 @@ pub struct State {
trait Storage<C>: Sized {
fn read(client: &mut C, location: Location, offset: usize, length: usize) -> Result<Chunk>;
fn start_write(client: &mut C, offset: usize, expected_length: usize) -> Result<Self>;
fn start_write(
client: &mut C,
location: Location,
offset: usize,
expected_length: usize,
) -> Result<Self>;
fn extend_buffer(&mut self, client: &mut C, data: &[u8]) -> Result<usize>;
fn validate_checksum(&mut self, client: &mut C) -> bool;
fn validate_checksum(&mut self, client: &mut C) -> Result<bool>;
fn commit(&mut self, client: &mut C, location: Location) -> Result<()>;
fn commit(&mut self, client: &mut C) -> Result<()>;
fn abort(&mut self, client: &mut C) -> Result<()> {
let _ = client;
Ok(())
}
}
#[cfg(not(feature = "chunked"))]
type SelectedStorage = SimpleStorage;
#[cfg(feature = "chunked")]
type SelectedStorage = ChunkedStorage;
// 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 {
location: Location,
buffer: Message,
}
@@ -135,7 +173,12 @@ impl<C: Client> Storage<C> for SimpleStorage {
Ok(buffer)
}
fn start_write(client: &mut C, offset: usize, expected_length: usize) -> Result<Self> {
fn start_write(
client: &mut C,
location: Location,
offset: usize,
expected_length: usize,
) -> Result<Self> {
let buffer = if offset == 0 {
Message::new()
} else {
@@ -152,7 +195,7 @@ impl<C: Client> Storage<C> for SimpleStorage {
return Err(Error::Other);
}
Ok(Self { buffer })
Ok(Self { buffer, location })
}
fn extend_buffer(&mut self, client: &mut C, data: &[u8]) -> Result<usize> {
@@ -169,19 +212,19 @@ impl<C: Client> Storage<C> for SimpleStorage {
Ok(self.buffer.len())
}
fn validate_checksum(&mut self, client: &mut C) -> bool {
fn validate_checksum(&mut self, client: &mut C) -> Result<bool> {
let Some(n) = self.buffer.len().checked_sub(HASH_SIZE) else {
return false;
return Ok(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..]
Ok(checksum[..HASH_SIZE] == self.buffer[n..])
}
fn commit(&mut self, client: &mut C, location: Location) -> Result<()> {
fn commit(&mut self, client: &mut C) -> Result<()> {
try_syscall!(client.write_file(
location,
self.location,
PathBuf::from(FILENAME),
self.buffer.clone(),
None
@@ -191,3 +234,165 @@ impl<C: Client> Storage<C> for SimpleStorage {
Ok(())
}
}
#[cfg(feature = "chunked")]
struct ChunkedStorage {
location: Location,
expected_length: usize,
create_file: bool,
}
#[cfg(feature = "chunked")]
impl<C: TrussedRequirements> Storage<C> for ChunkedStorage {
fn read(client: &mut C, location: Location, offset: usize, length: usize) -> Result<Chunk> {
debug!("ChunkedStorage::read: offset = {offset}, length = {length}");
let mut chunk = Chunk::new();
let file_size = try_syscall!(client.entry_metadata(location, PathBuf::from(FILENAME)))
.map_err(|_| Error::Other)?
.metadata
.map(|metadata| metadata.len())
.unwrap_or_default();
if file_size < MIN_SIZE {
// The stored file is missing or too short, so we fall back to an empty array.
trace!("Sending empty array instead of missing or corrupted file");
let start = offset.min(MIN_SIZE);
let end = (offset + length).min(MIN_SIZE);
chunk.extend_from_slice(&EMPTY_ARRAY[start..end]).unwrap();
return Ok(chunk);
}
while offset + chunk.len() < offset + length {
let n = MAX_MESSAGE_LENGTH.min(length - chunk.len());
let reply = try_syscall!(client.partial_read_file(
location,
PathBuf::from(FILENAME),
offset + chunk.len(),
n
))
.map_err(|_| Error::Other)?;
chunk
.extend_from_slice(&reply.data)
.map_err(|_| Error::Other)?;
if offset + chunk.len() >= reply.file_length {
break;
}
}
trace!("Read chunk with {} bytes", chunk.len());
Ok(chunk)
}
fn start_write(
_client: &mut C,
location: Location,
offset: usize,
expected_length: usize,
) -> Result<Self> {
debug!(
"ChunkedStorage::start_write: offset = {offset}, expected_length = {expected_length}"
);
let create_file = offset == 0;
Ok(ChunkedStorage {
location,
create_file,
expected_length,
})
}
fn extend_buffer(&mut self, client: &mut C, data: &[u8]) -> Result<usize> {
debug!("ChunkedStorage::extend_buffer: |data| = {}", data.len());
let mut n = 0;
for chunk in data.chunks(trussed::config::MAX_MESSAGE_LENGTH) {
trace!("Writing {} bytes", chunk.len());
let path = PathBuf::from(FILENAME_TMP);
let mut message = Message::new();
message.extend_from_slice(chunk).unwrap();
if self.create_file {
try_syscall!(client.write_file(self.location, path, message, None)).map_err(
|_err| {
error!("failed to write initial chunk: {_err:?}");
Error::Other
},
)?;
self.create_file = false;
n = data.len();
} else {
n = try_syscall!(client.append_file(self.location, path, message))
.map(|reply| reply.file_length)
.map_err(|_err| {
error!("failed to append chunk: {_err:?}");
Error::Other
})?;
}
}
Ok(n)
}
fn validate_checksum(&mut self, client: &mut C) -> Result<bool> {
use sha2::{digest::Digest as _, Sha256};
debug!("ChunkedStorage::validate_checksum");
let mut digest = Sha256::new();
let mut received_hash: Bytes<HASH_SIZE> = Bytes::new();
let mut bytes_read = 0;
let (mut chunk, mut len) =
try_syscall!(client.start_chunked_read(self.location, PathBuf::from(FILENAME_TMP)))
.map(|reply| (reply.data, reply.len))
.map_err(|_err| {
error!("Failed to read file: {:?}", _err);
Error::Other
})?;
loop {
trace!("read chunk: {}", chunk.len());
let remaining_data = self
.expected_length
.saturating_sub(bytes_read)
.saturating_sub(HASH_SIZE);
let data_end = remaining_data.min(chunk.len());
digest.update(&chunk[..data_end]);
if received_hash
.extend_from_slice(&chunk[data_end..chunk.len()])
.is_err()
{
return Ok(false);
}
bytes_read += chunk.len();
if bytes_read >= len {
break;
}
(chunk, len) = try_syscall!(client.read_file_chunk())
.map(|reply| (reply.data, reply.len))
.map_err(|_err| {
error!("Failed to read chunk: {:?}", _err);
Error::Other
})?;
}
let actual_hash = digest.finalize();
Ok(bytes_read == self.expected_length
&& received_hash.as_slice() == &actual_hash[..HASH_SIZE])
}
fn commit(&mut self, client: &mut C) -> Result<()> {
debug!("ChunkedStorage::commit");
try_syscall!(client.rename(
self.location,
PathBuf::from(FILENAME_TMP),
PathBuf::from(FILENAME)
))
.map_err(|_| Error::Other)?;
Ok(())
}
fn abort(&mut self, client: &mut C) -> Result<()> {
debug!("ChunkedStorage::abort");
try_syscall!(client.remove_file(self.location, PathBuf::from(FILENAME_TMP)))
.map_err(|_| Error::Other)?;
Ok(())
}
}
+16
View File
@@ -45,6 +45,8 @@ pub type Result<T> = core::result::Result<T, Error>;
/// - Ed25519 and P-256 are the core signature algorithms.
/// - AES-256, SHA-256 and its HMAC are used within the CTAP protocols.
/// - ChaCha8Poly1305 is our AEAD of choice, used e.g. for the key handles.
/// - Some Trussed extensions might be required depending on the activated features, see
/// [`ExtensionRequirements`][].
pub trait TrussedRequirements:
client::Client
+ client::P256
@@ -53,6 +55,7 @@ pub trait TrussedRequirements:
+ client::Sha256
+ client::HmacSha256
+ client::Ed255 // + client::Totp
+ ExtensionRequirements
{
}
@@ -64,9 +67,22 @@ impl<T> TrussedRequirements for T where
+ client::Sha256
+ client::HmacSha256
+ client::Ed255 // + client::Totp
+ ExtensionRequirements
{
}
#[cfg(not(feature = "chunked"))]
pub trait ExtensionRequirements {}
#[cfg(not(feature = "chunked"))]
impl<T> ExtensionRequirements for T {}
#[cfg(feature = "chunked")]
pub trait ExtensionRequirements: trussed_staging::streaming::ChunkedClient {}
#[cfg(feature = "chunked")]
impl<T> ExtensionRequirements for T where T: trussed_staging::streaming::ChunkedClient {}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
/// Externally defined configuration.
pub struct Config {
+2 -4
View File
@@ -70,7 +70,7 @@ impl<const N: usize> CredentialCacheGeneric<N> {
pub type CredentialCache = CredentialCacheGeneric<MAX_CREDENTIAL_COUNT_IN_LIST>;
#[derive(Clone, Debug, /*uDebug, Eq, PartialEq,*/ serde::Deserialize, serde::Serialize)]
#[derive(Clone, Debug)]
pub struct State {
/// Batch device identity (aaguid, certificate, key).
pub identity: Identity,
@@ -218,9 +218,7 @@ pub struct ActiveGetAssertionData {
pub extensions: Option<ctap_types::ctap2::get_assertion::ExtensionsInput>,
}
#[derive(
Clone, Debug, /*uDebug,*/ Default, /*PartialEq,*/ serde::Deserialize, serde::Serialize,
)]
#[derive(Clone, Debug, Default)]
pub struct RuntimeState {
key_agreement_key: Option<KeyId>,
pin_token: Option<KeyId>,