diff --git a/CHANGELOG.md b/CHANGELOG.md index 725e0bb..c43e3a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.toml b/Cargo.toml index c9149b8..7e7d514 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ctap-types" -version = "0.6.0-rc.1" +version = "0.6.0-rc.2" authors = ["Nicolas Stalder ", "The Trussed developers"] edition = "2021" license = "Apache-2.0 OR MIT" diff --git a/src/arbitrary.rs b/src/arbitrary.rs index 25e6561..9549ed7 100644 --- a/src/arbitrary.rs +++ b/src/arbitrary.rs @@ -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)? { @@ -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, diff --git a/src/ctap2/client_pin.rs b/src/ctap2/client_pin.rs index 36e226d..d77e932 100644 --- a/src/ctap2/client_pin.rs +++ b/src/ctap2/client_pin.rs @@ -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] diff --git a/src/ctap2/config.rs b/src/ctap2/config.rs index a4e96e3..adda550 100644 --- a/src/ctap2/config.rs +++ b/src/ctap2/config.rs @@ -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] @@ -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(¶ms, &mut buffer); + assert_eq!(result.err(), None); + } } diff --git a/src/ctap2/make_credential.rs b/src/ctap2/make_credential.rs index f6aac4f..cd4ea6b 100644 --- a/src/ctap2/make_credential.rs +++ b/src/ctap2/make_credential.rs @@ -51,6 +51,10 @@ pub struct ExtensionsInput<'a> { #[serde(skip_serializing_if = "Option::is_none")] pub large_blob_key: Option, + #[serde(rename = "minPinLength")] + #[serde(skip_serializing_if = "Option::is_none")] + pub min_pin_length: Option, + /// `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. @@ -84,6 +88,10 @@ pub struct ExtensionsOutput { #[serde(skip_serializing_if = "Option::is_none")] pub hmac_secret: Option, + #[serde(rename = "minPinLength")] + #[serde(skip_serializing_if = "Option::is_none")] + pub min_pin_length: Option, + /// `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. @@ -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")), @@ -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),