Merge pull request #92 from Nitrokey/dynamic-limit

Add dynamic estimation of remaining credential space
This commit is contained in:
sosthene-nitrokey
2024-08-01 14:56:45 +02:00
committed by GitHub
6 changed files with 87 additions and 48 deletions
+6 -4
View File
@@ -24,6 +24,7 @@ serde_bytes = { version = "0.11.14", default-features = false }
serde-indexed = "0.1.0"
sha2 = { version = "0.10", default-features = false }
trussed = "0.1"
trussed-fs-info = "0.1.0"
trussed-hkdf = { version = "0.2.0" }
trussed-chunked = { version = "0.1.0", optional = true }
@@ -65,7 +66,7 @@ p256 = { version = "0.13.2", features = ["ecdh"] }
rand = "0.8.4"
sha2 = "0.10"
trussed = { version = "0.1", features = ["virt"] }
trussed-staging = { version = "0.3.0", features = ["chunked", "hkdf", "virt"] }
trussed-staging = { version = "0.3.0", features = ["chunked", "hkdf", "virt", "fs-info"] }
trussed-usbip = { version = "0.0.1", default-features = false, features = ["ctaphid"] }
usbd-ctaphid = "0.1.0"
x509-parser = "0.16.0"
@@ -78,10 +79,11 @@ cbor-smol = { git = "https://github.com/sosthene-nitrokey/cbor-smol.git", rev =
ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "72eb68b61e3f14957c5ab89bd22f776ac860eb62" }
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" }
littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "2b45a7559ff44260c6dd693e4cb61f54ae5efc53" }
trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "b548d379dcbd67d29453d94847b7bc33ae92e673" }
trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "a055e4f79a10122c8c0c882161442e6e02f0c5c6" }
littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "960e57d9fc0d209308c8e15dc26252bbe1ff6ba8" }
trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "chunked-v0.1.0" }
trussed-fs-info = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "170ab14f3bb6760399749d78e1b94e3b70106739" }
trussed-hkdf = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "hkdf-v0.2.0" }
trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "v0.3.0" }
trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "170ab14f3bb6760399749d78e1b94e3b70106739" }
trussed-usbip = { git = "https://github.com/Nitrokey/pc-usbip-runner.git", tag = "v0.0.1-nitrokey.1" }
usbd-ctaphid = { git = "https://github.com/trussed-dev/usbd-ctaphid.git", rev = "dcff9009c3cd1ef9e5b09f8f307aca998fc9a8c8" }
+5 -4
View File
@@ -10,7 +10,7 @@ cargo-fuzz = true
[dependencies]
ctap-types = { version = "0.2.0", features = ["arbitrary"] }
libfuzzer-sys = "0.4"
trussed-staging = { version = "0.3.0", features = ["chunked", "hkdf", "virt"] }
trussed-staging = { version = "0.3.0", features = ["chunked", "hkdf", "virt", "fs-info"] }
[dependencies.fido-authenticator]
path = ".."
@@ -24,9 +24,10 @@ bench = false
[patch.crates-io]
ctap-types = { git = "https://github.com/trussed-dev/ctap-types.git", rev = "72eb68b61e3f14957c5ab89bd22f776ac860eb62" }
littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "2b45a7559ff44260c6dd693e4cb61f54ae5efc53" }
trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "b548d379dcbd67d29453d94847b7bc33ae92e673" }
trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "a055e4f79a10122c8c0c882161442e6e02f0c5c6" }
littlefs2 = { git = "https://github.com/trussed-dev/littlefs2.git", rev = "960e57d9fc0d209308c8e15dc26252bbe1ff6ba8" }
trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "chunked-v0.1.0" }
trussed-hkdf = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "hkdf-v0.2.0" }
trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "v0.3.0" }
cbor-smol = { git = "https://github.com/sosthene-nitrokey/cbor-smol.git", rev = "9a77dc9b528b08f531d76b44af2f5336c4ef17e0"}
trussed-fs-info = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "170ab14f3bb6760399749d78e1b94e3b70106739" }
trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "170ab14f3bb6760399749d78e1b94e3b70106739" }
+10 -24
View File
@@ -1,5 +1,6 @@
//! The `ctap_types::ctap2::Authenticator` implementation.
use credential_management::CredentialManagement;
use ctap_types::{
ctap2::{
self, client_pin::Permissions, AttestationFormatsPreference, AttestationStatement,
@@ -21,15 +22,9 @@ use trussed::{
};
use crate::{
constants,
constants::{self, MAX_RESIDENT_CREDENTIALS_GUESSTIMATE},
credential::{self, Credential, FullCredential, Key, StrippedCredential},
format_hex,
state::{
self,
// // (2022-02-27): 9288 bytes
// MinCredentialHeap,
},
Result, SigningAlgorithm, TrussedRequirements, UserPresence,
format_hex, state, Result, SigningAlgorithm, TrussedRequirements, UserPresence,
};
#[allow(unused_imports)]
@@ -393,21 +388,12 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
self.delete_resident_key_by_user_id(&rp_id_hash, &credential.user.id)
.ok();
let mut key_store_full = false;
// then check the maximum number of RK credentials
if let Some(max_count) = self.config.max_resident_credential_count {
let mut cm = credential_management::CredentialManagement::new(self);
let metadata = cm.get_creds_metadata();
let count = metadata
.existing_resident_credentials_count
.unwrap_or(max_count);
debug!("resident cred count: {} (max: {})", count, max_count);
if count >= max_count {
error!("maximum resident credential count reached");
key_store_full = true;
}
}
let mut key_store_full = self.can_fit(serialized_credential.len()) == Some(false)
|| CredentialManagement::new(self).count_credentials()
>= self
.config
.max_resident_credential_count
.unwrap_or(MAX_RESIDENT_CREDENTIALS_GUESSTIMATE);
if !key_store_full {
// then store key, making it resident
@@ -1870,7 +1856,7 @@ impl<UP: UserPresence, T: TrussedRequirements> crate::Authenticator<UP, T> {
);
} else {
info!("deleting parent {:?} as this was its last RK", &rp_path);
syscall!(self.trussed.remove_dir(Location::Internal, rp_path,));
try_syscall!(self.trussed.remove_dir(Location::Internal, rp_path,)).ok();
}
}
+15 -11
View File
@@ -65,14 +65,21 @@ where
info!("get metadata");
let mut response: Response = Default::default();
let max_resident_credentials = self
.config
.max_resident_credential_count
.unwrap_or(MAX_RESIDENT_CREDENTIALS_GUESSTIMATE);
response.existing_resident_credentials_count = Some(0);
let max_resident_credentials = self.estimate_remaining();
let credential_count = self.count_credentials();
response.existing_resident_credentials_count = Some(credential_count);
response.max_possible_remaining_residential_credentials_count =
Some(max_resident_credentials);
Some(max_resident_credentials.unwrap_or_else(|| {
self.config
.max_resident_credential_count
.unwrap_or(MAX_RESIDENT_CREDENTIALS_GUESSTIMATE)
.saturating_sub(credential_count)
}));
response
}
pub fn count_credentials(&mut self) -> u32 {
let dir = PathBuf::from(b"rk");
let maybe_first_rp =
syscall!(self
@@ -81,7 +88,7 @@ where
.entry;
let first_rp = match maybe_first_rp {
None => return response,
None => return 0,
Some(rp) => rp,
};
@@ -98,10 +105,7 @@ where
match maybe_next_rp {
None => {
response.existing_resident_credentials_count = Some(num_rks);
response.max_possible_remaining_residential_credentials_count =
Some(max_resident_credentials.saturating_sub(num_rks));
return response;
return num_rks;
}
Some(rp) => {
last_rp = PathBuf::from(rp.file_name());
+8 -4
View File
@@ -1,11 +1,13 @@
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},
try_syscall,
types::{Bytes, Location, Message, PathBuf},
};
#[cfg(not(feature = "chunked"))]
use trussed::{syscall, types::Mechanism};
use crate::{Result, TrussedRequirements};
const HASH_SIZE: usize = 16;
@@ -151,12 +153,14 @@ 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.
#[cfg(not(feature = "chunked"))]
struct SimpleStorage {
location: Location,
buffer: Message,
}
impl<C: Client> Storage<C> for SimpleStorage {
#[cfg(not(feature = "chunked"))]
impl<C: TrussedRequirements> Storage<C> for SimpleStorage {
fn read(client: &mut C, location: Location, offset: usize, length: usize) -> Result<Chunk> {
let result = try_syscall!(client.read_file(location, PathBuf::from(FILENAME)));
let data = if let Ok(reply) = &result {
+43 -1
View File
@@ -20,7 +20,12 @@ generate_macros!();
use core::time::Duration;
use trussed::{client, syscall, types::Message, Client as TrussedClient};
use trussed::{
client, syscall,
types::{Location, Message},
Client as TrussedClient,
};
use trussed_fs_info::{FsInfoClient, FsInfoReply};
use trussed_hkdf::HkdfClient;
/// Re-export of `ctap-types` authenticator errors.
@@ -57,6 +62,7 @@ pub trait TrussedRequirements:
+ client::Sha256
+ client::HmacSha256
+ client::Ed255 // + client::Totp
+ FsInfoClient
+ HkdfClient
+ ExtensionRequirements
{
@@ -70,6 +76,7 @@ impl<T> TrussedRequirements for T where
+ client::Sha256
+ client::HmacSha256
+ client::Ed255 // + client::Totp
+ FsInfoClient
+ HkdfClient
+ ExtensionRequirements
{
@@ -266,6 +273,41 @@ where
}
}
fn estimate_remaining_inner(info: &FsInfoReply) -> Option<u32> {
let block_size = info.block_info.as_ref()?.size;
// 1 block for the directory, 1 for the private key, 400 bytes for a reasonnable key and metadata
let size_taken = 2 * block_size + 400;
// Remove 5 block kept as buffer
Some((info.available_space.saturating_sub(5 * block_size) / size_taken) as u32)
}
fn estimate_remaining(&mut self) -> Option<u32> {
let info = syscall!(self.trussed.fs_info(Location::Internal));
debug!("Got filesystem info: {info:?}");
Self::estimate_remaining_inner(&info)
}
fn can_fit_inner(info: &FsInfoReply, size: usize) -> Option<bool> {
let block_size = info.block_info.as_ref()?.size;
// 1 block for the rp directory, 5 block of margin, 50 bytes for a reasonnable metadata
let size_taken = 6 * block_size + size + 50;
Some(size_taken < info.available_space)
}
/// Can a credential of size `size` be stored with safe margins
///
/// This assumes that the key has already been generated and is stored.
fn can_fit(&mut self, size: usize) -> Option<bool> {
debug!("Can fit for {size} bytes");
let info = syscall!(self.trussed.fs_info(Location::Internal));
debug!("Got filesystem info: {info:?}");
debug!(
"Available storage: {}",
Self::estimate_remaining_inner(&info)
);
Self::can_fit_inner(&info, size)
}
fn hash(&mut self, data: &[u8]) -> [u8; 32] {
let hash = syscall!(self.trussed.hash_sha256(data)).hash;
hash.as_slice().try_into().expect("hash should fit")