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
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@ 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.1...HEAD
[Unreleased]: https://github.com/trussed-dev/ctap-types/compare/0.6.0-rc.2...HEAD

-

## [0.6.0-rc.2] 2026-05-27

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

- Rename `authenticator_config` to `config`.
- Add `platform-serde` feature for additional `Serialize` and `Deserialize` implementations not required by authenticators.
- Add `ctap2::client_pin::MAX_PIN_LENGTH` and `ctap2::config::{DEFAULT_MIN_PIN_LENGTH, MAX_RP_ID_LENGTH, MAX_SUBCOMMAND_PARAMS_CBOR_LEN}` constants.
- Add `min_pin_length` to `make_credential::{ExtensionsInput, ExtensionsOutput}`.

## [0.6.0-rc.1] 2026-05-21

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.1"
version = "0.6.0-rc.2"
authors = ["Nicolas Stalder <n@stalder.io>", "The Trussed developers"]
edition = "2021"
license = "Apache-2.0 OR MIT"
Expand Down
2 changes: 2 additions & 0 deletions src/arbitrary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ impl<'a> Arbitrary<'a> for ctap2::make_credential::ExtensionsInput<'a> {
let cred_protect = u.arbitrary()?;
let hmac_secret = u.arbitrary()?;
let large_blob_key = u.arbitrary()?;
let min_pin_length = u.arbitrary()?;
#[cfg(feature = "third-party-payment")]
let third_party_payment = u.arbitrary()?;
let cred_blob = if bool::arbitrary(u)? {
Expand All @@ -313,6 +314,7 @@ impl<'a> Arbitrary<'a> for ctap2::make_credential::ExtensionsInput<'a> {
cred_protect,
hmac_secret,
large_blob_key,
min_pin_length,
#[cfg(feature = "third-party-payment")]
third_party_payment,
cred_blob,
Expand Down
10 changes: 7 additions & 3 deletions src/ctap2/client_pin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,13 @@ bitflags! {
}
}

// minimum PIN length: 4 unicode
// maximum PIN length: UTF-8 represented by <= 63 bytes
// maximum consecutive incorrect PIN attempts: 8
// minimum PIN length: 4 unicode (see `config::DEFAULT_MIN_PIN_LENGTH`).
// maximum consecutive incorrect PIN attempts: 8.

/// CTAP 2.1 §6.5.1: "Maximum PIN Length: 63 bytes." Authenticators MUST
/// NOT accept PINs whose UTF-8 byte representation exceeds this. Applies
/// to `setPin` and `changePin`'s decrypted `newPinEnc`.
pub const MAX_PIN_LENGTH: usize = 63;

#[derive(Clone, Debug, Eq, PartialEq, SerializeIndexed, DeserializeIndexed)]
#[non_exhaustive]
Expand Down
38 changes: 38 additions & 0 deletions src/ctap2/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,28 @@ use crate::Vec;

pub const MAX_MIN_PIN_LENGTH_RP_IDS: usize = 4;

/// CTAP 2.1 §6.5.1: "Minimum PIN Length: 4 code points." Spec floor for
/// `minPINLength` — the authenticator MUST NOT accept a shorter PIN and
/// `setMinPINLength` MUST NOT lower the effective minimum below this.
pub const DEFAULT_MIN_PIN_LENGTH: u8 = 4;

/// A RP-ID is a DNS hostname, max 253 bytes.
pub const MAX_RP_ID_LENGTH: usize = 253;

/// Worst-case CBOR-encoded length of [`SubcommandParameters`].
///
/// Derivation:
/// - `a3` map(3) 1
/// - key 0x01 + `new_min_pin_length: u8` `01` + `18 xx` 3
/// - key 0x02 + `min_pin_length_rp_ids` array of N strings:
/// N = [`MAX_MIN_PIN_LENGTH_RP_IDS`]; text-hdr `78 LL` is 2 bytes for
/// lengths 24..=255. 1 (key) + 1 (arr hdr) + 4 × (2 + 253) = 1028
/// - key 0x03 + `force_change_pin: bool` `03` + `f4`/`f5` 2
///
/// Sum: 1 + 3 + 1028 + 2 = 1034. Rounded up to give a small safety
/// margin against future field additions.
pub const MAX_SUBCOMMAND_PARAMS_CBOR_LEN: usize = 1100;

#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize_repr, Deserialize_repr)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive]
Expand Down Expand Up @@ -176,4 +198,20 @@ mod tests {
],
);
}

#[test]
fn test_max_subcommand_params_cbor_len() {
let rp_id = "a".repeat(253);
let mut rp_ids = Vec::new();
rp_ids.resize(4, rp_id.as_str()).unwrap();
let params = SubcommandParameters {
new_min_pin_length: Some(u8::MAX),
min_pin_length_rp_ids: Some(rp_ids),
force_change_pin: Some(true),
};

let mut buffer = [0; MAX_SUBCOMMAND_PARAMS_CBOR_LEN];
let result = cbor_smol::cbor_serialize(&params, &mut buffer);
assert_eq!(result.err(), None);
}
}
10 changes: 10 additions & 0 deletions src/ctap2/make_credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ pub struct ExtensionsInput<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
pub large_blob_key: Option<bool>,

#[serde(rename = "minPinLength")]
#[serde(skip_serializing_if = "Option::is_none")]
pub min_pin_length: Option<bool>,

/// `hmac-secret-mc` (CTAP 2.2 §11.4.5 / WebAuthn L3): platform-supplied
/// hmac-secret request evaluated at MakeCredential time, returning
/// hmac-secret outputs alongside the freshly-minted credential.
Expand Down Expand Up @@ -84,6 +88,10 @@ pub struct ExtensionsOutput {
#[serde(skip_serializing_if = "Option::is_none")]
pub hmac_secret: Option<bool>,

#[serde(rename = "minPinLength")]
#[serde(skip_serializing_if = "Option::is_none")]
pub min_pin_length: Option<u8>,

/// `hmac-secret-mc` (CTAP 2.2): encrypted hmac-secret outputs produced at
/// MakeCredential time. Wire format mirrors GetAssertion's `hmac-secret`
/// output — `enc(output1)` or `enc(output1 || output2)`, up to 80 bytes.
Expand Down Expand Up @@ -239,6 +247,7 @@ mod tests {
cred_protect: Some(1),
hmac_secret: Some(true),
large_blob_key: Some(true),
min_pin_length: Some(true),
#[cfg(feature = "third-party-payment")]
third_party_payment: Some(true),
cred_blob: Some(serde_bytes::Bytes::new(b"1234")),
Expand All @@ -260,6 +269,7 @@ mod tests {
let extensions = ExtensionsOutput {
cred_protect: Some(1),
hmac_secret: Some(true),
min_pin_length: Some(6),
#[cfg(feature = "third-party-payment")]
third_party_payment: Some(true),
cred_blob: Some(true),
Expand Down
Loading