Skip to content
Merged
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
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

[Unreleased]: https://github.com/trussed-dev/ctap-types/compare/0.6.0-rc.3...HEAD
[Unreleased]: https://github.com/trussed-dev/ctap-types/compare/0.6.0-rc.4...HEAD

-
## [0.6.0-rc.4] 2026-06-01

[0.6.0-rc.4]: https://github.com/trussed-dev/ctap-types/compare/0.6.0-rc.3...0.6.0-rc.4

- Improve `Authenticator` trait to reduce stack usage.

## [0.6.0-rc.3] 2026-05-28

Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ctap-types"
version = "0.6.0-rc.3"
version = "0.6.0-rc.4"
authors = ["Nicolas Stalder <n@stalder.io>", "The Trussed developers"]
edition = "2021"
license = "Apache-2.0 OR MIT"
Expand Down
204 changes: 150 additions & 54 deletions src/ctap2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -508,14 +508,55 @@ pub enum Error {
/// [`Response`].
pub trait Authenticator {
fn get_info(&mut self) -> get_info::Response;

/// Build a `MakeCredential` response into the caller-supplied buffer.
///
/// Returns by-out-parameter instead of by-value because the response
/// is large (~6 KB with `mldsa44` enabled) and the CTAP dispatch chain
/// is several layers deep — every layer that carries a
/// `Result<Response>` reserves a Response-sized slot in its frame even
/// when NRVO would elide the final move. Writing into the caller's
/// slot keeps the chain flat (~6 KB total instead of ~6 KB per layer).
///
/// Same pattern as `std::io::Read::read_to_end(&mut self, buf: &mut Vec<u8>)`
/// / `BufRead::read_line(buf: &mut String)`. For callers that don't
/// care about the stack copy, [`Authenticator::make_credential`] is a
/// convenience by-value wrapper.
fn make_credential_into(
&mut self,
request: &make_credential::Request,
response: &mut make_credential::Response,
) -> Result<()>;

/// Convenience by-value wrapper around [`make_credential_into`] for
/// callers that don't need to avoid the stack copy.
fn make_credential(
&mut self,
request: &make_credential::Request,
) -> Result<make_credential::Response>;
) -> Result<make_credential::Response> {
let mut response = make_credential::Response::empty();
self.make_credential_into(request, &mut response)?;
Ok(response)
}

/// Build a `GetAssertion` response into the caller-supplied buffer.
/// See [`make_credential_into`] for the rationale.
fn get_assertion_into(
&mut self,
request: &get_assertion::Request,
response: &mut get_assertion::Response,
) -> Result<()>;

/// Convenience by-value wrapper around [`get_assertion_into`].
fn get_assertion(
&mut self,
request: &get_assertion::Request,
) -> Result<get_assertion::Response>;
) -> Result<get_assertion::Response> {
let mut response = get_assertion::Response::empty();
self.get_assertion_into(request, &mut response)?;
Ok(response)
}

fn get_next_assertion(&mut self) -> Result<get_assertion::Response>;
fn reset(&mut self) -> Result<()>;
fn client_pin(&mut self, request: &client_pin::Request) -> Result<client_pin::Response>;
Expand All @@ -537,120 +578,175 @@ pub trait Authenticator {
Err(Error::InvalidCommand)
}

/// Dispatches the enum of possible requests into the appropriate trait method.
/// Dispatches the enum of possible requests into the appropriate trait
/// method. Writes the result into `*response` in place, so the caller
/// owns the (~6 KB with `mldsa44`) Response storage and intermediate
/// frames don't carry duplicate copies. Saves ~6 KB per chain layer
/// vs the previous by-value-return shape.
///
/// Each match arm that constructs a non-trivial Response variant is
/// outlined into its own `#[inline(never)]` helper below. Without that,
/// LLVM reserves a separate stack region for *every* variant's temporary
/// in `call_ctap2`'s frame (measured ~29 KB before outlining); with it,
/// only the currently-active arm's helper frame is live, shrinking the
/// shared dispatch frame to a small jump table.
#[inline(never)]
fn call_ctap2(&mut self, request: &Request) -> Result<Response> {
fn call_ctap2(&mut self, request: &Request, response: &mut Response) -> Result<()> {
match request {
// 0x4
Request::GetInfo => {
debug_now!("CTAP2.GI");
Ok(Response::GetInfo(self.get_info()))
self.dispatch_get_info(response)
}

// 0x2
Request::MakeCredential(request) => {
debug_now!("CTAP2.MC");
Ok(Response::MakeCredential(
self.make_credential(request).inspect_err(|_e| {
debug!("error: {:?}", _e);
})?,
))
self.dispatch_make_credential(request, response)
}

// 0x1
Request::GetAssertion(request) => {
debug_now!("CTAP2.GA");
Ok(Response::GetAssertion(
self.get_assertion(request).inspect_err(|_e| {
debug!("error: {:?}", _e);
})?,
))
self.dispatch_get_assertion(request, response)
}

// 0x8
Request::GetNextAssertion => {
debug_now!("CTAP2.GNA");
Ok(Response::GetNextAssertion(
self.get_next_assertion().inspect_err(|_e| {
debug!("error: {:?}", _e);
})?,
))
self.dispatch_get_next_assertion(response)
}

// 0x7
Request::Reset => {
debug_now!("CTAP2.RST");
self.reset().inspect_err(|_e| {
debug!("error: {:?}", _e);
})?;
Ok(Response::Reset)
*response = Response::Reset;
Ok(())
}

// 0x6
Request::ClientPin(request) => {
debug_now!("CTAP2.PIN");
Ok(Response::ClientPin(self.client_pin(request).inspect_err(
|_e| {
debug!("error: {:?}", _e);
},
)?))
self.dispatch_client_pin(request, response)
}

// 0xA
Request::CredentialManagement(request) => {
debug_now!("CTAP2.CM");
Ok(Response::CredentialManagement(
self.credential_management(request).inspect_err(|_e| {
debug!("error: {:?}", _e);
})?,
))
self.dispatch_credential_management(request, response)
}

// 0xB
Request::Selection => {
debug_now!("CTAP2.SEL");
self.selection().inspect_err(|_e| {
debug!("error: {:?}", _e);
})?;
Ok(Response::Selection)
*response = Response::Selection;
Ok(())
}

// 0xC
Request::LargeBlobs(request) => {
debug_now!("CTAP2.LB");
Ok(Response::LargeBlobs(
self.large_blobs(request).inspect_err(|_e| {
debug!("error: {:?}", _e);
})?,
))
self.dispatch_large_blobs(request, response)
}

// 0xD
Request::Config(request) => {
debug_now!("CTAP2.CFG");
self.config(request).inspect_err(|_e| {
debug!("error: {:?}", _e);
})?;
Ok(Response::Config)
*response = Response::Config;
Ok(())
}

// Not stable
Request::Vendor(op) => {
debug_now!("CTAP2.V");
self.vendor(*op).inspect_err(|_e| {
debug!("error: {:?}", _e);
})?;
Ok(Response::Vendor)
*response = Response::Vendor;
Ok(())
}
}
}
}

impl<A: Authenticator> crate::Rpc<Error, Request<'_>, Response> for A {
/// Dispatches the enum of possible requests into the appropriate trait method.
#[inline(never)]
fn call(&mut self, request: &Request) -> Result<Response> {
self.call_ctap2(request)
fn dispatch_get_info(&mut self, response: &mut Response) -> Result<()> {
*response = Response::GetInfo(self.get_info());
Ok(())
}

#[inline(never)]
fn dispatch_make_credential(
&mut self,
request: &make_credential::Request,
response: &mut Response,
) -> Result<()> {
*response = Response::MakeCredential(make_credential::Response::empty());
let Response::MakeCredential(inner) = response else {
unreachable!()
};
self.make_credential_into(request, inner).inspect_err(|_e| {
debug!("error: {:?}", _e);
})
}

#[inline(never)]
fn dispatch_get_assertion(
&mut self,
request: &get_assertion::Request,
response: &mut Response,
) -> Result<()> {
*response = Response::GetAssertion(get_assertion::Response::empty());
let Response::GetAssertion(inner) = response else {
unreachable!()
};
self.get_assertion_into(request, inner).inspect_err(|_e| {
debug!("error: {:?}", _e);
})
}

#[inline(never)]
fn dispatch_get_next_assertion(&mut self, response: &mut Response) -> Result<()> {
*response = Response::GetNextAssertion(self.get_next_assertion().inspect_err(|_e| {
debug!("error: {:?}", _e);
})?);
Ok(())
}

#[inline(never)]
fn dispatch_client_pin(
&mut self,
request: &client_pin::Request,
response: &mut Response,
) -> Result<()> {
*response = Response::ClientPin(self.client_pin(request).inspect_err(|_e| {
debug!("error: {:?}", _e);
})?);
Ok(())
}

#[inline(never)]
fn dispatch_credential_management(
&mut self,
request: &credential_management::Request,
response: &mut Response,
) -> Result<()> {
*response = Response::CredentialManagement(
self.credential_management(request).inspect_err(|_e| {
debug!("error: {:?}", _e);
})?,
);
Ok(())
}

#[inline(never)]
fn dispatch_large_blobs(
&mut self,
request: &large_blobs::Request,
response: &mut Response,
) -> Result<()> {
*response = Response::LargeBlobs(self.large_blobs(request).inspect_err(|_e| {
debug!("error: {:?}", _e);
})?);
Ok(())
}
}

Expand Down
20 changes: 19 additions & 1 deletion src/ctap2/get_assertion.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{Bytes, Vec};
use crate::{Bytes, String, Vec};
use cosey::EcdhEsHkdf256PublicKey;
use serde::{Deserialize, Serialize};
use serde_bytes::ByteArray;
Expand Down Expand Up @@ -179,6 +179,24 @@ impl ResponseBuilder {
}
}

impl Response {
/// Empty `Response` with default fields. Used by `Authenticator::call_ctap2`
/// to preallocate the `Response::GetAssertion` variant slot so the inner
/// `get_assertion` impl can write via `&mut` — same shape as
/// `make_credential::Response::empty()`.
pub fn empty() -> Self {
ResponseBuilder {
credential: PublicKeyCredentialDescriptor {
id: Bytes::new(),
key_type: String::new(),
},
auth_data: Bytes::new(),
signature: Bytes::new(),
}
.build()
}
}

#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[non_exhaustive]
pub struct UnsignedExtensionOutputs {}
Expand Down
18 changes: 18 additions & 0 deletions src/ctap2/make_credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,24 @@ impl ResponseBuilder {
}
}

impl Response {
/// Empty `Response` with default fields. Used by `Authenticator::call_ctap2`
/// to preallocate the `Response::MakeCredential` variant slot in the
/// outer `ctap2::Response` so the inner `make_credential` impl can write
/// via `&mut` — saves the ~6 KB return-by-value copy through the
/// dispatch chain. Inner is sized to the type's full capacity
/// (`auth_data` is `Bytes<AUTHENTICATOR_DATA_LENGTH>` ≈ 2 KB with
/// `mldsa44`); the slot is allocated either way, the win is that it
/// lives in ONE place instead of three.
pub fn empty() -> Self {
ResponseBuilder {
fmt: AttestationStatementFormat::None,
auth_data: super::SerializedAuthenticatorData::new(),
}
.build()
}
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "platform-serde", derive(Deserialize))]
#[non_exhaustive]
Expand Down
Loading