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

## Unreleased

-
- Add `long_touch_for_reset` to `Config` and, if set, implement the `enableLongTouchForReset` subcommand for `config` and always require a long touch for a reset.
- Add `fido2_up_timeout` to `Config`.

## [v0.4.0-rc.3](https://github.com/trussed-dev/fido-authenticator/releases/tag/v0.4.0-rc.3) (2026-06-01)

Expand Down
2 changes: 2 additions & 0 deletions fuzz/fuzz_targets/ctap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ fuzz_target!(|requests: Vec<Request<'_>>| {
ccid_transport: false,
firmware_version: Some(0.into()),
credential_id_version: None,
long_touch_for_reset: true,
fido2_up_timeout: None,
},
);

Expand Down
57 changes: 43 additions & 14 deletions src/ctap2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use trussed_core::{
};

use crate::{
constants::{self, MAX_RESIDENT_CREDENTIALS_GUESSTIMATE},
constants::MAX_RESIDENT_CREDENTIALS_GUESSTIMATE,
credential::{self, Credential, FullCredential, Key, StrippedCredential},
format_hex, state, Result, SigningAlgorithm, TrussedRequirements, UserPresence,
};
Expand Down Expand Up @@ -163,13 +163,18 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
response.min_pin_length = Some(self.state.persistent.min_pin_length());
response.force_pin_change = Some(self.state.persistent.force_pin_change());
response.max_rpids_for_set_min_pin_length = Some(MAX_MIN_PIN_LENGTH_RP_IDS);
response.long_touch_for_reset = Some(self.config.long_touch_for_reset);
response.attestation_formats = Some(attestation_formats);
// CTAP 2.3 §6.4 0x1F: supported authenticatorConfig sub-command IDs.
// 0x02 toggleAlwaysUv (CTAP 2.1 §6.11.2)
// 0x03 setMinPINLength (CTAP 2.1 §6.11.3)
// 0x02 toggleAlwaysUv (CTAP 2.3 §6.11.2)
// 0x03 setMinPINLength (CTAP 2.3 §6.11.4)
// 0x04 enableLongTouchForReset (CTAP 2.3 §6.11.5)
let mut cfg_cmds = Vec::new();
cfg_cmds.push(0x02).unwrap();
cfg_cmds.push(0x03).unwrap();
if self.config.long_touch_for_reset {
cfg_cmds.push(0x04).unwrap();
}
response.authenticator_config_commands = Some(cfg_cmds);
response
}
Expand Down Expand Up @@ -216,6 +221,10 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
parameters: &ctap2::make_credential::Request,
response: &mut ctap2::make_credential::Response,
) -> Result<()> {
// CTAP 2.1 §6.1.1.2: rp.id must be present and non-empty.
if parameters.rp.id.is_empty() {
return Err(Error::MissingParameter);
}
let rp_id_hash = self.hash(parameters.rp.id.as_ref());

// 1-4.
Expand Down Expand Up @@ -254,7 +263,7 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
{
info_now!("Excluded!");
self.up
.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?;
.user_present(&mut self.trussed, self.config.fido2_up_timeout())?;
return Err(Error::CredentialExcluded);
}
}
Expand Down Expand Up @@ -382,7 +391,7 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti

// 10. get UP, if denied error OperationDenied
self.up
.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?;
.user_present(&mut self.trussed, self.config.fido2_up_timeout())?;

// 11. generate credential keypair
let location = match rk_requested {
Expand Down Expand Up @@ -635,11 +644,18 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
#[cfg(not(feature = "disable-reset-time-window"))]
return Err(Error::NotAllowed);
}
// 2. check for user presence
// denied -> OperationDenied
// timeout -> UserActionTimeout
self.up
.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?;
// 2. check for user presence (denied -> OperationDenied, timeout ->
// UserActionTimeout). Short touch by default (pre-2.3 behavior);
// when `long_touch_for_reset` is enabled we require a continuous
// ≥5 s "long touch" (CTAP 2.3 §6.6 / §7.7) via `Level::Strong`.
// The button/hardware backend is untouched.
if self.config.long_touch_for_reset {
self.up
.user_present_strong(&mut self.trussed, self.config.fido2_up_timeout())?;
} else {
self.up
.user_present(&mut self.trussed, self.config.fido2_up_timeout())?;
}

// Delete resident keys
syscall!(self.trussed.delete_all(Location::Internal));
Expand All @@ -663,7 +679,7 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti

fn selection(&mut self) -> Result<()> {
self.up
.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)
.user_present(&mut self.trussed, self.config.fido2_up_timeout())
}

// https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#authenticatorConfig
Expand All @@ -682,10 +698,10 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti

// 2. If the authenticator does not support the subcommand being
// invoked, per subCommand's value, return CTAP1_ERR_INVALID_PARAMETER.
// EnableLongTouchForReset lands with the long-touch reset commit.
// EnterpriseAttestation / VendorPrototype are not supported.
match request.sub_command {
Subcommand::SetMinPINLength | Subcommand::ToggleAlwaysUv => {}
Subcommand::EnableLongTouchForReset if self.config.long_touch_for_reset => {}
_ => return Err(Error::InvalidParameter),
}

Expand Down Expand Up @@ -762,6 +778,15 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
match request.sub_command {
Subcommand::SetMinPINLength => self.config_set_min_pin_length(request),
Subcommand::ToggleAlwaysUv => self.state.persistent.toggle_always_uv(&mut self.trussed),
// CTAP 2.3 §6.11.5: governed by the `long_touch_for_reset` config
// option. Acknowledge when enabled; otherwise it is unavailable.
Subcommand::EnableLongTouchForReset => {
if self.config.long_touch_for_reset {
Ok(())
} else {
Err(Error::InvalidParameter)
}
}
// Step 2 filtered every other variant. `Subcommand` is
// `#[non_exhaustive]` so the catch-all is still required.
_ => Err(Error::InvalidParameter),
Expand Down Expand Up @@ -1214,6 +1239,10 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
) -> Result<()> {
debug_now!("remaining stack size: {} bytes", msp() - 0x2000_0000);

// CTAP 2.1 §6.2.1.2: rpId must be present and non-empty.
if parameters.rp_id.is_empty() {
return Err(Error::MissingParameter);
}
let rp_id_hash = self.hash(parameters.rp_id.as_ref());

// 1-4.
Expand Down Expand Up @@ -1278,7 +1307,7 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
if !self.skip_up_check() {
info_now!("asking for up");
self.up
.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?;
.user_present(&mut self.trussed, self.config.fido2_up_timeout())?;
}
true
} else {
Expand Down Expand Up @@ -1843,7 +1872,7 @@ impl<UP: UserPresence, T: TrussedRequirements> crate::Authenticator<UP, T> {
if let Some(pin_auth) = pin_auth {
if pin_auth.is_empty() {
self.up
.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?;
.user_present(&mut self.trussed, self.config.fido2_up_timeout())?;
if !self.state.persistent.pin_is_set() {
return Err(Error::PinNotSet);
} else {
Expand Down
48 changes: 48 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,20 @@ pub struct Config {
/// To avoid invalidating existing credentials, this value is only used if the state is clean,
/// i. e. on the first start or after a reset. Otherwise, `V1` is used.
pub credential_id_version: Option<credential::CredentialIdVersion>,
/// Whether `authenticatorReset` requires a long touch (a continuous ≥5 s
/// press) rather than a normal short touch (CTAP 2.3 §6.6 / §7.7).
///
/// Recommended value: `true`. When `true`, reset requires
/// [`UserPresence::user_present_strong`] and `authenticatorGetInfo`
/// advertises `longTouchForReset = true`. A runner whose button backend
/// cannot detect a long press must explicitly opt out by setting this to
/// `false`, which restores the normal short-touch presence check. The
/// button/hardware backend is untouched either way.
pub long_touch_for_reset: bool,
/// The timeout for user presence requests in FIDO2/CTAP2 in milliseconds.
///
/// The default is [`constants::FIDO2_UP_TIMEOUT`][]. The spec requires at least 10 s.
pub fido2_up_timeout: Option<u32>,
}

impl Config {
Expand All @@ -152,12 +166,18 @@ impl Config {
ccid_transport: false,
firmware_version: None,
credential_id_version: None,
long_touch_for_reset: false,
fido2_up_timeout: None,
}
}

pub fn supports_large_blobs(&self) -> bool {
self.large_blobs.is_some()
}

pub fn fido2_up_timeout(&self) -> u32 {
self.fido2_up_timeout.unwrap_or(constants::FIDO2_UP_TIMEOUT)
}
}

/// This struct makes it possible to define the firmware version based on the credential ID format
Expand Down Expand Up @@ -353,6 +373,18 @@ pub trait UserPresence: Copy {
trussed: &mut T,
timeout_milliseconds: u32,
) -> Result<()>;

/// Strong user-presence check (CTAP 2.3 §7.7 long-touch reset). Default
/// falls back to a normal user-presence check; runners that can detect a
/// continuous ≥5 s touch should override this and ask trussed for
/// `consent::Level::Strong`.
fn user_present_strong<T: TrussedRequirements>(
self,
trussed: &mut T,
timeout_milliseconds: u32,
) -> Result<()> {
self.user_present(trussed, timeout_milliseconds)
}
}

#[deprecated(note = "use `Silent` directly`")]
Expand Down Expand Up @@ -390,6 +422,22 @@ impl UserPresence for Conforming {
_ => Error::OperationDenied,
})
}

fn user_present_strong<T: TrussedRequirements>(
self,
trussed: &mut T,
timeout_milliseconds: u32,
) -> Result<()> {
use trussed_core::types::consent::Level;
let result =
syscall!(trussed.confirm_user_present_with_level(Level::Strong, timeout_milliseconds))
.result;
result.map_err(|err| match err {
trussed_core::types::consent::Error::TimedOut => Error::UserActionTimeout,
trussed_core::types::consent::Error::Interrupted => Error::KeepaliveCancel,
_ => Error::OperationDenied,
})
}
}

impl<UP, T> Authenticator<UP, T>
Expand Down
Loading
Loading