Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ credential-id-format-v2 = ["trussed-core/aes256-gcm"]
# enables support for a large-blob array longer than 1024 bytes
chunked = ["dep:trussed-chunked"]

mldsa44 = ["trussed-core/mldsa44", "ctap-types/mldsa44"]

log-all = []
log-none = []
log-trace = []
Expand All @@ -54,6 +56,7 @@ cbc = { version = "0.1.2", features = ["alloc"] }
ciborium = "0.2.2"
ciborium-io = "0.2.2"
cipher = "0.4.4"
ctap-types = { version = "=0.6.0-rc.4", features = ["mldsa44"] }
ctaphid = { version = "0.3.1", default-features = false }
ctaphid-dispatch = "0.4"
delog = { version = "0.1.6", features = ["std-log"] }
Expand All @@ -69,7 +72,7 @@ rand = "0.8.4"
rand_chacha = "0.3"
sha2 = "0.10"
serde_test = "1.0.176"
trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "b2492feada78ee17b936c99c13df480a4fc6d2f5", features = ["virt"] }
trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "b2492feada78ee17b936c99c13df480a4fc6d2f5", features = ["mldsa44", "virt"] }
trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "v0.4.0", features = ["chunked", "hkdf", "virt", "fs-info"] }
trussed-usbip = { git = "https://github.com/trussed-dev/pc-usbip-runner.git", rev = "017921df0930707c4af68882ccb1f8b3f1bbf7c5", default-features = false, features = ["ctaphid"] }
usbd-ctaphid = "0.4"
Expand All @@ -79,7 +82,13 @@ x509-parser = "0.16"
features = ["chunked", "dispatch"]

[patch.crates-io]
trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "b2492feada78ee17b936c99c13df480a4fc6d2f5" }
ctap-types = { git = "https://github.com/0x0ece/ctap-types", rev = "1056c78b06fddc1010afebf549434ac097a6db42" }
trussed = { git = "https://github.com/0x0ece/trussed", rev = "a1b33c46eb16ef7fa5f6324f7c099ff339fe21e7" }
trussed-core = { git = "https://github.com/0x0ece/trussed", rev = "a1b33c46eb16ef7fa5f6324f7c099ff339fe21e7" }

[patch."https://github.com/trussed-dev/trussed.git"]
trussed = { git = "https://github.com/0x0ece/trussed", rev = "a1b33c46eb16ef7fa5f6324f7c099ff339fe21e7" }
trussed-core = { git = "https://github.com/0x0ece/trussed", rev = "a1b33c46eb16ef7fa5f6324f7c099ff339fe21e7" }

[profile.test]
opt-level = 2
4 changes: 3 additions & 1 deletion fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,7 @@ doc = false
bench = false

[patch.crates-io]
trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "b2492feada78ee17b936c99c13df480a4fc6d2f5" }
trussed = { git = "https://github.com/0x0ece/trussed", rev = "a1b33c46eb16ef7fa5f6324f7c099ff339fe21e7" }
trussed-core = { git = "https://github.com/0x0ece/trussed", rev = "a1b33c46eb16ef7fa5f6324f7c099ff339fe21e7" }
trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "v0.4.0" }
ctap-types = { git = "https://github.com/0x0ece/ctap-types", rev = "1056c78b06fddc1010afebf549434ac097a6db42" }
6 changes: 6 additions & 0 deletions src/ctap1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
}
};

// U2F register's `attestation_certificate` is fixed at `Bytes<1024>`.
// Real attestation certs comfortably fit; we lift it from the
// trussed `Message`-typed read so it works regardless of how the
// mldsa44 feature sizes that Message buffer.
let cert =
ctap_types::Bytes::<1024>::try_from(&*cert).map_err(|_| Error::NotEnoughMemory)?;
Ok(register::Response::new(
0x05,
&cose_key,
Expand Down
172 changes: 115 additions & 57 deletions src/ctap2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
algorithms
.push(KnownPublicKeyCredentialParameters { alg: ED_DSA })
.unwrap();
#[cfg(feature = "mldsa44")]
algorithms
.push(KnownPublicKeyCredentialParameters {
alg: ctap_types::webauthn::ML_DSA_44,
})
.unwrap();
let algorithms = FilteredPublicKeyCredentialParameters(algorithms);

let remaining_discoverable_credentials = self.estimate_remaining();
Expand Down Expand Up @@ -272,8 +278,16 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti

// 7. check pubKeyCredParams algorithm is valid + supported COSE identifier

// CTAP §6.1.2: walk pubKeyCredParams in order and pick the first
// supported algorithm. The guard on every arm matters — without
// it later entries silently overwrite earlier ones and we end
// up with last-match instead of first-match (caught by the
// ML-DSA-44 vs EdDSA preference test).
let mut algorithm: Option<SigningAlgorithm> = None;
for param in parameters.pub_key_cred_params.0.iter() {
if algorithm.is_some() {
break;
}
match param.alg {
-7 =>
{
Expand All @@ -285,6 +299,12 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
-8 => {
algorithm = Some(SigningAlgorithm::Ed25519);
}
#[cfg(feature = "mldsa44")]
-50 => {
if algorithm.is_none() {
algorithm = Some(SigningAlgorithm::MlDsa44);
}
}
_ => {}
}
}
Expand Down Expand Up @@ -575,7 +595,7 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
};
// debug_now!("authData = {:?}", &authenticator_data);

let serialized_auth_data = authenticator_data.serialize()?;
let mut serialized_auth_data = authenticator_data.serialize()?;

// select attestation format or use packed attestation as default
let att_stmt_fmt = parameters
Expand All @@ -589,20 +609,28 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
Some(AttestationStatement::None(NoneAttestationStatement {}))
}
SupportedAttestationFormat::Packed => {
let mut commitment = Bytes::<1024>::new();
commitment
.extend_from_slice(&serialized_auth_data)
.map_err(|_| Error::Other)?;
commitment
// Build the "commitment" (auth_data ‖ cdh) IN PLACE inside
// `serialized_auth_data` to avoid a separate 2.3 KB local.
// With `mldsa44`, `SerializedAuthenticatorData` has 2048 B
// capacity, comfortably fitting the ~1577 B auth_data + 32 B
// cdh = ~1609 B. After signing we truncate to restore the
// original auth_data length so the buffer can be moved into
// `response.auth_data`.
let auth_data_len = serialized_auth_data.len();
serialized_auth_data
.extend_from_slice(parameters.client_data_hash)
.map_err(|_| Error::Other)?;

let (attestation_key, attestation_algorithm) = attestation_maybe
.as_ref()
.map(|attestation| (attestation.0, SigningAlgorithm::P256))
.unwrap_or((private_key, algorithm));
let signature =
attestation_algorithm.sign(&mut self.trussed, attestation_key, &commitment);
let signature = attestation_algorithm.sign(
&mut self.trussed,
attestation_key,
&serialized_auth_data,
);
serialized_auth_data.truncate(auth_data_len);
let packed = PackedAttestationStatement {
alg: attestation_algorithm.into(),
sig: Bytes::try_from(&*signature).map_err(|_| Error::Other)?,
Expand All @@ -626,6 +654,11 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
info_now!("deleted private credential key: {}", _success);
}

// Write fields directly into the caller-provided slot — avoids
// the 6 KB Response by-value return + move through the dispatch
// chain. `serialized_auth_data` still lives transiently on this
// function's stack (≈2 KB); future work could write it directly
// into `response.auth_data` via a mutable serialize sink.
response.fmt = att_stmt_fmt
.map(From::from)
.unwrap_or(AttestationStatementFormat::None);
Expand Down Expand Up @@ -2150,66 +2183,55 @@ impl<UP: UserPresence, T: TrussedRequirements> crate::Authenticator<UP, T> {
extensions: extensions_output,
};

let serialized_auth_data = authenticator_data.serialize()?;
let mut serialized_auth_data = authenticator_data.serialize()?;

let mut commitment = Bytes::<1024>::new();
commitment
.extend_from_slice(&serialized_auth_data)
.map_err(|_| Error::Other)?;
commitment
// Build commitment in place: append client_data_hash to serialized_auth_data,
// sign over the concatenation, then truncate back. Mirrors the elision
// done in make_credential — avoids a separate Bytes<1024> commitment buffer.
let auth_data_len = serialized_auth_data.len();
serialized_auth_data
.extend_from_slice(&data.client_data_hash)
.map_err(|_| Error::Other)?;

let signing_algorithm =
SigningAlgorithm::try_from(credential.algorithm()).map_err(|_| Error::Other)?;
let signature =
Bytes::try_from(&*signing_algorithm.sign(&mut self.trussed, key, &commitment)).unwrap();
let signature = Bytes::try_from(&*signing_algorithm.sign(
&mut self.trussed,
key,
&serialized_auth_data,
))
.unwrap();

// select preferred format or skip attestation statement
// select preferred format or skip attestation statement.
//
// The Packed branch's `PackedAttestationStatement` carries a
// `Bytes<MAX_PACKED_SIG_LENGTH>` sig (2436 B) and an x5c
// `Bytes<MAX_X5C_CERT_LENGTH>` cert (2052 B) — ~4.5 KB total
// with `mldsa44`. Outline the construction into a `#[inline(never)]`
// helper so those temporaries live in the helper's frame, not
// in `assert_with_credential`'s (which is preserved on the lower
// task's stack during the 72 KB libcrux_sign call above us).
let att_stmt_fmt = data
.attestation_formats_preference
.as_ref()
.and_then(SupportedAttestationFormat::select);
let att_stmt = if let Some(format) = att_stmt_fmt {
match format {
SupportedAttestationFormat::None => {
Some(AttestationStatement::None(NoneAttestationStatement {}))
}
SupportedAttestationFormat::Packed => {
let (attestation_maybe, _) = self.state.identity.attestation(&mut self.trussed);
let (signature, attestation_algorithm) = {
if let Some(attestation) = attestation_maybe.as_ref() {
let signing_algorithm = SigningAlgorithm::P256;
let signature = signing_algorithm.sign(
&mut self.trussed,
attestation.0,
&commitment,
);
(
Bytes::try_from(&*signature).map_err(|_| Error::Other)?,
signing_algorithm.into(),
)
} else {
(signature.clone(), credential.algorithm())
}
};
let packed = PackedAttestationStatement {
alg: attestation_algorithm,
sig: signature,
x5c: attestation_maybe.as_ref().map(|attestation| {
// See: https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation-cert-requirements
let cert = attestation.1.clone();
let mut x5c = Vec::new();
x5c.push(cert).ok();
x5c
}),
};
Some(AttestationStatement::Packed(packed))
}
match att_stmt_fmt {
Some(SupportedAttestationFormat::None) => {
response.att_stmt = Some(AttestationStatement::None(NoneAttestationStatement {}));
}
} else {
None
};
Some(SupportedAttestationFormat::Packed) => {
self.build_packed_att_stmt(
&serialized_auth_data,
&signature,
credential.algorithm(),
response,
)?;
}
None => {}
}

// Truncate back so the response carries only authData (without cdh).
serialized_auth_data.truncate(auth_data_len);

if !is_rk {
syscall!(self.trussed.delete(key));
Expand All @@ -2219,7 +2241,6 @@ impl<UP: UserPresence, T: TrussedRequirements> crate::Authenticator<UP, T> {
response.auth_data = serialized_auth_data;
response.signature = signature;
response.number_of_credentials = num_credentials;
response.att_stmt = att_stmt;

// User with empty IDs are ignored for compatibility
if is_rk {
Expand Down Expand Up @@ -2250,6 +2271,43 @@ impl<UP: UserPresence, T: TrussedRequirements> crate::Authenticator<UP, T> {
Ok(())
}

/// Build a `Packed` attestation statement for `get_assertion` and
/// write it into `response.att_stmt`. Outlined so its ~4.5 KB worth
/// of temporaries (`Bytes<MAX_PACKED_SIG_LENGTH>` re-sign output plus
/// the x5c cert clone) live here instead of inflating the caller's
/// preserved stack while libcrux_sign runs above us.
#[inline(never)]
fn build_packed_att_stmt(
&mut self,
message: &[u8],
fallback_sig: &Bytes<{ ctap_types::sizes::MAX_PACKED_SIG_LENGTH }>,
fallback_alg: i32,
response: &mut ctap2::get_assertion::Response,
) -> Result<()> {
let (attestation_maybe, _) = self.state.identity.attestation(&mut self.trussed);
let (sig, alg) = if let Some(attestation) = attestation_maybe.as_ref() {
let signing_algorithm = SigningAlgorithm::P256;
let att_sig = signing_algorithm.sign(&mut self.trussed, attestation.0, message);
(
Bytes::try_from(&*att_sig).map_err(|_| Error::Other)?,
signing_algorithm.into(),
)
} else {
(fallback_sig.clone(), fallback_alg)
};
response.att_stmt = Some(AttestationStatement::Packed(PackedAttestationStatement {
alg,
sig,
x5c: attestation_maybe.as_ref().map(|attestation| {
let cert = attestation.1.clone();
let mut x5c = Vec::new();
x5c.push(cert).ok();
x5c
}),
}));
Ok(())
}

#[inline(never)]
fn delete_resident_key_by_user_id(
&mut self,
Expand Down
7 changes: 7 additions & 0 deletions src/ctap2/credential_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,13 @@ where
SigningAlgorithm::Ed25519 => PublicKey::Ed25519Key(
ctap_types::serde::cbor_deserialize(&cose_public_key).unwrap(),
),
// `cosey::PublicKey` doesn't have an ML-DSA variant (yet); the
// credential itself works for GA, but `credentialManagement` can't
// serialise its public key via this path. Skip rather than crash —
// the platform will see `Err(InvalidCredential)` and can fall back
// to GA + signature verification to obtain the key.
#[cfg(feature = "mldsa44")]
SigningAlgorithm::MlDsa44 => return Err(Error::InvalidCredential),
};
let cred_protect = match credential.cred_protect {
Some(x) => Some(x),
Expand Down
6 changes: 3 additions & 3 deletions src/ctap2/pin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ impl SharedSecret {
}

#[must_use]
pub fn encrypt<T: CryptoClient>(&self, trussed: &mut T, data: &[u8]) -> Bytes<1024> {
pub fn encrypt<T: CryptoClient>(&self, trussed: &mut T, data: &[u8]) -> Message {
let key_id = self.aes_key_id();
let iv = self.generate_iv(trussed);
let mut ciphertext =
Expand All @@ -444,7 +444,7 @@ impl SharedSecret {
}

#[must_use]
fn wrap<T: CryptoClient>(&self, trussed: &mut T, key: KeyId) -> Bytes<1024> {
fn wrap<T: CryptoClient>(&self, trussed: &mut T, key: KeyId) -> Message {
let wrapping_key = self.aes_key_id();
let iv = self.generate_iv(trussed);
let mut wrapped_key = syscall!(trussed.wrap_key(
Expand All @@ -465,7 +465,7 @@ impl SharedSecret {
}

#[must_use]
pub fn decrypt<T: CryptoClient>(&self, trussed: &mut T, data: &[u8]) -> Option<Bytes<1024>> {
pub fn decrypt<T: CryptoClient>(&self, trussed: &mut T, data: &[u8]) -> Option<Message> {
let key_id = self.aes_key_id();
let (iv, data) = match self {
Self::V1 { .. } => (Default::default(), data),
Expand Down
Loading
Loading