mirror of
https://github.com/trussed-dev/fido-authenticator.git
synced 2026-06-20 04:16:16 -07:00
Merge pull request #42 from Nitrokey/large-blobs-chunked
Use streaming API for large blobs
This commit is contained in:
+9
-3
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>,
|
||||
|
||||
Reference in New Issue
Block a user