diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d69e649..240a373 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,10 +16,28 @@ jobs: - name: Check library run: | cargo check + cargo check --features arbitrary cargo check --features get-info-full cargo check --features large-blobs cargo check --all-features + build-no-std: + name: Check library (no-std) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + target: x86_64-unknown-linux-gnu + override: true + - name: Check library (no-std) + run: | + cargo check + cargo check --features get-info-full + cargo check --features large-blobs + test: name: Run tests runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 8160139..81b2191 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [Unreleased]: https://github.com/trussed-dev/ctap-types/compare/0.2.0...HEAD -- +### Added + +- Add a `std` feature (disabled by default) +- Add `arbitrary::Arbitrary` implementations for all requests behind an `arbitrary` feature (disabled by default) ## [0.2.0] - 2024-06-21 diff --git a/Cargo.toml b/Cargo.toml index 98eff84..df6e8dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ description = "no_std friendly types for FIDO CTAP" repository = "https://github.com/trussed-dev/ctap-types" [dependencies] +arbitrary = { version = "1.3.2", features = ["derive"], optional = true } bitflags = "1.3" cbor-smol = "0.4" cosey = "0.3" @@ -21,6 +22,10 @@ serde_bytes = { version = "0.11.14", default-features = false } serde_repr = "0.1" [features] +std = [] + +# implements arbitrary::Arbitrary for requests +arbitrary = ["dep:arbitrary", "std"] # enables all fields for ctap2::get_info get-info-full = [] # enables support for implementing the large-blobs extension, see src/sizes.rs diff --git a/src/arbitrary.rs b/src/arbitrary.rs new file mode 100644 index 0000000..fe22654 --- /dev/null +++ b/src/arbitrary.rs @@ -0,0 +1,332 @@ +use core::{fmt::Debug, ops::ControlFlow}; + +use arbitrary::{Arbitrary, Error, Result, Unstructured}; +use cosey::EcdhEsHkdf256PublicKey; +use heapless::{String, Vec}; +use heapless_bytes::Bytes; +use serde_bytes::ByteArray; + +use crate::{ctap1, ctap2, webauthn}; + +// cannot be derived because of missing impl for &[T; N] +impl<'a> Arbitrary<'a> for ctap1::authenticate::Request<'a> { + fn arbitrary(u: &mut Unstructured<'a>) -> Result { + let control_byte = Arbitrary::arbitrary(u)?; + let challenge = u.bytes(32)?.try_into().unwrap(); + let app_id = u.bytes(32)?.try_into().unwrap(); + let key_handle = Arbitrary::arbitrary(u)?; + Ok(Self { + control_byte, + challenge, + app_id, + key_handle, + }) + } +} + +// cannot be derived because of missing impl for &[T; N] +impl<'a> Arbitrary<'a> for ctap1::register::Request<'a> { + fn arbitrary(u: &mut Unstructured<'a>) -> Result { + let challenge = u.bytes(32)?.try_into().unwrap(); + let app_id = u.bytes(32)?.try_into().unwrap(); + Ok(Self { challenge, app_id }) + } +} + +// cannot be derived because of missing impl for serde_bytes::Bytes, EcdhEsHkdf256PublicKey +impl<'a> Arbitrary<'a> for ctap2::client_pin::Request<'a> { + fn arbitrary(u: &mut Unstructured<'a>) -> Result { + let pin_protocol = u.arbitrary()?; + let sub_command = u.arbitrary()?; + let key_agreement = arbitrary_option(u, arbitrary_key)?; + let pin_auth = if bool::arbitrary(u)? { + Some(serde_bytes::Bytes::new(u.arbitrary()?)) + } else { + None + }; + let new_pin_enc = if bool::arbitrary(u)? { + Some(serde_bytes::Bytes::new(u.arbitrary()?)) + } else { + None + }; + let pin_hash_enc = if bool::arbitrary(u)? { + Some(serde_bytes::Bytes::new(u.arbitrary()?)) + } else { + None + }; + let _placeholder07 = u.arbitrary()?; + let _placeholder08 = u.arbitrary()?; + let permissions = u.arbitrary()?; + let rp_id = u.arbitrary()?; + Ok(Self { + pin_protocol, + sub_command, + key_agreement, + pin_auth, + new_pin_enc, + pin_hash_enc, + _placeholder07, + _placeholder08, + permissions, + rp_id, + }) + } +} + +// cannot be derived because of missing impl for serde_bytes::Bytes +impl<'a> Arbitrary<'a> for ctap2::credential_management::Request<'a> { + fn arbitrary(u: &mut Unstructured<'a>) -> Result { + let sub_command = u.arbitrary()?; + let sub_command_params = u.arbitrary()?; + let pin_protocol = u.arbitrary()?; + let pin_auth = if bool::arbitrary(u)? { + Some(serde_bytes::Bytes::new(u.arbitrary()?)) + } else { + None + }; + Ok(Self { + sub_command, + sub_command_params, + pin_protocol, + pin_auth, + }) + } +} + +// cannot be derived because of missing impl for serde_bytes::ByteArray +impl<'a> Arbitrary<'a> for ctap2::credential_management::SubcommandParameters<'a> { + fn arbitrary(u: &mut Unstructured<'a>) -> Result { + let rp_id_hash = arbitrary_option(u, arbitrary_byte_array)?; + let credential_id = u.arbitrary()?; + let user = u.arbitrary()?; + Ok(Self { + rp_id_hash, + credential_id, + user, + }) + } +} + +// cannot be derived because of missing impl for EcdhEsHkdf256PublicKey, Bytes<_> +impl<'a> Arbitrary<'a> for ctap2::get_assertion::HmacSecretInput { + fn arbitrary(u: &mut Unstructured<'a>) -> Result { + let key_agreement = arbitrary_key(u)?; + let salt_enc = arbitrary_bytes(u)?; + let salt_auth = arbitrary_bytes(u)?; + let pin_protocol = u.arbitrary()?; + Ok(Self { + key_agreement, + salt_enc, + salt_auth, + pin_protocol, + }) + } +} + +// cannot be derived because of missing impl for serde_bytes::Bytes, Vec<_> +impl<'a> Arbitrary<'a> for ctap2::get_assertion::Request<'a> { + fn arbitrary(u: &mut Unstructured<'a>) -> Result { + let rp_id = u.arbitrary()?; + let client_data_hash = serde_bytes::Bytes::new(u.arbitrary()?); + let allow_list = arbitrary_option(u, arbitrary_vec)?; + let extensions = u.arbitrary()?; + let options = u.arbitrary()?; + let pin_auth = if bool::arbitrary(u)? { + Some(serde_bytes::Bytes::new(u.arbitrary()?)) + } else { + None + }; + let pin_protocol = u.arbitrary()?; + Ok(Self { + rp_id, + client_data_hash, + allow_list, + extensions, + options, + pin_auth, + pin_protocol, + }) + } +} + +// cannot be derived because of missing impl for serde_bytes::Bytes +impl<'a> Arbitrary<'a> for ctap2::large_blobs::Request<'a> { + fn arbitrary(u: &mut Unstructured<'a>) -> Result { + let get = u.arbitrary()?; + let set = if bool::arbitrary(u)? { + Some(serde_bytes::Bytes::new(u.arbitrary()?)) + } else { + None + }; + let offset = u.arbitrary()?; + let length = u.arbitrary()?; + let pin_uv_auth_param = if bool::arbitrary(u)? { + Some(serde_bytes::Bytes::new(u.arbitrary()?)) + } else { + None + }; + let pin_uv_auth_protocol = u.arbitrary()?; + Ok(Self { + get, + set, + offset, + length, + pin_uv_auth_param, + pin_uv_auth_protocol, + }) + } +} + +// cannot be derived because of missing impl for serde_bytes::Bytes +impl<'a> Arbitrary<'a> for ctap2::make_credential::Request<'a> { + fn arbitrary(u: &mut Unstructured<'a>) -> Result { + let client_data_hash = serde_bytes::Bytes::new(u.arbitrary()?); + let rp = u.arbitrary()?; + let user = u.arbitrary()?; + let pub_key_cred_params = u.arbitrary()?; + let exclude_list = arbitrary_option(u, arbitrary_vec)?; + let extensions = u.arbitrary()?; + let options = u.arbitrary()?; + let pin_auth = if bool::arbitrary(u)? { + Some(serde_bytes::Bytes::new(u.arbitrary()?)) + } else { + None + }; + let pin_protocol = u.arbitrary()?; + let enterprise_attestation = u.arbitrary()?; + Ok(Self { + client_data_hash, + rp, + user, + pub_key_cred_params, + exclude_list, + extensions, + options, + pin_auth, + pin_protocol, + enterprise_attestation, + }) + } +} + +// cannot be derived because of missing impl for Vec<_> +impl<'a> Arbitrary<'a> for webauthn::FilteredPublicKeyCredentialParameters { + fn arbitrary(u: &mut Unstructured<'a>) -> Result { + let parameters = arbitrary_vec(u)?; + Ok(Self(parameters)) + } +} + +// 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 { + let id = serde_bytes::Bytes::new(u.arbitrary()?); + let key_type = u.arbitrary()?; + Ok(Self { id, key_type }) + } +} + +// cannot be derived because of missing impl for String<_> +impl<'a> Arbitrary<'a> for webauthn::PublicKeyCredentialRpEntity { + fn arbitrary(u: &mut Unstructured<'a>) -> Result { + let id = arbitrary_str(u)?; + let name = if bool::arbitrary(u)? { + Some(arbitrary_str(u)?) + } else { + None + }; + let icon = Arbitrary::arbitrary(u)?; + Ok(Self { id, name, icon }) + } +} + +// cannot be derived because of missing impl for Bytes<_> and String<_> +impl<'a> Arbitrary<'a> for webauthn::PublicKeyCredentialUserEntity { + fn arbitrary(u: &mut Unstructured<'a>) -> Result { + let id = arbitrary_bytes(u)?; + let icon = if bool::arbitrary(u)? { + Some(arbitrary_str(u)?) + } else { + None + }; + let name = if bool::arbitrary(u)? { + Some(arbitrary_str(u)?) + } else { + None + }; + let display_name = if bool::arbitrary(u)? { + Some(arbitrary_str(u)?) + } else { + None + }; + Ok(Self { + id, + icon, + name, + display_name, + }) + } +} + +fn arbitrary_byte_array<'a, const N: usize>(u: &mut Unstructured<'_>) -> Result<&'a ByteArray> { + let bytes: &[u8; N] = u.bytes(N)?.try_into().unwrap(); + // TODO: conversion should be provided by serde_bytes + Ok(unsafe { &*(bytes as *const [u8; N] as *const ByteArray) }) +} + +fn arbitrary_bytes(u: &mut Unstructured<'_>) -> Result> { + let n = usize::arbitrary(u)?.min(N); + Ok(Bytes::from_slice(u.bytes(n)?).unwrap()) +} + +fn arbitrary_vec<'a, T: Arbitrary<'a> + Debug, const N: usize>( + u: &mut Unstructured<'a>, +) -> Result> { + let mut vec = Vec::new(); + u.arbitrary_loop(Some(0), Some(N.try_into().unwrap()), |u| { + vec.push(u.arbitrary()?).unwrap(); + Ok(ControlFlow::Continue(())) + })?; + Ok(vec) +} + +fn arbitrary_str(u: &mut Unstructured<'_>) -> Result> { + let n = usize::arbitrary(u)?.min(N); + match core::str::from_utf8(u.peek_bytes(n).ok_or(Error::NotEnoughData)?) { + Ok(s) => { + u.bytes(n)?; + Ok(s.try_into().unwrap()) + } + Err(e) => { + let i = e.valid_up_to(); + let valid = u.bytes(i)?; + let s = unsafe { core::str::from_utf8_unchecked(valid) }; + Ok(s.try_into().unwrap()) + } + } +} + +fn arbitrary_option<'a, T, F>(u: &mut Unstructured<'a>, f: F) -> Result> +where + F: FnOnce(&mut Unstructured<'a>) -> Result, +{ + if bool::arbitrary(u)? { + f(u).map(Some) + } else { + Ok(None) + } +} + +fn arbitrary_key(u: &mut Unstructured<'_>) -> Result { + let x = arbitrary_bytes(u)?; + let y = arbitrary_bytes(u)?; + Ok(EcdhEsHkdf256PublicKey { x, y }) +} diff --git a/src/authenticator.rs b/src/authenticator.rs index 06dd272..94a6661 100644 --- a/src/authenticator.rs +++ b/src/authenticator.rs @@ -7,6 +7,7 @@ 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 // - first is 88 bytes // - second is 10456 bytes diff --git a/src/ctap1.rs b/src/ctap1.rs index def7486..81d9f9b 100644 --- a/src/ctap1.rs +++ b/src/ctap1.rs @@ -72,6 +72,7 @@ pub mod register { #[repr(u8)] #[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub enum ControlByte { // Conor: // I think U2F check-only maps to FIDO2 MakeCredential with the credID in the excludeList, @@ -109,6 +110,7 @@ pub type RegisterResponse = register::Response; pub type AuthenticateResponse = authenticate::Response; #[derive(Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[allow(clippy::large_enum_variant)] /// Enum of all CTAP1 requests. pub enum Request<'a> { diff --git a/src/ctap2.rs b/src/ctap2.rs index 54e3c12..67e1e87 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -20,6 +20,7 @@ pub mod make_credential; pub type Result = core::result::Result; #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[non_exhaustive] #[allow(clippy::large_enum_variant)] // clippy says...large size difference @@ -178,6 +179,7 @@ impl Response { } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[non_exhaustive] pub struct AuthenticatorOptions { #[serde(skip_serializing_if = "Option::is_none")] diff --git a/src/ctap2/client_pin.rs b/src/ctap2/client_pin.rs index 76c27c9..35419e3 100644 --- a/src/ctap2/client_pin.rs +++ b/src/ctap2/client_pin.rs @@ -5,6 +5,7 @@ use serde_indexed::{DeserializeIndexed, SerializeIndexed}; use serde_repr::{Deserialize_repr, Serialize_repr}; #[derive(Clone, Debug, Eq, PartialEq, Serialize_repr, Deserialize_repr)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[non_exhaustive] #[repr(u8)] pub enum PinV1Subcommand { @@ -72,11 +73,11 @@ pub struct Request<'a> { // 0x07 #[serde(skip_serializing_if = "Option::is_none")] - _placeholder07: Option<()>, + pub(crate) _placeholder07: Option<()>, // 0x08 #[serde(skip_serializing_if = "Option::is_none")] - _placeholder08: Option<()>, + pub(crate) _placeholder08: Option<()>, // 0x09 // Bitfield of permissions diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index 9d3762c..2f9e082 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -18,6 +18,7 @@ pub enum CredentialProtectionPolicy { } #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize_repr, Deserialize_repr)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[non_exhaustive] #[repr(u8)] pub enum Subcommand { diff --git a/src/ctap2/get_assertion.rs b/src/ctap2/get_assertion.rs index 213bb58..d6373ba 100644 --- a/src/ctap2/get_assertion.rs +++ b/src/ctap2/get_assertion.rs @@ -21,6 +21,7 @@ pub struct HmacSecretInput { } #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[non_exhaustive] pub struct ExtensionsInput { #[serde(rename = "hmac-secret")] diff --git a/src/ctap2/make_credential.rs b/src/ctap2/make_credential.rs index 7ba3ad8..cdb6188 100644 --- a/src/ctap2/make_credential.rs +++ b/src/ctap2/make_credential.rs @@ -23,6 +23,7 @@ impl TryFrom for CredentialProtectionPolicy { } #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[non_exhaustive] pub struct Extensions { #[serde(rename = "credProtect")] diff --git a/src/lib.rs b/src/lib.rs index 6763da2..da6cbc0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![cfg_attr(not(test), no_std)] +#![cfg_attr(all(not(test), not(feature = "std")), no_std)] // #![no_std] //! `ctap-types` maps the various types involved in the FIDO CTAP protocol @@ -25,6 +25,8 @@ pub use heapless_bytes; pub use heapless_bytes::Bytes; pub use serde_bytes::ByteArray; +#[cfg(feature = "arbitrary")] +mod arbitrary; pub mod authenticator; pub mod ctap1; pub mod ctap2; diff --git a/src/operation.rs b/src/operation.rs index 4f6f4ec..e6ae88b 100644 --- a/src/operation.rs +++ b/src/operation.rs @@ -1,5 +1,6 @@ /// the authenticator API, consisting of "operations" #[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub enum Operation { MakeCredential, GetAssertion, @@ -49,6 +50,7 @@ impl Operation { /// Vendor CTAP2 operations, from 0x40 to 0x7f. #[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct VendorOperation(u8); impl VendorOperation { diff --git a/src/webauthn.rs b/src/webauthn.rs index 14489ce..48d4dc6 100644 --- a/src/webauthn.rs +++ b/src/webauthn.rs @@ -29,6 +29,7 @@ pub struct PublicKeyCredentialRpEntity { /// This field must be parsed but not used or stored. Therefore this wrapper type can be /// deserialized from a string but does not store any data. #[derive(Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct Icon; impl<'de> Deserialize<'de> for Icon {