Skip to content

Commit a8a73be

Browse files
0x0ecerobin-nitrokey
authored andcommitted
ctap2: take &mut Response in trait + outline call_ctap2 arms to reduce stack by 40kB
1 parent 50a9caf commit a8a73be

4 files changed

Lines changed: 188 additions & 56 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

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

11-
-
11+
- Improve `Authenticator` trait to reduce stack usage.
1212

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

src/ctap2.rs

Lines changed: 150 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -508,14 +508,55 @@ pub enum Error {
508508
/// [`Response`].
509509
pub trait Authenticator {
510510
fn get_info(&mut self) -> get_info::Response;
511+
512+
/// Build a `MakeCredential` response into the caller-supplied buffer.
513+
///
514+
/// Returns by-out-parameter instead of by-value because the response
515+
/// is large (~6 KB with `mldsa44` enabled) and the CTAP dispatch chain
516+
/// is several layers deep — every layer that carries a
517+
/// `Result<Response>` reserves a Response-sized slot in its frame even
518+
/// when NRVO would elide the final move. Writing into the caller's
519+
/// slot keeps the chain flat (~6 KB total instead of ~6 KB per layer).
520+
///
521+
/// Same pattern as `std::io::Read::read_to_end(&mut self, buf: &mut Vec<u8>)`
522+
/// / `BufRead::read_line(buf: &mut String)`. For callers that don't
523+
/// care about the stack copy, [`Authenticator::make_credential`] is a
524+
/// convenience by-value wrapper.
525+
fn make_credential_into(
526+
&mut self,
527+
request: &make_credential::Request,
528+
response: &mut make_credential::Response,
529+
) -> Result<()>;
530+
531+
/// Convenience by-value wrapper around [`make_credential_into`] for
532+
/// callers that don't need to avoid the stack copy.
511533
fn make_credential(
512534
&mut self,
513535
request: &make_credential::Request,
514-
) -> Result<make_credential::Response>;
536+
) -> Result<make_credential::Response> {
537+
let mut response = make_credential::Response::empty();
538+
self.make_credential_into(request, &mut response)?;
539+
Ok(response)
540+
}
541+
542+
/// Build a `GetAssertion` response into the caller-supplied buffer.
543+
/// See [`make_credential_into`] for the rationale.
544+
fn get_assertion_into(
545+
&mut self,
546+
request: &get_assertion::Request,
547+
response: &mut get_assertion::Response,
548+
) -> Result<()>;
549+
550+
/// Convenience by-value wrapper around [`get_assertion_into`].
515551
fn get_assertion(
516552
&mut self,
517553
request: &get_assertion::Request,
518-
) -> Result<get_assertion::Response>;
554+
) -> Result<get_assertion::Response> {
555+
let mut response = get_assertion::Response::empty();
556+
self.get_assertion_into(request, &mut response)?;
557+
Ok(response)
558+
}
559+
519560
fn get_next_assertion(&mut self) -> Result<get_assertion::Response>;
520561
fn reset(&mut self) -> Result<()>;
521562
fn client_pin(&mut self, request: &client_pin::Request) -> Result<client_pin::Response>;
@@ -537,120 +578,175 @@ pub trait Authenticator {
537578
Err(Error::InvalidCommand)
538579
}
539580

540-
/// Dispatches the enum of possible requests into the appropriate trait method.
581+
/// Dispatches the enum of possible requests into the appropriate trait
582+
/// method. Writes the result into `*response` in place, so the caller
583+
/// owns the (~6 KB with `mldsa44`) Response storage and intermediate
584+
/// frames don't carry duplicate copies. Saves ~6 KB per chain layer
585+
/// vs the previous by-value-return shape.
586+
///
587+
/// Each match arm that constructs a non-trivial Response variant is
588+
/// outlined into its own `#[inline(never)]` helper below. Without that,
589+
/// LLVM reserves a separate stack region for *every* variant's temporary
590+
/// in `call_ctap2`'s frame (measured ~29 KB before outlining); with it,
591+
/// only the currently-active arm's helper frame is live, shrinking the
592+
/// shared dispatch frame to a small jump table.
541593
#[inline(never)]
542-
fn call_ctap2(&mut self, request: &Request) -> Result<Response> {
594+
fn call_ctap2(&mut self, request: &Request, response: &mut Response) -> Result<()> {
543595
match request {
544596
// 0x4
545597
Request::GetInfo => {
546598
debug_now!("CTAP2.GI");
547-
Ok(Response::GetInfo(self.get_info()))
599+
self.dispatch_get_info(response)
548600
}
549-
550601
// 0x2
551602
Request::MakeCredential(request) => {
552603
debug_now!("CTAP2.MC");
553-
Ok(Response::MakeCredential(
554-
self.make_credential(request).inspect_err(|_e| {
555-
debug!("error: {:?}", _e);
556-
})?,
557-
))
604+
self.dispatch_make_credential(request, response)
558605
}
559-
560606
// 0x1
561607
Request::GetAssertion(request) => {
562608
debug_now!("CTAP2.GA");
563-
Ok(Response::GetAssertion(
564-
self.get_assertion(request).inspect_err(|_e| {
565-
debug!("error: {:?}", _e);
566-
})?,
567-
))
609+
self.dispatch_get_assertion(request, response)
568610
}
569-
570611
// 0x8
571612
Request::GetNextAssertion => {
572613
debug_now!("CTAP2.GNA");
573-
Ok(Response::GetNextAssertion(
574-
self.get_next_assertion().inspect_err(|_e| {
575-
debug!("error: {:?}", _e);
576-
})?,
577-
))
614+
self.dispatch_get_next_assertion(response)
578615
}
579-
580616
// 0x7
581617
Request::Reset => {
582618
debug_now!("CTAP2.RST");
583619
self.reset().inspect_err(|_e| {
584620
debug!("error: {:?}", _e);
585621
})?;
586-
Ok(Response::Reset)
622+
*response = Response::Reset;
623+
Ok(())
587624
}
588-
589625
// 0x6
590626
Request::ClientPin(request) => {
591627
debug_now!("CTAP2.PIN");
592-
Ok(Response::ClientPin(self.client_pin(request).inspect_err(
593-
|_e| {
594-
debug!("error: {:?}", _e);
595-
},
596-
)?))
628+
self.dispatch_client_pin(request, response)
597629
}
598-
599630
// 0xA
600631
Request::CredentialManagement(request) => {
601632
debug_now!("CTAP2.CM");
602-
Ok(Response::CredentialManagement(
603-
self.credential_management(request).inspect_err(|_e| {
604-
debug!("error: {:?}", _e);
605-
})?,
606-
))
633+
self.dispatch_credential_management(request, response)
607634
}
608-
609635
// 0xB
610636
Request::Selection => {
611637
debug_now!("CTAP2.SEL");
612638
self.selection().inspect_err(|_e| {
613639
debug!("error: {:?}", _e);
614640
})?;
615-
Ok(Response::Selection)
641+
*response = Response::Selection;
642+
Ok(())
616643
}
617-
618644
// 0xC
619645
Request::LargeBlobs(request) => {
620646
debug_now!("CTAP2.LB");
621-
Ok(Response::LargeBlobs(
622-
self.large_blobs(request).inspect_err(|_e| {
623-
debug!("error: {:?}", _e);
624-
})?,
625-
))
647+
self.dispatch_large_blobs(request, response)
626648
}
627-
628649
// 0xD
629650
Request::Config(request) => {
630651
debug_now!("CTAP2.CFG");
631652
self.config(request).inspect_err(|_e| {
632653
debug!("error: {:?}", _e);
633654
})?;
634-
Ok(Response::Config)
655+
*response = Response::Config;
656+
Ok(())
635657
}
636-
637658
// Not stable
638659
Request::Vendor(op) => {
639660
debug_now!("CTAP2.V");
640661
self.vendor(*op).inspect_err(|_e| {
641662
debug!("error: {:?}", _e);
642663
})?;
643-
Ok(Response::Vendor)
664+
*response = Response::Vendor;
665+
Ok(())
644666
}
645667
}
646668
}
647-
}
648669

649-
impl<A: Authenticator> crate::Rpc<Error, Request<'_>, Response> for A {
650-
/// Dispatches the enum of possible requests into the appropriate trait method.
651670
#[inline(never)]
652-
fn call(&mut self, request: &Request) -> Result<Response> {
653-
self.call_ctap2(request)
671+
fn dispatch_get_info(&mut self, response: &mut Response) -> Result<()> {
672+
*response = Response::GetInfo(self.get_info());
673+
Ok(())
674+
}
675+
676+
#[inline(never)]
677+
fn dispatch_make_credential(
678+
&mut self,
679+
request: &make_credential::Request,
680+
response: &mut Response,
681+
) -> Result<()> {
682+
*response = Response::MakeCredential(make_credential::Response::empty());
683+
let Response::MakeCredential(inner) = response else {
684+
unreachable!()
685+
};
686+
self.make_credential_into(request, inner).inspect_err(|_e| {
687+
debug!("error: {:?}", _e);
688+
})
689+
}
690+
691+
#[inline(never)]
692+
fn dispatch_get_assertion(
693+
&mut self,
694+
request: &get_assertion::Request,
695+
response: &mut Response,
696+
) -> Result<()> {
697+
*response = Response::GetAssertion(get_assertion::Response::empty());
698+
let Response::GetAssertion(inner) = response else {
699+
unreachable!()
700+
};
701+
self.get_assertion_into(request, inner).inspect_err(|_e| {
702+
debug!("error: {:?}", _e);
703+
})
704+
}
705+
706+
#[inline(never)]
707+
fn dispatch_get_next_assertion(&mut self, response: &mut Response) -> Result<()> {
708+
*response = Response::GetNextAssertion(self.get_next_assertion().inspect_err(|_e| {
709+
debug!("error: {:?}", _e);
710+
})?);
711+
Ok(())
712+
}
713+
714+
#[inline(never)]
715+
fn dispatch_client_pin(
716+
&mut self,
717+
request: &client_pin::Request,
718+
response: &mut Response,
719+
) -> Result<()> {
720+
*response = Response::ClientPin(self.client_pin(request).inspect_err(|_e| {
721+
debug!("error: {:?}", _e);
722+
})?);
723+
Ok(())
724+
}
725+
726+
#[inline(never)]
727+
fn dispatch_credential_management(
728+
&mut self,
729+
request: &credential_management::Request,
730+
response: &mut Response,
731+
) -> Result<()> {
732+
*response = Response::CredentialManagement(
733+
self.credential_management(request).inspect_err(|_e| {
734+
debug!("error: {:?}", _e);
735+
})?,
736+
);
737+
Ok(())
738+
}
739+
740+
#[inline(never)]
741+
fn dispatch_large_blobs(
742+
&mut self,
743+
request: &large_blobs::Request,
744+
response: &mut Response,
745+
) -> Result<()> {
746+
*response = Response::LargeBlobs(self.large_blobs(request).inspect_err(|_e| {
747+
debug!("error: {:?}", _e);
748+
})?);
749+
Ok(())
654750
}
655751
}
656752

src/ctap2/get_assertion.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{Bytes, Vec};
1+
use crate::{Bytes, String, Vec};
22
use cosey::EcdhEsHkdf256PublicKey;
33
use serde::{Deserialize, Serialize};
44
use serde_bytes::ByteArray;
@@ -179,6 +179,24 @@ impl ResponseBuilder {
179179
}
180180
}
181181

182+
impl Response {
183+
/// Empty `Response` with default fields. Used by `Authenticator::call_ctap2`
184+
/// to preallocate the `Response::GetAssertion` variant slot so the inner
185+
/// `get_assertion` impl can write via `&mut` — same shape as
186+
/// `make_credential::Response::empty()`.
187+
pub fn empty() -> Self {
188+
ResponseBuilder {
189+
credential: PublicKeyCredentialDescriptor {
190+
id: Bytes::new(),
191+
key_type: String::new(),
192+
},
193+
auth_data: Bytes::new(),
194+
signature: Bytes::new(),
195+
}
196+
.build()
197+
}
198+
}
199+
182200
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
183201
#[non_exhaustive]
184202
pub struct UnsignedExtensionOutputs {}

src/ctap2/make_credential.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,24 @@ impl ResponseBuilder {
207207
}
208208
}
209209

210+
impl Response {
211+
/// Empty `Response` with default fields. Used by `Authenticator::call_ctap2`
212+
/// to preallocate the `Response::MakeCredential` variant slot in the
213+
/// outer `ctap2::Response` so the inner `make_credential` impl can write
214+
/// via `&mut` — saves the ~6 KB return-by-value copy through the
215+
/// dispatch chain. Inner is sized to the type's full capacity
216+
/// (`auth_data` is `Bytes<AUTHENTICATOR_DATA_LENGTH>` ≈ 2 KB with
217+
/// `mldsa44`); the slot is allocated either way, the win is that it
218+
/// lives in ONE place instead of three.
219+
pub fn empty() -> Self {
220+
ResponseBuilder {
221+
fmt: AttestationStatementFormat::None,
222+
auth_data: super::SerializedAuthenticatorData::new(),
223+
}
224+
.build()
225+
}
226+
}
227+
210228
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
211229
#[cfg_attr(feature = "platform-serde", derive(Deserialize))]
212230
#[non_exhaustive]

0 commit comments

Comments
 (0)