diff --git a/CHANGELOG.md b/CHANGELOG.md index ce2a57f..4ab2f0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,23 @@ 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.4...HEAD +[Unreleased]: https://github.com/trussed-dev/ctap-types/compare/0.6.0-rc.5...HEAD + +- + +## [0.6.0-rc.5] 2026-06-08 + +[0.6.0-rc.5]: https://github.com/trussed-dev/ctap-types/compare/0.6.0-rc.4...0.6.0-rc.5 + +- Remove unused `Rpc` trait. +- Remove `cbor-smol` re-export as `serde`. +- Make `ctap2::CtapMappingError` private. +- Set `#[repr(u8)]` for `ctap2::Error` and implement `From` for `u8`. +- `ctap2::get_info`: Use `ByteArray<16>` instead of `Bytes<16>` for AAGUID. +- Remove re-exports for `heapless` and `heapless-bytes` as the relevant types are already re-exported. +- `ctap2::get_info`: Use subcommand enum for `authenticator_config_commands`. +- `webauthn`: Make `KnownPublicKeyCredentialParameters` an enum. +- `authenticator`: Move `Authenticator`, `Request` and `Response` to the crate root. ## [0.6.0-rc.4] 2026-06-01 diff --git a/Cargo.toml b/Cargo.toml index ddae0a6..862e2e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ctap-types" -version = "0.6.0-rc.4" +version = "0.6.0-rc.5" authors = ["Nicolas Stalder ", "The Trussed developers"] edition = "2021" license = "Apache-2.0 OR MIT" @@ -27,6 +27,7 @@ ciborium = "0.2" hex = "0.4" hex-literal = "0.4.1" serde_test = "1.0.176" +strum = { version = "0.28", features = ["derive"] } [features] std = [] diff --git a/src/arbitrary.rs b/src/arbitrary.rs index 9549ed7..10bb00a 100644 --- a/src/arbitrary.rs +++ b/src/arbitrary.rs @@ -235,14 +235,6 @@ impl<'a> Arbitrary<'a> for webauthn::FilteredPublicKeyCredentialParameters { } } -// cannot be derived because we want to make sure that we have valid values -impl<'a> Arbitrary<'a> for webauthn::KnownPublicKeyCredentialParameters { - fn arbitrary(u: &mut Unstructured<'a>) -> Result { - let alg = *u.choose(&webauthn::KNOWN_ALGS)?; - Ok(Self { alg }) - } -} - // cannot be derived because of missing impl for serde_bytes::Bytes impl<'a> Arbitrary<'a> for webauthn::PublicKeyCredentialDescriptorRef<'a> { fn arbitrary(u: &mut Unstructured<'a>) -> Result { diff --git a/src/authenticator.rs b/src/authenticator.rs index 94a6661..067b1a0 100644 --- a/src/authenticator.rs +++ b/src/authenticator.rs @@ -3,9 +3,6 @@ use crate::ctap1; use crate::ctap2; -pub use ctap1::Authenticator as Ctap1Authenticator; -pub use ctap2::Authenticator as Ctap2Authenticator; - #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] // clippy says (2022-02-26): large size difference diff --git a/src/ctap1.rs b/src/ctap1.rs index 9ce3e55..3d67dd3 100644 --- a/src/ctap1.rs +++ b/src/ctap1.rs @@ -255,13 +255,6 @@ pub trait Authenticator { } } -impl crate::Rpc, Response> for A { - /// Dispatches the enum of possible requests into the appropriate trait method. - fn call(&mut self, request: &Request<'_>) -> Result { - self.call_ctap1(request) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/ctap2.rs b/src/ctap2.rs index 9944987..3a07a0e 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -53,7 +53,7 @@ pub enum Request<'a> { Vendor(crate::operation::VendorOperation), } -pub enum CtapMappingError { +enum CtapMappingError { InvalidCommand(u8), ParsingError(cbor_smol::Error), } @@ -444,6 +444,7 @@ impl<'de> Deserialize<'de> for AttestationFormatsPreference { #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] +#[repr(u8)] pub enum Error { Success = 0x00, InvalidCommand = 0x01, @@ -502,10 +503,13 @@ pub enum Error { VendorLast = 0xFF, } +impl From for u8 { + fn from(error: Error) -> u8 { + error as _ + } +} + /// CTAP2 authenticator API -/// -/// Note that all Authenticators automatically implement [`crate::Rpc`] with [`Request`] and -/// [`Response`]. pub trait Authenticator { fn get_info(&mut self) -> get_info::Response; diff --git a/src/ctap2/client_pin.rs b/src/ctap2/client_pin.rs index d77e932..c21c1f0 100644 --- a/src/ctap2/client_pin.rs +++ b/src/ctap2/client_pin.rs @@ -512,7 +512,7 @@ mod tests { // The following test would then fail, as [1] != [2] let mut buf = [0u8; 64]; let example = PinV1Subcommand::GetKeyAgreement; - let ser = crate::serde::cbor_serialize(&example, &mut buf).unwrap(); + let ser = cbor_smol::cbor_serialize(&example, &mut buf).unwrap(); assert_eq!(ser, &[0x02]); } } diff --git a/src/ctap2/get_info.rs b/src/ctap2/get_info.rs index 9bbc142..857048a 100644 --- a/src/ctap2/get_info.rs +++ b/src/ctap2/get_info.rs @@ -1,8 +1,9 @@ use crate::webauthn::FilteredPublicKeyCredentialParameters; #[cfg(feature = "get-info-full")] use crate::String; -use crate::{Bytes, TryFromStrError, Vec}; +use crate::{TryFromStrError, Vec}; use serde::{Deserialize, Serialize}; +use serde_bytes::ByteArray; use serde_indexed::{DeserializeIndexed, SerializeIndexed}; pub type AuthenticatorInfo = Response; @@ -19,7 +20,7 @@ pub struct Response { pub extensions: Option>, // 0x03 - pub aaguid: Bytes<16>, + pub aaguid: ByteArray<16>, // 0x04 #[serde(skip_serializing_if = "Option::is_none")] @@ -176,18 +177,13 @@ pub struct Response { // FIDO_2_3 #[cfg(feature = "get-info-full")] #[serde(skip_serializing_if = "Option::is_none")] - pub authenticator_config_commands: Option>, + pub authenticator_config_commands: Option>, } impl Default for Response { fn default() -> Self { - let mut zero_aaguid = Vec::::new(); - zero_aaguid.resize_default(16).unwrap(); - let mut aaguid = Bytes::new(); - aaguid.resize_zero(16).unwrap(); - let mut response = ResponseBuilder { - aaguid, + aaguid: ByteArray::new([0; 16]), versions: Vec::new(), } .build(); @@ -199,7 +195,7 @@ impl Default for Response { #[derive(Debug)] pub struct ResponseBuilder { pub versions: Vec, - pub aaguid: Bytes<16>, + pub aaguid: ByteArray<16>, } impl ResponseBuilder { @@ -262,6 +258,7 @@ impl ResponseBuilder { } #[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[cfg_attr(test, derive(strum::EnumCount, strum::VariantArray))] #[non_exhaustive] #[serde(into = "&str", try_from = "&str")] pub enum Version { @@ -312,6 +309,7 @@ impl TryFrom<&str> for Version { pub const EXTENSION_COUNT: usize = 7; #[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[cfg_attr(test, derive(strum::EnumCount, strum::VariantArray))] #[non_exhaustive] #[serde(into = "&str", try_from = "&str")] pub enum Extension { @@ -368,6 +366,7 @@ impl TryFrom<&str> for Extension { pub const TRANSPORT_COUNT: usize = 3; #[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[cfg_attr(test, derive(strum::EnumCount, strum::VariantArray))] #[non_exhaustive] #[serde(into = "&str", try_from = "&str")] pub enum Transport { @@ -530,7 +529,42 @@ pub struct Certifications { #[cfg(test)] mod tests { use super::*; + use core::fmt::Debug; use serde_test::{assert_ser_tokens, assert_tokens, Token}; + use strum::{EnumCount, VariantArray}; + + fn test_enum(count: usize) + where + T: EnumCount + + VariantArray + + Into<&'static str> + + for<'a> TryFrom<&'a str> + + PartialEq + + Debug + + Copy, + { + assert_eq!(count, T::COUNT); + + for variant in T::VARIANTS { + let variant_str: &str = (*variant).into(); + assert_eq!(Some(*variant), T::try_from(variant_str).ok()); + } + } + + #[test] + fn test_version() { + test_enum::(VERSION_COUNT); + } + + #[test] + fn test_transport() { + test_enum::(TRANSPORT_COUNT); + } + + #[test] + fn test_extension() { + test_enum::(EXTENSION_COUNT); + } #[test] fn test_serde_version() { @@ -577,7 +611,7 @@ mod tests { #[test] fn test_serde_get_info_minimal() { let versions = Vec::from_slice(&[Version::Fido2_0, Version::Fido2_1]).unwrap(); - let aaguid = Bytes::try_from(&[0xff; 16]).unwrap(); + let aaguid = ByteArray::new([0xff; 16]); let response = ResponseBuilder { versions, aaguid }.build(); assert_tokens( &response, @@ -599,12 +633,12 @@ mod tests { fn test_serde_get_info_default() { // This corresponds to the response sent by the Nitrokey 3, see for example: // https://github.com/Nitrokey/nitrokey-3-firmware/blob/0d7209f1f75354878c0cf3454055defe8372ed14/utils/fido2-mds/metadata/v4/metadata-nk3xn-v4.json - const AAGUID: &[u8] = &[ + const AAGUID: [u8; 16] = [ 236, 153, 219, 25, 205, 31, 76, 6, 162, 169, 148, 15, 23, 166, 163, 11, ]; let versions = Vec::from_slice(&[Version::U2fV2, Version::Fido2_0, Version::Fido2_1]).unwrap(); - let aaguid = Bytes::try_from(AAGUID).unwrap(); + let aaguid = ByteArray::new(AAGUID); let mut options = CtapOptions::default(); options.rk = true; options.plat = Some(false); @@ -621,83 +655,111 @@ mod tests { response.max_creds_in_list = Some(10); response.max_cred_id_length = Some(255); response.transports = Some(Vec::from_slice(&[Transport::Nfc, Transport::Usb]).unwrap()); - assert_ser_tokens( - &response, - &[ - Token::Map { len: Some(9) }, - // 0x01: versions - Token::U64(0x01), - Token::Seq { len: Some(3) }, - Token::BorrowedStr("U2F_V2"), - Token::BorrowedStr("FIDO_2_0"), - Token::BorrowedStr("FIDO_2_1"), - Token::SeqEnd, - // 0x02: extensions - Token::U64(0x02), - Token::Some, - Token::Seq { len: Some(2) }, - Token::BorrowedStr("credProtect"), - Token::BorrowedStr("hmac-secret"), - Token::SeqEnd, - // 0x03: aaguid - Token::U64(0x03), - Token::BorrowedBytes(AAGUID), - // 0x04: options - Token::U64(0x04), - Token::Some, - Token::Struct { - name: "CtapOptions", - len: 7, - }, - Token::BorrowedStr("rk"), - Token::Bool(true), - Token::BorrowedStr("up"), - Token::Bool(true), - Token::BorrowedStr("plat"), - Token::Some, - Token::Bool(false), - Token::BorrowedStr("credMgmt"), - Token::Some, - Token::Bool(true), - Token::BorrowedStr("clientPin"), - Token::Some, - Token::Bool(false), - Token::BorrowedStr("largeBlobs"), - Token::Some, - Token::Bool(false), - Token::BorrowedStr("pinUvAuthToken"), - Token::Some, - Token::Bool(true), - Token::StructEnd, - // 0x05: maxMsgSize - Token::U64(0x05), - Token::Some, - Token::U64(3072), - // 0x06: pinUvAuthProtocols - Token::U64(0x06), - Token::Some, - Token::Seq { len: Some(2) }, - Token::U8(1), - Token::U8(0), - Token::SeqEnd, - // 0x07: maxCredentialCountInList - Token::U64(0x07), - Token::Some, - Token::U64(10), - // 0x08: maxCredentialIdLength - Token::U64(0x08), - Token::Some, - Token::U64(255), - // 0x09: transports - Token::U64(0x09), + + #[cfg(feature = "get-info-full")] + { + response.authenticator_config_commands = Some( + Vec::from_slice(&[ + crate::ctap2::config::Subcommand::ToggleAlwaysUv, + crate::ctap2::config::Subcommand::SetMinPINLength, + ]) + .unwrap(), + ); + } + + let len = 9 + if cfg!(feature = "get-info-full") { + 1 + } else { + 0 + }; + + let mut expected = vec![ + Token::Map { len: Some(len) }, + // 0x01: versions + Token::U64(0x01), + Token::Seq { len: Some(3) }, + Token::BorrowedStr("U2F_V2"), + Token::BorrowedStr("FIDO_2_0"), + Token::BorrowedStr("FIDO_2_1"), + Token::SeqEnd, + // 0x02: extensions + Token::U64(0x02), + Token::Some, + Token::Seq { len: Some(2) }, + Token::BorrowedStr("credProtect"), + Token::BorrowedStr("hmac-secret"), + Token::SeqEnd, + // 0x03: aaguid + Token::U64(0x03), + Token::BorrowedBytes(&AAGUID), + // 0x04: options + Token::U64(0x04), + Token::Some, + Token::Struct { + name: "CtapOptions", + len: 7, + }, + Token::BorrowedStr("rk"), + Token::Bool(true), + Token::BorrowedStr("up"), + Token::Bool(true), + Token::BorrowedStr("plat"), + Token::Some, + Token::Bool(false), + Token::BorrowedStr("credMgmt"), + Token::Some, + Token::Bool(true), + Token::BorrowedStr("clientPin"), + Token::Some, + Token::Bool(false), + Token::BorrowedStr("largeBlobs"), + Token::Some, + Token::Bool(false), + Token::BorrowedStr("pinUvAuthToken"), + Token::Some, + Token::Bool(true), + Token::StructEnd, + // 0x05: maxMsgSize + Token::U64(0x05), + Token::Some, + Token::U64(3072), + // 0x06: pinUvAuthProtocols + Token::U64(0x06), + Token::Some, + Token::Seq { len: Some(2) }, + Token::U8(1), + Token::U8(0), + Token::SeqEnd, + // 0x07: maxCredentialCountInList + Token::U64(0x07), + Token::Some, + Token::U64(10), + // 0x08: maxCredentialIdLength + Token::U64(0x08), + Token::Some, + Token::U64(255), + // 0x09: transports + Token::U64(0x09), + Token::Some, + Token::Seq { len: Some(2) }, + Token::BorrowedStr("nfc"), + Token::BorrowedStr("usb"), + Token::SeqEnd, + ]; + if cfg!(feature = "get-info-full") { + expected.extend([ + // 0x1F: authenticatorConfigCommands + Token::U64(0x1F), Token::Some, Token::Seq { len: Some(2) }, - Token::BorrowedStr("nfc"), - Token::BorrowedStr("usb"), + Token::U8(0x02), + Token::U8(0x03), Token::SeqEnd, - Token::MapEnd, - ], - ); + ]); + } + expected.push(Token::MapEnd); + + assert_ser_tokens(&response, &expected); } #[test] diff --git a/src/lib.rs b/src/lib.rs index dabda54..bedc01e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ #![cfg_attr(all(not(test), not(feature = "std")), no_std)] +#![cfg_attr(docsrs, feature(doc_cfg))] // #![no_std] //! `ctap-types` maps the various types involved in the FIDO CTAP protocol @@ -19,24 +20,22 @@ extern crate delog; generate_macros!(); -pub use heapless; -pub use heapless::{String, Vec}; -pub use heapless_bytes; +pub use heapless::{String, Vec, VecView}; pub use heapless_bytes::Bytes; pub use serde_bytes::ByteArray; #[cfg(feature = "arbitrary")] mod arbitrary; -pub mod authenticator; +mod authenticator; pub mod ctap1; pub mod ctap2; pub(crate) mod operation; -pub use cbor_smol as serde; pub mod sizes; #[cfg(test)] mod test; pub mod webauthn; +pub use authenticator::{Authenticator, Request, Response}; pub use ctap2::{Error, Result}; use core::fmt::{self, Display, Formatter}; @@ -51,8 +50,3 @@ impl Display for TryFromStrError { "invalid enum value".fmt(f) } } - -/// Call a remote procedure with a request, receive a response, maybe. -pub trait Rpc { - fn call(&mut self, request: &Request) -> core::result::Result; -} diff --git a/src/webauthn.rs b/src/webauthn.rs index 7a9b80e..59a908c 100644 --- a/src/webauthn.rs +++ b/src/webauthn.rs @@ -135,15 +135,30 @@ impl PublicKeyCredentialUserEntity { } } -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct KnownPublicKeyCredentialParameters { - pub alg: i32, +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(strum::EnumCount, strum::VariantArray))] +#[non_exhaustive] +pub enum KnownPublicKeyCredentialParameters { + ES256, + EdDSA, +} + +impl KnownPublicKeyCredentialParameters { + pub const ALL: [Self; COUNT_KNOWN_ALGS] = [Self::ES256, Self::EdDSA]; + + pub fn alg(&self) -> i32 { + match self { + Self::ES256 => ES256, + Self::EdDSA => ED_DSA, + } + } } impl From for PublicKeyCredentialParameters { fn from(value: KnownPublicKeyCredentialParameters) -> Self { Self { - alg: value.alg, + alg: value.alg(), key_type: String::try_from("public-key").unwrap(), } } @@ -155,12 +170,11 @@ pub enum UnknownPKCredentialParam { } /// ECDSA w/ SHA-256 -pub const ES256: i32 = -7; +const ES256: i32 = -7; /// EdDSA -pub const ED_DSA: i32 = -8; +const ED_DSA: i32 = -8; pub const COUNT_KNOWN_ALGS: usize = 2; -pub const KNOWN_ALGS: [i32; COUNT_KNOWN_ALGS] = [ES256, ED_DSA]; impl TryFrom for KnownPublicKeyCredentialParameters { type Error = UnknownPKCredentialParam; @@ -168,10 +182,12 @@ impl TryFrom for KnownPublicKeyCredentialParamete fn try_from(value: PublicKeyCredentialParameters) -> Result { if value.key_type != "public-key" { Err(UnknownPKCredentialParam::UnknownType) - } else if KNOWN_ALGS.contains(&value.alg) { - Ok(Self { alg: value.alg }) } else { - Err(UnknownPKCredentialParam::UnknownAlg) + match value.alg { + ES256 => Ok(Self::ES256), + ED_DSA => Ok(Self::EdDSA), + _ => Err(UnknownPKCredentialParam::UnknownAlg), + } } } } @@ -190,7 +206,7 @@ impl Serialize for FilteredPublicKeyCredentialParameters { use serde::ser::SerializeSeq; let mut seq = serializer.serialize_seq(Some(self.0.len()))?; for element in &self.0 { - let el: PublicKeyCredentialParameters = element.clone().into(); + let el = PublicKeyCredentialParameters::from(*element); seq.serialize_element(&el)? } seq.end() @@ -274,6 +290,16 @@ pub struct PublicKeyCredentialDescriptorRef<'a> { #[cfg(test)] mod tests { use super::*; + use strum::{EnumCount as _, VariantArray as _}; + + #[test] + fn test_known_cred_params() { + assert_eq!(KnownPublicKeyCredentialParameters::COUNT, COUNT_KNOWN_ALGS); + assert_eq!( + KnownPublicKeyCredentialParameters::ALL, + KnownPublicKeyCredentialParameters::VARIANTS + ); + } #[test] fn test_truncate() { diff --git a/tests/bennofs.rs b/tests/bennofs.rs index 1034ff4..1c8bdc8 100644 --- a/tests/bennofs.rs +++ b/tests/bennofs.rs @@ -1,4 +1,4 @@ -use ctap_types::serde::{cbor_deserialize, cbor_serialize}; +use cbor_smol::{cbor_deserialize, cbor_serialize}; use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Serialize, Deserialize)] diff --git a/tests/get_assertion.rs b/tests/get_assertion.rs index 51a58fc..0d25b38 100644 --- a/tests/get_assertion.rs +++ b/tests/get_assertion.rs @@ -1,5 +1,5 @@ fn test<'data, T: serde::Deserialize<'data> + std::fmt::Debug>(data: &'data [u8]) { - let result = ctap_types::serde::cbor_deserialize::(data); + let result = cbor_smol::cbor_deserialize::(data); assert!(result.is_ok(), "{:?}", result); }