From 42578472de87724b616049acc4946f6fd61df4db Mon Sep 17 00:00:00 2001 From: Alfie Fresta Date: Fri, 23 May 2025 19:02:04 +0200 Subject: [PATCH 1/6] Use thiserror for errors --- Cargo.lock | 84 +++++++++++++++--------------- libwebauthn/src/transport/error.rs | 6 +-- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c950ff23..2dfb41ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,9 +63,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", @@ -78,33 +78,33 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.8" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", "once_cell_polyfill", @@ -214,9 +214,9 @@ dependencies = [ [[package]] name = "base64ct" -version = "1.7.3" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bitflags" @@ -325,9 +325,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" [[package]] name = "bytemuck" @@ -376,9 +376,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.24" +version = "1.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" +checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" dependencies = [ "shlex", ] @@ -432,9 +432,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "combine" @@ -978,9 +978,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.3" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" [[package]] name = "heapless" @@ -1079,7 +1079,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown 0.15.3", + "hashbrown 0.15.4", ] [[package]] @@ -1265,9 +1265,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -1546,9 +1546,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -1556,9 +1556,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", @@ -1629,9 +1629,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" @@ -2164,15 +2164,15 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "snow" -version = "0.10.0-alpha.1" +version = "0.10.0-beta.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e55510cf1cf5094776adcdc1141531281f3a7e3f4840c02f0495b489b766f7aa" +checksum = "53efc30c13747741729afcad533db7bbb7edd516fbc705172c2564f8533417fa" dependencies = [ "aes-gcm", "blake2", @@ -2180,7 +2180,7 @@ dependencies = [ "chacha20poly1305", "curve25519-dalek", "p256", - "rand_core 0.6.4", + "rand_core 0.9.3", "ring", "rustc_version", "sha2", @@ -2440,15 +2440,15 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.9" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" [[package]] name = "toml_edit" -version = "0.22.26" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "toml_datetime", @@ -2468,9 +2468,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" dependencies = [ "proc-macro2", "quote", @@ -2479,9 +2479,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", diff --git a/libwebauthn/src/transport/error.rs b/libwebauthn/src/transport/error.rs index aa19941b..cade5ace 100644 --- a/libwebauthn/src/transport/error.rs +++ b/libwebauthn/src/transport/error.rs @@ -1,6 +1,6 @@ pub use crate::proto::CtapError; -#[derive(thiserror::Error, Debug, Copy, Clone, PartialEq)] +#[derive(thiserror::Error, Debug, PartialEq)] pub enum PlatformError { #[error("pin too short")] PinTooShort, @@ -18,7 +18,7 @@ pub enum PlatformError { SyntaxError, } -#[derive(thiserror::Error, Debug, Copy, Clone, PartialEq)] +#[derive(thiserror::Error, Debug, PartialEq)] pub enum TransportError { #[error("connection failed")] ConnectionFailed, @@ -44,7 +44,7 @@ pub enum TransportError { IoError(std::io::ErrorKind), } -#[derive(thiserror::Error, Debug, Copy, Clone, PartialEq)] +#[derive(thiserror::Error, Debug, PartialEq)] pub enum Error { #[error("Transport error: {0}")] Transport(#[from] TransportError), From 18b0db82b31e41f1d2a3c0fbacf2a2c434cd2851 Mon Sep 17 00:00:00 2001 From: Alfie Fresta Date: Sun, 25 May 2025 20:24:39 +0200 Subject: [PATCH 2/6] Allow unknown CBOR fields --- .gitmodules | 3 + Cargo.lock | 34 ++++++++- libwebauthn/Cargo.toml | 2 +- libwebauthn/src/fido.rs | 13 ++-- .../src/management/authenticator_config.rs | 4 +- libwebauthn/src/management/bio_enrollment.rs | 6 +- .../src/management/credential_management.rs | 8 +- libwebauthn/src/ops/u2f.rs | 4 +- libwebauthn/src/proto/ctap2/cbor/mod.rs | 2 + libwebauthn/src/proto/ctap2/cbor/request.rs | 17 ++--- libwebauthn/src/proto/ctap2/cbor/serde.rs | 75 +++++++++++++++++++ libwebauthn/src/proto/ctap2/model.rs | 6 +- .../src/proto/ctap2/model/get_assertion.rs | 4 +- libwebauthn/src/proto/ctap2/protocol.rs | 4 +- .../src/transport/cable/qr_code_device.rs | 3 +- libwebauthn/src/transport/cable/tunnel.rs | 5 +- libwebauthn/src/transport/error.rs | 9 +++ serde-indexed | 1 + 18 files changed, 162 insertions(+), 38 deletions(-) create mode 100644 libwebauthn/src/proto/ctap2/cbor/serde.rs create mode 160000 serde-indexed diff --git a/.gitmodules b/.gitmodules index 120a69f7..254a2d58 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "solo/src/ext"] path = solo/src/ext url = https://github.com/AlfioEmanueleFresta/solo.git +[submodule "serde-indexed"] + path = serde-indexed + url = git@github.com:AlfioEmanueleFresta/serde-indexed.git diff --git a/Cargo.lock b/Cargo.lock index 2dfb41ac..dc01b25e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -536,7 +536,7 @@ dependencies = [ "heapless-bytes", "iso7816", "serde", - "serde-indexed", + "serde-indexed 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde_bytes", "serde_repr", ] @@ -1013,6 +1013,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + [[package]] name = "hidapi" version = "2.6.3" @@ -1242,7 +1248,7 @@ dependencies = [ "qrcode", "rand 0.8.5", "serde", - "serde-indexed", + "serde-indexed 0.1.1", "serde_bytes", "serde_cbor", "serde_derive", @@ -2023,6 +2029,30 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-byte-array" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63213ee4ed648dbd87db6fa993d4275b46bfb4ddfd95b3756045007c2b28f742" +dependencies = [ + "serde", +] + +[[package]] +name = "serde-indexed" +version = "0.1.1" +dependencies = [ + "heapless", + "hex-literal", + "proc-macro2", + "quote", + "serde", + "serde-byte-array", + "serde_bytes", + "serde_cbor", + "syn 2.0.101", +] + [[package]] name = "serde-indexed" version = "0.1.1" diff --git a/libwebauthn/Cargo.toml b/libwebauthn/Cargo.toml index 387e87f0..ae965e11 100644 --- a/libwebauthn/Cargo.toml +++ b/libwebauthn/Cargo.toml @@ -29,7 +29,7 @@ futures = "0.3.5" tokio = { version = "1.45", features = ["full"] } serde = "1.0.110" serde_cbor = "0.11.2" -serde-indexed = "0.1.1" +serde-indexed = { path = "../serde-indexed" } serde_derive = "1.0.123" serde_repr = "0.1.6" serde_bytes = "0.11.5" diff --git a/libwebauthn/src/fido.rs b/libwebauthn/src/fido.rs index de91b540..090e2f1d 100644 --- a/libwebauthn/src/fido.rs +++ b/libwebauthn/src/fido.rs @@ -12,6 +12,7 @@ use std::{ }; use tracing::{error, warn}; +use crate::proto::ctap2::cbor::{CborDeserialize, CborSerialize}; use crate::{ proto::{ ctap2::{Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialType}, @@ -95,7 +96,10 @@ where // signCount | 4 // attestedCredentialData | variable // extensions | variable - let mut res = self.rp_id_hash.to_vec(); + let mut res = self.rp_id_hash.to_vec().map_err(|e| { + error!("Failed to create AuthenticatorData output vec at rp_id_hash: {e:?}"); + Error::Platform(e.into()) + })?; res.push(self.flags.bits()); res.write_u32::(self.signature_count) .map_err(|e| { @@ -120,7 +124,7 @@ where })?; res.extend(&att_data.credential_id); let cose_encoded_public_key = - serde_cbor::to_vec(&att_data.credential_public_key) + att_data.credential_public_key.to_vec() .map_err(|e| { error!( "Failed to create AuthenticatorData output vec at attested_credential.credential_public_key: {e:?}" @@ -132,7 +136,7 @@ where if self.extensions.is_some() || self.flags.contains(AuthenticatorDataFlags::EXTENSION_DATA) { - res.extend(serde_cbor::to_vec(&self.extensions).map_err(|e| { + res.extend(self.extensions.to_vec().map_err(|e| { error!("Failed to create AuthenticatorData output vec at extensions: {e:?}"); Error::Platform(PlatformError::InvalidDeviceResponse) })?); @@ -215,9 +219,8 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for AuthenticatorData { let mut credential_id = vec![0u8; credential_id_len]; cursor.read_exact(&mut credential_id).unwrap(); // We checked the length - let mut deserializer = serde_cbor::Deserializer::from_reader(&mut cursor); let credential_public_key: PublicKey = - Deserialize::deserialize(&mut deserializer).map_err(DesError::custom)?; + CborDeserialize::from_reader(&mut cursor).map_err(DesError::custom)?; attested_credential = Some(AttestedCredentialData { aaguid, diff --git a/libwebauthn/src/management/authenticator_config.rs b/libwebauthn/src/management/authenticator_config.rs index 09c3adb7..19f1d84a 100644 --- a/libwebauthn/src/management/authenticator_config.rs +++ b/libwebauthn/src/management/authenticator_config.rs @@ -1,3 +1,4 @@ +use crate::proto::ctap2::cbor::CborSerialize; use crate::proto::ctap2::Ctap2ClientPinRequest; pub use crate::transport::error::{CtapError, Error}; use crate::transport::Channel; @@ -14,7 +15,6 @@ use crate::{ }; use async_trait::async_trait; use serde_bytes::ByteBuf; -use serde_cbor::ser::to_vec; use std::time::Duration; use tracing::info; @@ -173,7 +173,7 @@ impl Ctap2UserVerifiableRequest for Ctap2AuthenticatorConfigRequest { data.push(0x0D); data.push(self.subcommand as u8); if self.subcommand == Ctap2AuthenticatorConfigCommand::SetMinPINLength { - data.extend(to_vec(&self.subcommand_params).unwrap()); + data.extend((&self.subcommand_params).to_vec().unwrap()); } let uv_auth_param = uv_proto.authenticate(uv_auth_token, &data); self.protocol = Some(uv_proto.version()); diff --git a/libwebauthn/src/management/bio_enrollment.rs b/libwebauthn/src/management/bio_enrollment.rs index 3c8a144a..c8762ec9 100644 --- a/libwebauthn/src/management/bio_enrollment.rs +++ b/libwebauthn/src/management/bio_enrollment.rs @@ -1,3 +1,4 @@ +use crate::proto::ctap2::cbor::CborSerialize; use crate::{ ops::webauthn::UserVerificationRequirement, pin::PinUvAuthProtocol, @@ -17,7 +18,6 @@ use crate::{ }; use async_trait::async_trait; use serde_bytes::ByteBuf; -use serde_cbor::ser::to_vec; use std::time::Duration; use tracing::info; @@ -213,7 +213,7 @@ where let remaining_samples = unwrap_field!(resp.remaining_samples); let template_id = unwrap_field!(resp.template_id).clone(); let sample_status = unwrap_field!(resp.last_enroll_sample_status); - Ok((template_id.to_vec(), sample_status, remaining_samples)) + Ok((template_id.to_vec()?, sample_status, remaining_samples)) } async fn capture_next_bio_enrollment_sample( @@ -295,7 +295,7 @@ impl Ctap2UserVerifiableRequest for Ctap2BioEnrollmentRequest { }; // e.g. "Authenticator calls verify(pinUvAuthToken, fingerprint (0x01) || removeEnrollment (0x06) || subCommandParams, pinUvAuthParam)" if let Some(params) = &self.subcommand_params { - data.extend(to_vec(params).unwrap()); + data.extend(params.to_vec().unwrap()); } let uv_auth_param = uv_proto.authenticate(uv_auth_token, &data); self.protocol = Some(uv_proto.version()); diff --git a/libwebauthn/src/management/credential_management.rs b/libwebauthn/src/management/credential_management.rs index 9e4def69..65e54666 100644 --- a/libwebauthn/src/management/credential_management.rs +++ b/libwebauthn/src/management/credential_management.rs @@ -1,3 +1,4 @@ +use crate::proto::ctap2::cbor::CborSerialize; use crate::{ ops::webauthn::UserVerificationRequirement, pin::PinUvAuthProtocol, @@ -17,7 +18,6 @@ use crate::{ }; use async_trait::async_trait; use serde_bytes::ByteBuf; -use serde_cbor::ser::to_vec; use std::time::Duration; use tracing::info; @@ -111,7 +111,7 @@ where Ok(( Ctap2RPData::new( unwrap_field!(resp.rp), - unwrap_field!(resp.rp_id_hash).to_vec(), + unwrap_field!(resp.rp_id_hash).to_vec()?, ), unwrap_field!(resp.total_rps), )) @@ -138,7 +138,7 @@ where }?; Ok(Ctap2RPData::new( unwrap_field!(resp.rp), - unwrap_field!(resp.rp_id_hash).to_vec(), + unwrap_field!(resp.rp_id_hash).to_vec()?, )) } @@ -282,7 +282,7 @@ impl Ctap2UserVerifiableRequest for Ctap2CredentialManagementRequest { // e.g. pinUvAuthParam (0x04): authenticate(pinUvAuthToken, enumerateCredentialsBegin (0x04) || subCommandParams). if let Some(params) = &self.subcommand_params { - data.extend(to_vec(params).unwrap()); + data.extend(params.to_vec().unwrap()); } let uv_auth_param = uv_proto.authenticate(uv_auth_token, &data); self.protocol = Some(uv_proto.version()); diff --git a/libwebauthn/src/ops/u2f.rs b/libwebauthn/src/ops/u2f.rs index 5d969ae6..a37986ed 100644 --- a/libwebauthn/src/ops/u2f.rs +++ b/libwebauthn/src/ops/u2f.rs @@ -2,7 +2,6 @@ use std::time::Duration; use cosey as cose; use serde_bytes::ByteBuf; -use serde_cbor::to_vec; use sha2::{Digest, Sha256}; use tracing::{error, trace}; use x509_parser::nom::AsBytes; @@ -14,6 +13,7 @@ use crate::ops::webauthn::{ }; use crate::proto::ctap1::{Ctap1RegisterRequest, Ctap1SignRequest}; use crate::proto::ctap1::{Ctap1RegisterResponse, Ctap1SignResponse}; +use crate::proto::ctap2::cbor::CborSerialize; use crate::proto::ctap2::{ Ctap2AttestationStatement, Ctap2COSEAlgorithmIdentifier, Ctap2GetAssertionResponse, Ctap2MakeCredentialResponse, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialType, @@ -79,7 +79,7 @@ impl UpgradableResponse for Regis x: x.into(), y: y.into(), }); - let cose_encoded_public_key = to_vec(&cose_public_key).unwrap(); + let cose_encoded_public_key = cose_public_key.to_vec()?; assert!(cose_encoded_public_key.len() == 77); // Let attestedCredData be a byte string with following structure: diff --git a/libwebauthn/src/proto/ctap2/cbor/mod.rs b/libwebauthn/src/proto/ctap2/cbor/mod.rs index c4215fd9..4ae0fbb2 100644 --- a/libwebauthn/src/proto/ctap2/cbor/mod.rs +++ b/libwebauthn/src/proto/ctap2/cbor/mod.rs @@ -1,5 +1,7 @@ mod request; mod response; +mod serde; pub use request::CborRequest; pub use response::CborResponse; +pub(crate) use serde::{CborDeserialize, CborError, CborSerialize}; diff --git a/libwebauthn/src/proto/ctap2/cbor/request.rs b/libwebauthn/src/proto/ctap2/cbor/request.rs index 7303b1e2..909c63b7 100644 --- a/libwebauthn/src/proto/ctap2/cbor/request.rs +++ b/libwebauthn/src/proto/ctap2/cbor/request.rs @@ -1,9 +1,6 @@ -extern crate serde_cbor; - -use serde_cbor::ser::to_vec; - use std::io::Error as IOError; +use crate::proto::ctap2::cbor::CborSerialize; use crate::proto::ctap2::model::Ctap2ClientPinRequest; use crate::proto::ctap2::model::Ctap2CommandCode; use crate::proto::ctap2::model::Ctap2GetAssertionRequest; @@ -43,7 +40,7 @@ impl From<&Ctap2MakeCredentialRequest> for CborRequest { fn from(request: &Ctap2MakeCredentialRequest) -> CborRequest { CborRequest { command: Ctap2CommandCode::AuthenticatorMakeCredential, - encoded_data: to_vec(request).unwrap(), + encoded_data: request.to_vec().unwrap(), } } } @@ -52,7 +49,7 @@ impl From<&Ctap2GetAssertionRequest> for CborRequest { fn from(request: &Ctap2GetAssertionRequest) -> CborRequest { CborRequest { command: Ctap2CommandCode::AuthenticatorGetAssertion, - encoded_data: to_vec(request).unwrap(), + encoded_data: request.to_vec().unwrap(), } } } @@ -61,7 +58,7 @@ impl From<&Ctap2ClientPinRequest> for CborRequest { fn from(request: &Ctap2ClientPinRequest) -> CborRequest { CborRequest { command: Ctap2CommandCode::AuthenticatorClientPin, - encoded_data: to_vec(request).unwrap(), + encoded_data: request.to_vec().unwrap(), } } } @@ -70,7 +67,7 @@ impl From<&Ctap2AuthenticatorConfigRequest> for CborRequest { fn from(request: &Ctap2AuthenticatorConfigRequest) -> CborRequest { CborRequest { command: Ctap2CommandCode::AuthenticatorConfig, - encoded_data: to_vec(request).unwrap(), + encoded_data: request.to_vec().unwrap(), } } } @@ -84,7 +81,7 @@ impl From<&Ctap2BioEnrollmentRequest> for CborRequest { }; CborRequest { command, - encoded_data: to_vec(request).unwrap(), + encoded_data: request.to_vec().unwrap(), } } } @@ -98,7 +95,7 @@ impl From<&Ctap2CredentialManagementRequest> for CborRequest { }; CborRequest { command, - encoded_data: to_vec(request).unwrap(), + encoded_data: request.to_vec().unwrap(), } } } diff --git a/libwebauthn/src/proto/ctap2/cbor/serde.rs b/libwebauthn/src/proto/ctap2/cbor/serde.rs new file mode 100644 index 00000000..636895c3 --- /dev/null +++ b/libwebauthn/src/proto/ctap2/cbor/serde.rs @@ -0,0 +1,75 @@ +use serde::Serialize; + +#[derive(thiserror::Error, Debug)] +pub enum CborError { + #[error("serde_cbor serialization error: {0}")] + SerdeCbor(#[from] serde_cbor::Error), +} + +impl PartialEq for CborError { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (CborError::SerdeCbor(e1), CborError::SerdeCbor(e2)) => { + e1.to_string() == e2.to_string() + } + } + } +} + +pub(crate) trait CborSerialize { + fn to_vec(&self) -> Result, CborError>; +} + +impl CborSerialize for T +where + T: Serialize, +{ + fn to_vec(&self) -> Result, CborError> { + serde_cbor::ser::to_vec(self).map_err(CborError::from) + } +} + +pub(crate) trait CborDeserialize: Sized + serde::de::DeserializeOwned { + fn from_reader(reader: R) -> Result; + fn from_slice(slice: &[u8]) -> Result; +} + +impl CborDeserialize for T +where + T: for<'de> serde::Deserialize<'de>, +{ + fn from_reader(reader: R) -> Result { + serde_cbor::de::from_reader(reader).map_err(CborError::from) + } + + fn from_slice(slice: &[u8]) -> Result { + serde_cbor::de::from_slice(slice).map_err(CborError::from) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_indexed::{DeserializeIndexed, SerializeIndexed}; + + #[derive(Debug, PartialEq, SerializeIndexed, DeserializeIndexed)] + #[serde_indexed(offset = 1)] + struct TestStruct { + pub a: u8, + pub b: u8, + } + + #[test] + fn test_deserialize_indexed_with_extra_field() { + // Map: 1 => 10, 2 => 20, 3 => 99 (unexpected) + let value = TestStruct { a: 10, b: 20 }; + let mut map = std::collections::BTreeMap::new(); + map.insert(1, 10u8); + map.insert(2, 20u8); + map.insert(3, 99u8); // unexpected field + + let cbor = map.to_vec().unwrap(); + let result = TestStruct::from_slice(&cbor).unwrap(); + assert_eq!(result, value); + } +} diff --git a/libwebauthn/src/proto/ctap2/model.rs b/libwebauthn/src/proto/ctap2/model.rs index 27151821..65d3c8c0 100644 --- a/libwebauthn/src/proto/ctap2/model.rs +++ b/libwebauthn/src/proto/ctap2/model.rs @@ -220,12 +220,12 @@ pub enum Ctap2UserVerificationOperation { #[cfg(test)] mod tests { + use crate::proto::ctap2::cbor::CborSerialize; use crate::proto::ctap2::Ctap2PublicKeyCredentialDescriptor; use super::{Ctap2COSEAlgorithmIdentifier, Ctap2CredentialType, Ctap2PublicKeyCredentialType}; use hex; use serde_bytes::ByteBuf; - use serde_cbor; #[test] /// Verify CBOR serialization conforms to CTAP canonical standard, including ordering (see #95) @@ -234,7 +234,7 @@ mod tests { algorithm: Ctap2COSEAlgorithmIdentifier::ES256, public_key_type: Ctap2PublicKeyCredentialType::PublicKey, }; - let serialized = serde_cbor::to_vec(&credential_type).unwrap(); + let serialized = credential_type.to_vec().unwrap(); // Known good, verified by hand with cbor.me playground let expected = hex::decode("a263616c672664747970656a7075626c69632d6b6579").unwrap(); assert_eq!(serialized, expected); @@ -248,7 +248,7 @@ mod tests { r#type: Ctap2PublicKeyCredentialType::PublicKey, transports: None, }; - let serialized = serde_cbor::to_vec(&credential_descriptor).unwrap(); + let serialized = credential_descriptor.to_vec().unwrap(); // Known good, verified by hand with cbor.me playground let expected = hex::decode("a2626964414264747970656a7075626c69632d6b6579").unwrap(); assert_eq!(serialized, expected); diff --git a/libwebauthn/src/proto/ctap2/model/get_assertion.rs b/libwebauthn/src/proto/ctap2/model/get_assertion.rs index 4000f686..3e198336 100644 --- a/libwebauthn/src/proto/ctap2/model/get_assertion.rs +++ b/libwebauthn/src/proto/ctap2/model/get_assertion.rs @@ -11,6 +11,8 @@ use crate::{ webauthn::{Error, PlatformError}, }; +use crate::proto::ctap2::cbor::CborSerialize; + use super::{ Ctap2AuthTokenPermissionRole, Ctap2COSEAlgorithmIdentifier, Ctap2GetInfoResponse, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialUserEntity, @@ -247,7 +249,7 @@ impl Ctap2GetAssertionRequestExtensions { // saltEnc(0x02): Encryption of the one or two salts (called salt1 (32 bytes) and salt2 (32 bytes)) using the shared secret as follows: // One salt case: encrypt(shared secret, salt1) // Two salt case: encrypt(shared secret, salt1 || salt2) - let mut salts = input.salt1.to_vec(); + let mut salts = input.salt1.to_vec()?; if let Some(salt2) = input.salt2 { salts.extend(salt2); } diff --git a/libwebauthn/src/proto/ctap2/protocol.rs b/libwebauthn/src/proto/ctap2/protocol.rs index b09f8e9e..c6ef6766 100644 --- a/libwebauthn/src/proto/ctap2/protocol.rs +++ b/libwebauthn/src/proto/ctap2/protocol.rs @@ -1,9 +1,9 @@ use std::time::Duration; use async_trait::async_trait; -use serde_cbor::from_slice; use tracing::{debug, instrument, trace, warn}; +use crate::proto::ctap2::cbor::CborDeserialize; use crate::proto::ctap2::cbor::CborRequest; use crate::proto::ctap2::{Ctap2BioEnrollmentResponse, Ctap2CommandCode}; use crate::transport::error::{CtapError, Error, PlatformError}; @@ -22,7 +22,7 @@ const TIMEOUT_GET_INFO: Duration = Duration::from_millis(250); macro_rules! parse_cbor { ($type:ty, $data:expr) => {{ - match from_slice::<$type>($data) { + match CborDeserialize::<$type>::from_slice($data) { Ok(f) => f, Err(e) => { tracing::error!( diff --git a/libwebauthn/src/transport/cable/qr_code_device.rs b/libwebauthn/src/transport/cable/qr_code_device.rs index 857ddf7f..8c70b061 100644 --- a/libwebauthn/src/transport/cable/qr_code_device.rs +++ b/libwebauthn/src/transport/cable/qr_code_device.rs @@ -16,6 +16,7 @@ use tracing::{debug, error}; use super::known_devices::CableKnownDeviceInfoStore; use super::tunnel::{self, KNOWN_TUNNEL_DOMAINS}; use super::{channel::CableChannel, Cable}; +use crate::proto::ctap2::cbor::CborSerialize; use crate::transport::cable::advertisement::await_advertisement; use crate::transport::cable::crypto::{derive, KeyPurpose}; use crate::transport::cable::digit_encode; @@ -59,7 +60,7 @@ pub struct CableQrCode { impl ToString for CableQrCode { fn to_string(&self) -> String { - let serialized = serde_cbor::to_vec(self).unwrap(); + let serialized = self.to_vec().unwrap(); format!("FIDO:/{}", digit_encode(&serialized)) } } diff --git a/libwebauthn/src/transport/cable/tunnel.rs b/libwebauthn/src/transport/cable/tunnel.rs index a6481f16..d2f754c3 100644 --- a/libwebauthn/src/transport/cable/tunnel.rs +++ b/libwebauthn/src/transport/cable/tunnel.rs @@ -24,6 +24,7 @@ use tungstenite::client::IntoClientRequest; use super::channel::CableChannel; use super::known_devices::ClientPayload; use super::known_devices::{CableKnownDeviceInfo, CableKnownDeviceInfoStore}; +use crate::proto::ctap2::cbor::CborSerialize; use crate::proto::ctap2::cbor::{CborRequest, CborResponse}; use crate::proto::ctap2::{Ctap2CommandCode, Ctap2GetInfoResponse}; use crate::transport::cable::known_devices::CableKnownDeviceId; @@ -584,7 +585,7 @@ async fn connection_recv_initial( } }; - Ok(initial_message.info.to_vec()) + Ok(initial_message.info.to_vec()?) } async fn connection_recv_update(message: &[u8]) -> Result, Error> { @@ -682,7 +683,7 @@ async fn connection_recv( } CableTunnelMessageType::Ctap => { // Handle the CTAP message - let cbor_response: CborResponse = (&cable_message.payload.to_vec()) + let cbor_response: CborResponse = (&cable_message.payload.to_vec()?) .try_into() .or(Err(TransportError::InvalidFraming))?; diff --git a/libwebauthn/src/transport/error.rs b/libwebauthn/src/transport/error.rs index cade5ace..e03734da 100644 --- a/libwebauthn/src/transport/error.rs +++ b/libwebauthn/src/transport/error.rs @@ -1,3 +1,4 @@ +use crate::proto::ctap2::cbor::CborError; pub use crate::proto::CtapError; #[derive(thiserror::Error, Debug, PartialEq)] @@ -16,6 +17,8 @@ pub enum PlatformError { NotSupported, #[error("syntax error")] SyntaxError, + #[error("cbor serialization error: {0}")] + CborError(#[from] CborError), } #[derive(thiserror::Error, Debug, PartialEq)] @@ -59,3 +62,9 @@ impl From for Error { Error::Transport(TransportError::NegotiationFailed) } } + +impl From for Error { + fn from(error: CborError) -> Self { + Error::Platform(PlatformError::CborError(error)) + } +} diff --git a/serde-indexed b/serde-indexed new file mode 160000 index 00000000..4cf0c9d5 --- /dev/null +++ b/serde-indexed @@ -0,0 +1 @@ +Subproject commit 4cf0c9d56c2a5f2b2b9bea8f23ea46c1871ab941 From 5e738112446723163d6a22deeedab99772ff6178 Mon Sep 17 00:00:00 2001 From: Alfie Fresta Date: Sat, 7 Jun 2025 14:03:36 +0100 Subject: [PATCH 3/6] Upgrade to serde-indexed v0.2.0 --- .gitmodules | 3 -- Cargo.lock | 33 +++----------- libwebauthn/Cargo.toml | 2 +- libwebauthn/src/proto/ctap2/cbor/serde.rs | 3 +- .../proto/ctap2/model/authenticator_config.rs | 9 +++- .../src/proto/ctap2/model/bio_enrollment.rs | 30 ++++++++----- .../src/proto/ctap2/model/client_pin.rs | 44 ++++++------------- .../ctap2/model/credential_management.rs | 29 +++++++----- .../src/proto/ctap2/model/get_assertion.rs | 23 ++++++++-- libwebauthn/src/proto/ctap2/model/get_info.rs | 30 ++++++++++++- .../src/proto/ctap2/model/make_credential.rs | 19 +++++++- .../src/transport/cable/known_devices.rs | 6 ++- .../src/transport/cable/qr_code_device.rs | 13 ++++++ libwebauthn/src/transport/cable/tunnel.rs | 8 ++-- serde-indexed | 1 - 15 files changed, 159 insertions(+), 94 deletions(-) delete mode 160000 serde-indexed diff --git a/.gitmodules b/.gitmodules index 254a2d58..120a69f7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ [submodule "solo/src/ext"] path = solo/src/ext url = https://github.com/AlfioEmanueleFresta/solo.git -[submodule "serde-indexed"] - path = serde-indexed - url = git@github.com:AlfioEmanueleFresta/serde-indexed.git diff --git a/Cargo.lock b/Cargo.lock index dc01b25e..d05cb0b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -536,7 +536,7 @@ dependencies = [ "heapless-bytes", "iso7816", "serde", - "serde-indexed 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde-indexed 0.1.1", "serde_bytes", "serde_repr", ] @@ -1013,12 +1013,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hex-literal" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" - [[package]] name = "hidapi" version = "2.6.3" @@ -1248,7 +1242,7 @@ dependencies = [ "qrcode", "rand 0.8.5", "serde", - "serde-indexed 0.1.1", + "serde-indexed 0.2.0", "serde_bytes", "serde_cbor", "serde_derive", @@ -2029,35 +2023,22 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-byte-array" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63213ee4ed648dbd87db6fa993d4275b46bfb4ddfd95b3756045007c2b28f742" -dependencies = [ - "serde", -] - [[package]] name = "serde-indexed" version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fca2da10b1f1623f47130256065e05e94fd7a98dbd26a780a4c5de831b21e5c2" dependencies = [ - "heapless", - "hex-literal", "proc-macro2", "quote", - "serde", - "serde-byte-array", - "serde_bytes", - "serde_cbor", - "syn 2.0.101", + "syn", ] [[package]] name = "serde-indexed" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca2da10b1f1623f47130256065e05e94fd7a98dbd26a780a4c5de831b21e5c2" +checksum = "8f68cf7478db8b81abcf71b6d195a34a4891bd3d39868731c4d73194d74ec7a3" dependencies = [ "proc-macro2", "quote", diff --git a/libwebauthn/Cargo.toml b/libwebauthn/Cargo.toml index ae965e11..a1542410 100644 --- a/libwebauthn/Cargo.toml +++ b/libwebauthn/Cargo.toml @@ -29,7 +29,7 @@ futures = "0.3.5" tokio = { version = "1.45", features = ["full"] } serde = "1.0.110" serde_cbor = "0.11.2" -serde-indexed = { path = "../serde-indexed" } +serde-indexed = "0.2.0" serde_derive = "1.0.123" serde_repr = "0.1.6" serde_bytes = "0.11.5" diff --git a/libwebauthn/src/proto/ctap2/cbor/serde.rs b/libwebauthn/src/proto/ctap2/cbor/serde.rs index 636895c3..eac89a2b 100644 --- a/libwebauthn/src/proto/ctap2/cbor/serde.rs +++ b/libwebauthn/src/proto/ctap2/cbor/serde.rs @@ -53,9 +53,10 @@ mod tests { use serde_indexed::{DeserializeIndexed, SerializeIndexed}; #[derive(Debug, PartialEq, SerializeIndexed, DeserializeIndexed)] - #[serde_indexed(offset = 1)] struct TestStruct { + #[serde(index = 0x01)] pub a: u8, + #[serde(index = 0x02)] pub b: u8, } diff --git a/libwebauthn/src/proto/ctap2/model/authenticator_config.rs b/libwebauthn/src/proto/ctap2/model/authenticator_config.rs index 260ff39b..71a96278 100644 --- a/libwebauthn/src/proto/ctap2/model/authenticator_config.rs +++ b/libwebauthn/src/proto/ctap2/model/authenticator_config.rs @@ -6,21 +6,24 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use super::Ctap2PinUvAuthProtocol; #[derive(Debug, Clone, SerializeIndexed)] -#[serde_indexed(offset = 1)] pub struct Ctap2AuthenticatorConfigRequest { // subCommand (0x01) + #[serde(index = 0x01)] pub subcommand: Ctap2AuthenticatorConfigCommand, // subCommandParams (0x02) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x02)] pub subcommand_params: Option, ///pinUvAuthProtocol (0x03) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x03)] pub protocol: Option, /// pinUvAuthParam (0x04): #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x04)] pub uv_auth_param: Option, } @@ -106,17 +109,19 @@ pub enum Ctap2AuthenticatorConfigParams { } #[derive(Debug, Clone, SerializeIndexed)] -#[serde_indexed(offset = 1)] pub struct Ctap2SetMinPINLengthParams { // newMinPINLength (0x01) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x01)] pub new_min_pin_length: Option, // minPinLengthRPIDs (0x02) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x02)] pub min_pin_length_rpids: Option>, // forceChangePin (0x03) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x03)] pub force_change_pin: Option, } diff --git a/libwebauthn/src/proto/ctap2/model/bio_enrollment.rs b/libwebauthn/src/proto/ctap2/model/bio_enrollment.rs index f6088a42..a2304fc2 100644 --- a/libwebauthn/src/proto/ctap2/model/bio_enrollment.rs +++ b/libwebauthn/src/proto/ctap2/model/bio_enrollment.rs @@ -9,29 +9,35 @@ use std::time::Duration; pub struct Ctap2BioEnrollmentRequest { // modality (0x01) Unsigned Integer Optional The user verification modality being requested #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x01)] pub modality: Option, // subCommand (0x02) Unsigned Integer Optional The authenticator user verification sub command currently being requested #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x02)] pub subcommand: Option, // subCommandParams (0x03) CBOR Map Optional Map of subCommands parameters. This parameter MAY be omitted when the subCommand does not take any arguments. #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x03)] pub subcommand_params: Option, // pinUvAuthProtocol (0x04) Unsigned Integer Optional PIN/UV protocol version chosen by the platform. #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x04)] pub protocol: Option, // pinUvAuthParam (0x05) Byte String Optional First 16 bytes of HMAC-SHA-256 of contents using pinUvAuthToken. #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x05)] pub uv_auth_param: Option, // getModality (0x06) Boolean Optional Get the user verification type modality. This MUST be set to true. #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x06)] pub get_modality: Option, - #[serde(skip_serializing_if = "always_skip_bool")] + #[serde(skip)] pub use_legacy_preview: bool, } @@ -48,63 +54,73 @@ pub enum Ctap2BioEnrollmentSubcommand { } #[derive(Debug, Clone, SerializeIndexed)] -#[serde_indexed(offset = 1)] pub struct Ctap2BioEnrollmentParams { #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x01)] template_id: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x02)] template_friendly_name: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x03)] timeout_milliseconds: Option, } #[derive(Debug, Default, Clone, DeserializeIndexed)] -#[serde_indexed(offset = 1)] pub struct Ctap2BioEnrollmentResponse { // modality (0x01) Unsigned Integer Optional The user verification modality. #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x01)] pub modality: Option, // fingerprintKind (0x02) Unsigned Integer Optional Indicates the type of fingerprint sensor. For touch type sensor, its value is 1. For swipe type sensor its value is 2. #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x02)] pub fingerprint_kind: Option, // maxCaptureSamplesRequiredForEnroll (0x03) Unsigned Integer Optional Indicates the maximum good samples required for enrollment. #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x03)] pub max_capture_samples_required_for_enroll: Option, // templateId (0x04) Byte String Optional Template Identifier. #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x04)] pub template_id: Option, // lastEnrollSampleStatus (0x05) Unsigned Integer Optional Last enrollment sample status. #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x05)] pub last_enroll_sample_status: Option, // remainingSamples (0x06) Unsigned Integer Optional Number of more sample required for enrollment to complete #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x06)] pub remaining_samples: Option, // templateInfos (0x07) CBOR ARRAY Optional Array of templateInfo’s #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x07)] pub template_infos: Option>, // maxTemplateFriendlyName (0x08) Unsigned Integer Optional Indicates the maximum number of bytes the authenticator will accept as a templateFriendlyName. #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x08)] pub max_template_friendly_name: Option, } #[derive(Debug, Clone, DeserializeIndexed)] -#[serde_indexed(offset = 1)] pub struct Ctap2BioEnrollmentTemplateId { // templateId (0x01) Byte String Required Template Identifier. #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x01)] pub template_id: Option, // templateFriendlyName (0x02) String Optional Template Friendly Name. #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x02)] pub template_friendly_name: Option, } @@ -262,9 +278,3 @@ impl Ctap2BioEnrollmentRequest { } } } - -// Required by serde_indexed, as serde(skip) isn't supported yet: -// https://github.com/trussed-dev/serde-indexed/pull/14 -fn always_skip_bool(_v: &bool) -> bool { - true -} diff --git a/libwebauthn/src/proto/ctap2/model/client_pin.rs b/libwebauthn/src/proto/ctap2/model/client_pin.rs index 94f9bbf2..62031664 100644 --- a/libwebauthn/src/proto/ctap2/model/client_pin.rs +++ b/libwebauthn/src/proto/ctap2/model/client_pin.rs @@ -10,39 +10,41 @@ use crate::pin::{PinUvAuthProtocol, PinUvAuthProtocolOne, PinUvAuthProtocolTwo}; pub struct Ctap2ClientPinRequest { ///pinUvAuthProtocol (0x01) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x01)] pub protocol: Option, /// subCommand (0x02) + #[serde(index = 0x02)] pub command: Ctap2PinUvAuthProtocolCommand, /// keyAgreement (0x03) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x03)] pub key_agreement: Option, /// pinUvAuthParam (0x04): #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x04)] pub uv_auth_param: Option, /// newPinEnc (0x05) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x05)] pub new_pin_encrypted: Option, /// pinHashEnc (0x06) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x06)] pub pin_hash_encrypted: Option, - #[serde(skip_serializing_if = "always_skip")] - pub unused_07: (), - - #[serde(skip_serializing_if = "always_skip")] - pub unused_08: (), - /// permissions (0x09) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x09)] pub permissions: Option, - /// permissions RPID (0x10) + /// permissions RPID (0x0A) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x0A)] pub permissions_rpid: Option, } @@ -55,8 +57,6 @@ impl Ctap2ClientPinRequest { uv_auth_param: None, new_pin_encrypted: None, pin_hash_encrypted: None, - unused_07: (), - unused_08: (), permissions: None, permissions_rpid: None, } @@ -74,8 +74,6 @@ impl Ctap2ClientPinRequest { uv_auth_param: None, new_pin_encrypted: None, pin_hash_encrypted: Some(ByteBuf::from(pin_hash_enc)), - unused_07: (), - unused_08: (), permissions: None, permissions_rpid: None, } @@ -89,8 +87,6 @@ impl Ctap2ClientPinRequest { uv_auth_param: None, new_pin_encrypted: None, pin_hash_encrypted: None, - unused_07: (), - unused_08: (), permissions: None, permissions_rpid: None, } @@ -110,8 +106,6 @@ impl Ctap2ClientPinRequest { uv_auth_param: None, new_pin_encrypted: None, pin_hash_encrypted: Some(ByteBuf::from(pin_hash_enc)), - unused_07: (), - unused_08: (), permissions: Some(permissions.bits()), permissions_rpid: permissions_rpid.map(str::to_owned), } @@ -130,8 +124,6 @@ impl Ctap2ClientPinRequest { uv_auth_param: None, new_pin_encrypted: None, pin_hash_encrypted: None, - unused_07: (), - unused_08: (), permissions: Some(permissions.bits()), permissions_rpid: permissions_rpid.map(str::to_owned), } @@ -146,8 +138,6 @@ impl Ctap2ClientPinRequest { uv_auth_param: None, new_pin_encrypted: None, pin_hash_encrypted: None, - unused_07: (), - unused_08: (), permissions: None, permissions_rpid: None, } @@ -167,8 +157,6 @@ impl Ctap2ClientPinRequest { uv_auth_param: Some(ByteBuf::from(uv_auth_param)), new_pin_encrypted: Some(ByteBuf::from(new_pin_enc)), pin_hash_encrypted: Some(ByteBuf::from(curr_pin_enc)), - unused_07: (), - unused_08: (), permissions: None, permissions_rpid: None, } @@ -187,8 +175,6 @@ impl Ctap2ClientPinRequest { uv_auth_param: Some(ByteBuf::from(uv_auth_param)), new_pin_encrypted: Some(ByteBuf::from(new_pin_enc)), pin_hash_encrypted: None, - unused_07: (), - unused_08: (), permissions: None, permissions_rpid: None, } @@ -237,31 +223,29 @@ pub enum Ctap2PinUvAuthProtocolCommand { } #[derive(Debug, Clone, Default, DeserializeIndexed)] -#[serde_indexed(offset = 1)] pub struct Ctap2ClientPinResponse { /// keyAgreement (0x01) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x01)] pub key_agreement: Option, /// pinUvAuthToken (0x02) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x02)] pub pin_uv_auth_token: Option, /// pinRetries (0x03) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x03)] pub pin_retries: Option, /// powerCycleState (0x04) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x04)] pub power_cycle_state: Option, /// uvRetries (0x05) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x05)] pub uv_retries: Option, } - -// Required by serde_indexed, as serde(skip) isn't supported yet: -// https://github.com/trussed-dev/serde-indexed/pull/14 -fn always_skip(_v: &()) -> bool { - true -} diff --git a/libwebauthn/src/proto/ctap2/model/credential_management.rs b/libwebauthn/src/proto/ctap2/model/credential_management.rs index 45db8583..37d75a8e 100644 --- a/libwebauthn/src/proto/ctap2/model/credential_management.rs +++ b/libwebauthn/src/proto/ctap2/model/credential_management.rs @@ -8,25 +8,28 @@ use serde_indexed::{DeserializeIndexed, SerializeIndexed}; use serde_repr::{Deserialize_repr, Serialize_repr}; #[derive(Debug, Clone, SerializeIndexed)] -#[serde_indexed(offset = 1)] pub struct Ctap2CredentialManagementRequest { //subCommand (0x01) Unsigned Integer subCommand currently being requested #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x01)] pub subcommand: Option, //subCommandParams (0x02) CBOR Map Map of subCommands parameters. #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x02)] pub subcommand_params: Option, //pinUvAuthProtocol (0x03) Unsigned Integer PIN/UV protocol version chosen by the platform. #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x03)] pub protocol: Option, //pinUvAuthParam (0x04) Byte String First 16 bytes of HMAC-SHA-256 of contents using pinUvAuthToken. #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x04)] pub uv_auth_param: Option, - #[serde(skip_serializing_if = "always_skip_bool")] + #[serde(skip)] pub use_legacy_preview: bool, } @@ -43,66 +46,78 @@ pub enum Ctap2CredentialManagementSubcommand { } #[derive(Debug, Clone, SerializeIndexed)] -#[serde_indexed(offset = 1)] pub struct Ctap2CredentialManagementParams { // rpIDHash (0x01) Byte String RP ID SHA-256 hash #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x01)] rpid_hash: Option, // credentialID (0x02) PublicKeyCredentialDescriptor Credential Identifier #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x02)] credential_id: Option, // user (0x03) PublicKeyCredentialUserEntity User Entity #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x03)] user: Option, } #[derive(Debug, Default, Clone, DeserializeIndexed)] -#[serde_indexed(offset = 1)] pub struct Ctap2CredentialManagementResponse { // existingResidentCredentialsCount (0x01) Unsigned Integer Number of existing discoverable credentials present on the authenticator. #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x01)] pub existing_resident_credentials_count: Option, // maxPossibleRemainingResidentCredentialsCount (0x02) Unsigned Integer Number of maximum possible remaining discoverable credentials which can be created on the authenticator. #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x02)] pub max_possible_remaining_resident_credentials_count: Option, // rp (0x03) PublicKeyCredentialRpEntity RP Information #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x03)] pub rp: Option, // rpIDHash (0x04) Byte String RP ID SHA-256 hash #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x04)] pub rp_id_hash: Option, // totalRPs (0x05) Unsigned Integer total number of RPs present on the authenticator #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x05)] pub total_rps: Option, // user (0x06) PublicKeyCredentialUserEntity User Information #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x06)] pub user: Option, // credentialID (0x07) PublicKeyCredentialDescriptor PublicKeyCredentialDescriptor #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x07)] pub credential_id: Option, // publicKey (0x08) COSE_Key Public key of the credential. #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x08)] pub public_key: Option, // totalCredentials (0x09) Unsigned Integer Total number of credentials present on the authenticator for the RP in question #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x09)] pub total_credentials: Option, // credProtect (0x0A) Unsigned Integer Credential protection policy. #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x0A)] pub cred_protect: Option, // largeBlobKey (0x0B) Byte string Large blob encryption key. #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x0B)] pub large_blob_key: Option, } @@ -252,9 +267,3 @@ impl Ctap2RPData { Self { rp, rp_id_hash } } } - -// Required by serde_indexed, as serde(skip) isn't supported yet: -// https://github.com/trussed-dev/serde-indexed/pull/14 -fn always_skip_bool(_v: &bool) -> bool { - true -} diff --git a/libwebauthn/src/proto/ctap2/model/get_assertion.rs b/libwebauthn/src/proto/ctap2/model/get_assertion.rs index 3e198336..e1aab464 100644 --- a/libwebauthn/src/proto/ctap2/model/get_assertion.rs +++ b/libwebauthn/src/proto/ctap2/model/get_assertion.rs @@ -107,32 +107,38 @@ pub enum Ctap2AttestationStatement { // https://www.w3.org/TR/webauthn/#op-get-assertion #[derive(Debug, Clone, SerializeIndexed)] -#[serde_indexed(offset = 1)] pub struct Ctap2GetAssertionRequest { /// rpId (0x01) + #[serde(index = 0x01)] pub relying_party_id: String, /// clientDataHash (0x02) + #[serde(index = 0x02)] pub client_data_hash: ByteBuf, /// allowList (0x03) #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(index = 0x03)] pub allow: Vec, /// extensions (0x04) #[serde(skip_serializing_if = "Self::skip_serializing_extensions")] + #[serde(index = 0x04)] pub extensions: Option, /// options (0x05) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x05)] pub options: Option, /// pinUvAuthParam (0x06) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x06)] pub pin_auth_param: Option, /// pinUvAuthProtocol (0x07) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x07)] pub pin_auth_proto: Option, } @@ -350,45 +356,56 @@ impl Ctap2GetAssertionRequestExtensions { } #[derive(Debug, Clone, SerializeIndexed)] -#[serde_indexed(offset = 1)] pub struct CalculatedHMACGetSecretInput { // keyAgreement(0x01): public key of platform key-agreement key. + #[serde(index = 0x01)] pub public_key: PublicKey, // saltEnc(0x02): Encryption of the one or two salts + #[serde(index = 0x02)] pub salt_enc: ByteBuf, // saltAuth(0x03): authenticate(shared secret, saltEnc) + #[serde(index = 0x03)] pub salt_auth: ByteBuf, // pinUvAuthProtocol(0x04): (optional) as selected when getting the shared secret. CTAP2.1 platforms MUST include this parameter if the value of pinUvAuthProtocol is not 1. #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x04)] pub pin_auth_proto: Option, } #[derive(Debug, Clone, DeserializeIndexed)] -#[serde_indexed(offset = 1)] pub struct Ctap2GetAssertionResponse { #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x01)] pub credential_id: Option, + #[serde(index = 0x02)] pub authenticator_data: AuthenticatorData, + #[serde(index = 0x03)] pub signature: ByteBuf, #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x04)] pub user: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x05)] pub credentials_count: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x06)] pub user_selected: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x07)] pub large_blob_key: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x08)] pub enterprise_attestation: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x09)] pub attestation_statement: Option, } diff --git a/libwebauthn/src/proto/ctap2/model/get_info.rs b/libwebauthn/src/proto/ctap2/model/get_info.rs index f4d6c79d..584270fc 100644 --- a/libwebauthn/src/proto/ctap2/model/get_info.rs +++ b/libwebauthn/src/proto/ctap2/model/get_info.rs @@ -7,120 +7,148 @@ use tracing::debug; use super::{Ctap2CredentialType, Ctap2UserVerificationOperation}; #[derive(Debug, Clone, DeserializeIndexed)] -#[serde_indexed(offset = 1)] pub struct Ctap2GetInfoResponse { /// versions (0x01) + #[serde(index = 0x01)] pub versions: Vec, /// extensions (0x02) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x02)] pub extensions: Option>, /// aaguid (0x03) + #[serde(index = 0x03)] pub aaguid: ByteBuf, /// options (0x04) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x04)] pub options: Option>, /// maxMsgSize (0x05) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x05)] pub max_msg_size: Option, /// pinUvAuthProtocols (0x06) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x06)] pub pin_auth_protos: Option>, /// maxCredentialCountInList (0x07) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x07)] pub max_credential_count: Option, /// maxCredentialIdLength (0x08) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x08)] pub max_credential_id_length: Option, /// transports (0x09) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x09)] pub transports: Option>, /// algorithms (0x0A) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x0A)] pub algorithms: Option>, /// maxSerializedLargeBlobArray (0x0B) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x0B)] pub max_blob_array: Option, /// forcePINChange (0x0C) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x0C)] pub force_pin_change: Option, /// minPINLength (0x0D) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x0D)] pub min_pin_length: Option, /// firmwareVersion (0x0E) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x0E)] pub firmware_version: Option, /// maxCredBlobLength (0x0F) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x0F)] pub max_cred_blob_length: Option, /// maxRPIDsForSetMinPINLength (0x10) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x10)] pub max_rpids_for_setminpinlength: Option, /// preferredPlatformUvAttempts (0x11) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x11)] pub preferred_platform_uv_attempts: Option, /// uvModality (0x12) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x12)] pub uv_modality: Option, /// certifications (0x13) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x13)] pub certifications: Option>, /// remainingDiscoverableCredentials (0x14) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x14)] pub remaining_discoverable_creds: Option, /// vendorPrototypeConfigCommands (0x15) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x15)] pub vendor_proto_config_cmds: Option>, /// attestationFormats (0x16) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x16)] pub attestation_formats: Option>, /// uvCountSinceLastPinEntry (0x17) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x17)] pub uv_count_since_last_pin_entry: Option, /// longTouchForReset (0x18) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x18)] pub long_touch_for_reset: Option, /// encIdentifier (0x19) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x19)] pub enc_identifier: Option, /// transportsForReset (0x1A) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x1A)] pub transports_for_reset: Option>, /// pinComplexityPolicy (0x1B) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x1B)] pub pin_complexity_policy: Option, /// pinComplexityPolicyURL (0x1C) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x1C)] pub pin_complexity_policy_url: Option, /// maxPINLength (0x1D) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x1D)] pub max_pin_length: Option, } diff --git a/libwebauthn/src/proto/ctap2/model/make_credential.rs b/libwebauthn/src/proto/ctap2/model/make_credential.rs index 19fab8b1..fce32a95 100644 --- a/libwebauthn/src/proto/ctap2/model/make_credential.rs +++ b/libwebauthn/src/proto/ctap2/model/make_credential.rs @@ -39,42 +39,51 @@ impl Ctap2MakeCredentialOptions { // https://www.w3.org/TR/webauthn/#authenticatormakecredential #[derive(Debug, Clone, SerializeIndexed)] -#[serde_indexed(offset = 1)] pub struct Ctap2MakeCredentialRequest { /// clientDataHash (0x01) + #[serde(index = 0x01)] pub hash: ByteBuf, /// rp (0x02) + #[serde(index = 0x02)] pub relying_party: Ctap2PublicKeyCredentialRpEntity, /// user (0x03) + #[serde(index = 0x03)] pub user: Ctap2PublicKeyCredentialUserEntity, /// pubKeyCredParams (0x04) + #[serde(index = 0x04)] pub algorithms: Vec, /// excludeList (0x05) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x05)] pub exclude: Option>, /// extensions (0x06) #[serde(skip_serializing_if = "Self::skip_serializing_extensions")] + #[serde(index = 0x06)] pub extensions: Option, /// options (0x07) #[serde(skip_serializing_if = "Self::skip_serializing_options")] + #[serde(index = 0x07)] pub options: Option, /// pinUvAuthParam (0x08) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x08)] pub pin_auth_param: Option, /// pinUvAuthProtocol (0x09) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x09)] pub pin_auth_proto: Option, /// enterpriseAttestation (0x0A) #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x0A)] pub enterprise_attestation: Option, } @@ -235,16 +244,22 @@ impl From for Ctap2MakeCredentialsRequestExten } #[derive(Debug, Clone, DeserializeIndexed)] -#[serde_indexed(offset = 1)] pub struct Ctap2MakeCredentialResponse { + #[serde(index = 0x01)] pub format: String, + + #[serde(index = 0x02)] pub authenticator_data: AuthenticatorData, + + #[serde(index = 0x03)] pub attestation_statement: Ctap2AttestationStatement, #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x04)] pub enterprise_attestation: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x05)] pub large_blob_key: Option, } diff --git a/libwebauthn/src/transport/cable/known_devices.rs b/libwebauthn/src/transport/cable/known_devices.rs index e6942ea6..ebc111ed 100644 --- a/libwebauthn/src/transport/cable/known_devices.rs +++ b/libwebauthn/src/transport/cable/known_devices.rs @@ -205,10 +205,14 @@ type ClientNonce = [u8; 16]; // Key 3: either the string “ga” to hint that a getAssertion will follow, or “mc” to hint that a makeCredential will follow. #[derive(Clone, Debug, SerializeIndexed)] -#[serde(offset = 1)] pub struct ClientPayload { + #[serde(index = 0x01)] pub link_id: ByteBuf, + + #[serde(index = 0x02)] pub client_nonce: ByteBuf, + + #[serde(index = 0x03)] pub hint: ClientPayloadHint, } diff --git a/libwebauthn/src/transport/cable/qr_code_device.rs b/libwebauthn/src/transport/cable/qr_code_device.rs index 8c70b061..3812d798 100644 --- a/libwebauthn/src/transport/cable/qr_code_device.rs +++ b/libwebauthn/src/transport/cable/qr_code_device.rs @@ -36,25 +36,38 @@ pub enum QrCodeOperationHint { #[derive(Debug, SerializeIndexed)] pub struct CableQrCode { // Key 0: a 33-byte, P-256, X9.62, compressed public key. + #[serde(index = 0x00)] pub public_key: ByteArray<33>, + // Key 1: a 16-byte random QR secret. + #[serde(index = 0x01)] pub qr_secret: ByteArray<16>, + /// Key 2: the number of assigned tunnel server domains known to this implementation. + #[serde(index = 0x02)] pub known_tunnel_domains_count: u8, + /// Key 3: (optional) the current time in epoch seconds. #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x03)] pub current_time: Option, + /// Key 4: (optional) a boolean that is true if the device displaying the QR code can perform state- /// assisted transactions. #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x04)] pub state_assisted: Option, + /// Key 5: either the string “ga” to hint that a getAssertion will follow, or “mc” to hint that a /// makeCredential will follow. Implementations SHOULD treat unknown values as if they were “ga”. /// This field exists so that guidance can be given to the user immediately upon scanning the QR code, /// prior to the authenticator receiving any CTAP message. While this hint SHOULD be as accurate as /// possible, it does not constrain the subsequent CTAP messages that the platform may send. + #[serde(index = 0x05)] pub operation_hint: QrCodeOperationHint, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x06)] pub supports_non_discoverable_mc: Option, } diff --git a/libwebauthn/src/transport/cable/tunnel.rs b/libwebauthn/src/transport/cable/tunnel.rs index d2f754c3..50924a30 100644 --- a/libwebauthn/src/transport/cable/tunnel.rs +++ b/libwebauthn/src/transport/cable/tunnel.rs @@ -86,14 +86,16 @@ impl CableTunnelMessage { } #[derive(Clone, Debug, DeserializeIndexed)] -#[serde_indexed(offset = 0)] struct CableInitialMessage { #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x00)] pub _padding: Option, + + #[serde(index = 0x01)] pub info: ByteBuf, + #[serde(skip_serializing_if = "Option::is_none")] - pub _unused_2: Option<()>, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(index = 0x03)] pub _supported_features: Option>, } diff --git a/serde-indexed b/serde-indexed deleted file mode 160000 index 4cf0c9d5..00000000 --- a/serde-indexed +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4cf0c9d56c2a5f2b2b9bea8f23ea46c1871ab941 From a69e374a438a3122da9cd08079e6b5eea1e16fe8 Mon Sep 17 00:00:00 2001 From: Alfie Fresta Date: Sat, 14 Jun 2025 19:37:46 +0200 Subject: [PATCH 4/6] Replace trait in favour of generic methods --- libwebauthn/src/fido.rs | 17 ++--- .../src/management/authenticator_config.rs | 4 +- libwebauthn/src/management/bio_enrollment.rs | 10 ++- .../src/management/credential_management.rs | 8 +-- libwebauthn/src/ops/u2f.rs | 4 +- libwebauthn/src/proto/ctap2/cbor/mod.rs | 2 +- libwebauthn/src/proto/ctap2/cbor/request.rs | 14 ++--- libwebauthn/src/proto/ctap2/cbor/serde.rs | 62 ++++++++++++------- libwebauthn/src/proto/ctap2/model.rs | 6 +- .../src/proto/ctap2/model/get_assertion.rs | 5 +- libwebauthn/src/proto/ctap2/protocol.rs | 5 +- libwebauthn/src/transport/cable/crypto.rs | 19 +++--- .../src/transport/cable/qr_code_device.rs | 4 +- libwebauthn/src/transport/cable/tunnel.rs | 9 ++- 14 files changed, 94 insertions(+), 75 deletions(-) diff --git a/libwebauthn/src/fido.rs b/libwebauthn/src/fido.rs index 090e2f1d..1cc4bcbb 100644 --- a/libwebauthn/src/fido.rs +++ b/libwebauthn/src/fido.rs @@ -12,7 +12,7 @@ use std::{ }; use tracing::{error, warn}; -use crate::proto::ctap2::cbor::{CborDeserialize, CborSerialize}; +use crate::proto::ctap2::cbor; use crate::{ proto::{ ctap2::{Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialType}, @@ -96,7 +96,7 @@ where // signCount | 4 // attestedCredentialData | variable // extensions | variable - let mut res = self.rp_id_hash.to_vec().map_err(|e| { + let mut res = cbor::to_vec(&self.rp_id_hash).map_err(|e| { error!("Failed to create AuthenticatorData output vec at rp_id_hash: {e:?}"); Error::Platform(e.into()) })?; @@ -124,10 +124,11 @@ where })?; res.extend(&att_data.credential_id); let cose_encoded_public_key = - att_data.credential_public_key.to_vec() + cbor::to_vec(&att_data.credential_public_key) .map_err(|e| { error!( - "Failed to create AuthenticatorData output vec at attested_credential.credential_public_key: {e:?}" + %e, + "Failed to create AuthenticatorData output vec at attested_credential.credential_public_key" ); Error::Platform(PlatformError::InvalidDeviceResponse) })?; @@ -136,8 +137,8 @@ where if self.extensions.is_some() || self.flags.contains(AuthenticatorDataFlags::EXTENSION_DATA) { - res.extend(self.extensions.to_vec().map_err(|e| { - error!("Failed to create AuthenticatorData output vec at extensions: {e:?}"); + res.extend(cbor::to_vec(&self.extensions).map_err(|e| { + error!(%e, "Failed to create AuthenticatorData output vec at extensions"); Error::Platform(PlatformError::InvalidDeviceResponse) })?); } @@ -220,7 +221,7 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for AuthenticatorData { cursor.read_exact(&mut credential_id).unwrap(); // We checked the length let credential_public_key: PublicKey = - CborDeserialize::from_reader(&mut cursor).map_err(DesError::custom)?; + cbor::from_reader(&mut cursor).map_err(DesError::custom)?; attested_credential = Some(AttestedCredentialData { aaguid, @@ -231,7 +232,7 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for AuthenticatorData { let extensions: Option = if flags.contains(AuthenticatorDataFlags::EXTENSION_DATA) { - serde_cbor::from_reader(&mut cursor).map_err(DesError::custom)? + cbor::from_reader(&mut cursor).map_err(DesError::custom)? } else { Default::default() }; diff --git a/libwebauthn/src/management/authenticator_config.rs b/libwebauthn/src/management/authenticator_config.rs index 19f1d84a..748300fa 100644 --- a/libwebauthn/src/management/authenticator_config.rs +++ b/libwebauthn/src/management/authenticator_config.rs @@ -1,4 +1,4 @@ -use crate::proto::ctap2::cbor::CborSerialize; +use crate::proto::ctap2::cbor; use crate::proto::ctap2::Ctap2ClientPinRequest; pub use crate::transport::error::{CtapError, Error}; use crate::transport::Channel; @@ -173,7 +173,7 @@ impl Ctap2UserVerifiableRequest for Ctap2AuthenticatorConfigRequest { data.push(0x0D); data.push(self.subcommand as u8); if self.subcommand == Ctap2AuthenticatorConfigCommand::SetMinPINLength { - data.extend((&self.subcommand_params).to_vec().unwrap()); + data.extend(cbor::to_vec(&self.subcommand_params).unwrap()); } let uv_auth_param = uv_proto.authenticate(uv_auth_token, &data); self.protocol = Some(uv_proto.version()); diff --git a/libwebauthn/src/management/bio_enrollment.rs b/libwebauthn/src/management/bio_enrollment.rs index c8762ec9..9fa219bb 100644 --- a/libwebauthn/src/management/bio_enrollment.rs +++ b/libwebauthn/src/management/bio_enrollment.rs @@ -1,4 +1,4 @@ -use crate::proto::ctap2::cbor::CborSerialize; +use crate::proto::ctap2::cbor; use crate::{ ops::webauthn::UserVerificationRequirement, pin::PinUvAuthProtocol, @@ -213,7 +213,11 @@ where let remaining_samples = unwrap_field!(resp.remaining_samples); let template_id = unwrap_field!(resp.template_id).clone(); let sample_status = unwrap_field!(resp.last_enroll_sample_status); - Ok((template_id.to_vec()?, sample_status, remaining_samples)) + Ok(( + cbor::to_vec(&template_id)?, + sample_status, + remaining_samples, + )) } async fn capture_next_bio_enrollment_sample( @@ -295,7 +299,7 @@ impl Ctap2UserVerifiableRequest for Ctap2BioEnrollmentRequest { }; // e.g. "Authenticator calls verify(pinUvAuthToken, fingerprint (0x01) || removeEnrollment (0x06) || subCommandParams, pinUvAuthParam)" if let Some(params) = &self.subcommand_params { - data.extend(params.to_vec().unwrap()); + data.extend(cbor::to_vec(¶ms).unwrap()); } let uv_auth_param = uv_proto.authenticate(uv_auth_token, &data); self.protocol = Some(uv_proto.version()); diff --git a/libwebauthn/src/management/credential_management.rs b/libwebauthn/src/management/credential_management.rs index 65e54666..63a4ffca 100644 --- a/libwebauthn/src/management/credential_management.rs +++ b/libwebauthn/src/management/credential_management.rs @@ -1,4 +1,4 @@ -use crate::proto::ctap2::cbor::CborSerialize; +use crate::proto::ctap2::cbor; use crate::{ ops::webauthn::UserVerificationRequirement, pin::PinUvAuthProtocol, @@ -111,7 +111,7 @@ where Ok(( Ctap2RPData::new( unwrap_field!(resp.rp), - unwrap_field!(resp.rp_id_hash).to_vec()?, + cbor::to_vec(&unwrap_field!(&resp.rp_id_hash))?, ), unwrap_field!(resp.total_rps), )) @@ -138,7 +138,7 @@ where }?; Ok(Ctap2RPData::new( unwrap_field!(resp.rp), - unwrap_field!(resp.rp_id_hash).to_vec()?, + cbor::to_vec(unwrap_field!(&resp.rp_id_hash))?, )) } @@ -282,7 +282,7 @@ impl Ctap2UserVerifiableRequest for Ctap2CredentialManagementRequest { // e.g. pinUvAuthParam (0x04): authenticate(pinUvAuthToken, enumerateCredentialsBegin (0x04) || subCommandParams). if let Some(params) = &self.subcommand_params { - data.extend(params.to_vec().unwrap()); + data.extend(cbor::to_vec(¶ms).unwrap()); } let uv_auth_param = uv_proto.authenticate(uv_auth_token, &data); self.protocol = Some(uv_proto.version()); diff --git a/libwebauthn/src/ops/u2f.rs b/libwebauthn/src/ops/u2f.rs index a37986ed..16321d36 100644 --- a/libwebauthn/src/ops/u2f.rs +++ b/libwebauthn/src/ops/u2f.rs @@ -13,7 +13,7 @@ use crate::ops::webauthn::{ }; use crate::proto::ctap1::{Ctap1RegisterRequest, Ctap1SignRequest}; use crate::proto::ctap1::{Ctap1RegisterResponse, Ctap1SignResponse}; -use crate::proto::ctap2::cbor::CborSerialize; +use crate::proto::ctap2::cbor; use crate::proto::ctap2::{ Ctap2AttestationStatement, Ctap2COSEAlgorithmIdentifier, Ctap2GetAssertionResponse, Ctap2MakeCredentialResponse, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialType, @@ -79,7 +79,7 @@ impl UpgradableResponse for Regis x: x.into(), y: y.into(), }); - let cose_encoded_public_key = cose_public_key.to_vec()?; + let cose_encoded_public_key = cbor::to_vec(&cose_public_key)?; assert!(cose_encoded_public_key.len() == 77); // Let attestedCredData be a byte string with following structure: diff --git a/libwebauthn/src/proto/ctap2/cbor/mod.rs b/libwebauthn/src/proto/ctap2/cbor/mod.rs index 4ae0fbb2..6226c2f1 100644 --- a/libwebauthn/src/proto/ctap2/cbor/mod.rs +++ b/libwebauthn/src/proto/ctap2/cbor/mod.rs @@ -4,4 +4,4 @@ mod serde; pub use request::CborRequest; pub use response::CborResponse; -pub(crate) use serde::{CborDeserialize, CborError, CborSerialize}; +pub(crate) use serde::{from_reader, from_slice, to_vec, CborError, Value}; diff --git a/libwebauthn/src/proto/ctap2/cbor/request.rs b/libwebauthn/src/proto/ctap2/cbor/request.rs index 909c63b7..715c8a22 100644 --- a/libwebauthn/src/proto/ctap2/cbor/request.rs +++ b/libwebauthn/src/proto/ctap2/cbor/request.rs @@ -1,6 +1,6 @@ use std::io::Error as IOError; -use crate::proto::ctap2::cbor::CborSerialize; +use crate::proto::ctap2::cbor; use crate::proto::ctap2::model::Ctap2ClientPinRequest; use crate::proto::ctap2::model::Ctap2CommandCode; use crate::proto::ctap2::model::Ctap2GetAssertionRequest; @@ -40,7 +40,7 @@ impl From<&Ctap2MakeCredentialRequest> for CborRequest { fn from(request: &Ctap2MakeCredentialRequest) -> CborRequest { CborRequest { command: Ctap2CommandCode::AuthenticatorMakeCredential, - encoded_data: request.to_vec().unwrap(), + encoded_data: cbor::to_vec(&request).unwrap(), } } } @@ -49,7 +49,7 @@ impl From<&Ctap2GetAssertionRequest> for CborRequest { fn from(request: &Ctap2GetAssertionRequest) -> CborRequest { CborRequest { command: Ctap2CommandCode::AuthenticatorGetAssertion, - encoded_data: request.to_vec().unwrap(), + encoded_data: cbor::to_vec(&request).unwrap(), } } } @@ -58,7 +58,7 @@ impl From<&Ctap2ClientPinRequest> for CborRequest { fn from(request: &Ctap2ClientPinRequest) -> CborRequest { CborRequest { command: Ctap2CommandCode::AuthenticatorClientPin, - encoded_data: request.to_vec().unwrap(), + encoded_data: cbor::to_vec(&request).unwrap(), } } } @@ -67,7 +67,7 @@ impl From<&Ctap2AuthenticatorConfigRequest> for CborRequest { fn from(request: &Ctap2AuthenticatorConfigRequest) -> CborRequest { CborRequest { command: Ctap2CommandCode::AuthenticatorConfig, - encoded_data: request.to_vec().unwrap(), + encoded_data: cbor::to_vec(&request).unwrap(), } } } @@ -81,7 +81,7 @@ impl From<&Ctap2BioEnrollmentRequest> for CborRequest { }; CborRequest { command, - encoded_data: request.to_vec().unwrap(), + encoded_data: cbor::to_vec(&request).unwrap(), } } } @@ -95,7 +95,7 @@ impl From<&Ctap2CredentialManagementRequest> for CborRequest { }; CborRequest { command, - encoded_data: request.to_vec().unwrap(), + encoded_data: cbor::to_vec(&request).unwrap(), } } } diff --git a/libwebauthn/src/proto/ctap2/cbor/serde.rs b/libwebauthn/src/proto/ctap2/cbor/serde.rs index eac89a2b..b47d4412 100644 --- a/libwebauthn/src/proto/ctap2/cbor/serde.rs +++ b/libwebauthn/src/proto/ctap2/cbor/serde.rs @@ -16,44 +16,38 @@ impl PartialEq for CborError { } } -pub(crate) trait CborSerialize { - fn to_vec(&self) -> Result, CborError>; -} +pub(crate) type Value = serde_cbor::Value; -impl CborSerialize for T +pub(crate) fn to_vec(serializable: &T) -> Result, CborError> where T: Serialize, { - fn to_vec(&self) -> Result, CborError> { - serde_cbor::ser::to_vec(self).map_err(CborError::from) - } + serde_cbor::ser::to_vec(serializable).map_err(CborError::from) } -pub(crate) trait CborDeserialize: Sized + serde::de::DeserializeOwned { - fn from_reader(reader: R) -> Result; - fn from_slice(slice: &[u8]) -> Result; +pub(crate) fn from_reader(reader: R) -> Result +where + T: for<'de> serde::Deserialize<'de>, + R: std::io::Read, +{ + serde_cbor::de::from_reader(reader).map_err(CborError::from) } -impl CborDeserialize for T +pub(crate) fn from_slice(slice: &[u8]) -> Result where T: for<'de> serde::Deserialize<'de>, { - fn from_reader(reader: R) -> Result { - serde_cbor::de::from_reader(reader).map_err(CborError::from) - } - - fn from_slice(slice: &[u8]) -> Result { - serde_cbor::de::from_slice(slice).map_err(CborError::from) - } + serde_cbor::de::from_slice(slice).map_err(CborError::from) } #[cfg(test)] mod tests { use super::*; + use serde_cbor; use serde_indexed::{DeserializeIndexed, SerializeIndexed}; #[derive(Debug, PartialEq, SerializeIndexed, DeserializeIndexed)] - struct TestStruct { + struct IndexedStruct { #[serde(index = 0x01)] pub a: u8, #[serde(index = 0x02)] @@ -61,16 +55,36 @@ mod tests { } #[test] - fn test_deserialize_indexed_with_extra_field() { + fn test_deserialize_indexed() { + let expected = IndexedStruct { a: 10, b: 20 }; + let mut map = std::collections::BTreeMap::new(); + map.insert(1, 10u8); + map.insert(2, 20u8); + + let cbor = to_vec(&map).unwrap(); + let result = from_slice::(&cbor).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn test_serialize_indexed() { + let indexed_struct = IndexedStruct { a: 10, b: 20 }; + let serialized = to_vec(&indexed_struct).unwrap(); + let expected = serde_cbor::to_vec(&indexed_struct).unwrap(); + assert_eq!(serialized, expected); + } + + #[test] + fn test_deserialize_indexed_ignore_extra_field() { // Map: 1 => 10, 2 => 20, 3 => 99 (unexpected) - let value = TestStruct { a: 10, b: 20 }; + let expected = IndexedStruct { a: 10, b: 20 }; let mut map = std::collections::BTreeMap::new(); map.insert(1, 10u8); map.insert(2, 20u8); map.insert(3, 99u8); // unexpected field - let cbor = map.to_vec().unwrap(); - let result = TestStruct::from_slice(&cbor).unwrap(); - assert_eq!(result, value); + let cbor = to_vec(&map).unwrap(); + let result = from_slice::(&cbor).unwrap(); + assert_eq!(result, expected); } } diff --git a/libwebauthn/src/proto/ctap2/model.rs b/libwebauthn/src/proto/ctap2/model.rs index 65d3c8c0..45064298 100644 --- a/libwebauthn/src/proto/ctap2/model.rs +++ b/libwebauthn/src/proto/ctap2/model.rs @@ -220,7 +220,7 @@ pub enum Ctap2UserVerificationOperation { #[cfg(test)] mod tests { - use crate::proto::ctap2::cbor::CborSerialize; + use crate::proto::ctap2::cbor; use crate::proto::ctap2::Ctap2PublicKeyCredentialDescriptor; use super::{Ctap2COSEAlgorithmIdentifier, Ctap2CredentialType, Ctap2PublicKeyCredentialType}; @@ -234,7 +234,7 @@ mod tests { algorithm: Ctap2COSEAlgorithmIdentifier::ES256, public_key_type: Ctap2PublicKeyCredentialType::PublicKey, }; - let serialized = credential_type.to_vec().unwrap(); + let serialized = cbor::to_vec(&credential_type).unwrap(); // Known good, verified by hand with cbor.me playground let expected = hex::decode("a263616c672664747970656a7075626c69632d6b6579").unwrap(); assert_eq!(serialized, expected); @@ -248,7 +248,7 @@ mod tests { r#type: Ctap2PublicKeyCredentialType::PublicKey, transports: None, }; - let serialized = credential_descriptor.to_vec().unwrap(); + let serialized = cbor::to_vec(&credential_descriptor).unwrap(); // Known good, verified by hand with cbor.me playground let expected = hex::decode("a2626964414264747970656a7075626c69632d6b6579").unwrap(); assert_eq!(serialized, expected); diff --git a/libwebauthn/src/proto/ctap2/model/get_assertion.rs b/libwebauthn/src/proto/ctap2/model/get_assertion.rs index e1aab464..be56f586 100644 --- a/libwebauthn/src/proto/ctap2/model/get_assertion.rs +++ b/libwebauthn/src/proto/ctap2/model/get_assertion.rs @@ -11,7 +11,7 @@ use crate::{ webauthn::{Error, PlatformError}, }; -use crate::proto::ctap2::cbor::CborSerialize; +use crate::proto::ctap2::cbor::{self, Value}; use super::{ Ctap2AuthTokenPermissionRole, Ctap2COSEAlgorithmIdentifier, Ctap2GetInfoResponse, @@ -21,7 +21,6 @@ use super::{ use cosey::PublicKey; use serde::{Deserialize, Serialize}; use serde_bytes::ByteBuf; -use serde_cbor::Value; use serde_indexed::{DeserializeIndexed, SerializeIndexed}; use sha2::{Digest, Sha256}; use std::collections::{BTreeMap, HashMap}; @@ -255,7 +254,7 @@ impl Ctap2GetAssertionRequestExtensions { // saltEnc(0x02): Encryption of the one or two salts (called salt1 (32 bytes) and salt2 (32 bytes)) using the shared secret as follows: // One salt case: encrypt(shared secret, salt1) // Two salt case: encrypt(shared secret, salt1 || salt2) - let mut salts = input.salt1.to_vec()?; + let mut salts = cbor::to_vec(&input.salt1)?; if let Some(salt2) = input.salt2 { salts.extend(salt2); } diff --git a/libwebauthn/src/proto/ctap2/protocol.rs b/libwebauthn/src/proto/ctap2/protocol.rs index c6ef6766..434e0e05 100644 --- a/libwebauthn/src/proto/ctap2/protocol.rs +++ b/libwebauthn/src/proto/ctap2/protocol.rs @@ -3,8 +3,7 @@ use std::time::Duration; use async_trait::async_trait; use tracing::{debug, instrument, trace, warn}; -use crate::proto::ctap2::cbor::CborDeserialize; -use crate::proto::ctap2::cbor::CborRequest; +use crate::proto::ctap2::cbor::{self, CborRequest}; use crate::proto::ctap2::{Ctap2BioEnrollmentResponse, Ctap2CommandCode}; use crate::transport::error::{CtapError, Error, PlatformError}; use crate::transport::Channel; @@ -22,7 +21,7 @@ const TIMEOUT_GET_INFO: Duration = Duration::from_millis(250); macro_rules! parse_cbor { ($type:ty, $data:expr) => {{ - match CborDeserialize::<$type>::from_slice($data) { + match cbor::from_slice::<$type>($data) { Ok(f) => f, Err(e) => { tracing::error!( diff --git a/libwebauthn/src/transport/cable/crypto.rs b/libwebauthn/src/transport/cable/crypto.rs index 297a1f9c..54682ce5 100644 --- a/libwebauthn/src/transport/cable/crypto.rs +++ b/libwebauthn/src/transport/cable/crypto.rs @@ -12,7 +12,6 @@ pub enum KeyPurpose { PSK = 3, } - pub fn derive(secret: &[u8], salt: Option<&[u8]>, purpose: KeyPurpose) -> [u8; 64] { let mut purpose32 = [0u8; 4]; purpose32[0] = purpose as u8; @@ -41,7 +40,7 @@ pub fn trial_decrypt_advert(eid_key: &[u8], candidate_advert: &[u8]) -> Option<[ let expected_tag = hmac_sha256(&eid_key[32..], &candidate_advert[..16]); if expected_tag[..4] != candidate_advert[16..] { - warn!({ expected = ?expected_tag[..4], actual = ?candidate_advert[16..] }, + warn!({ expected = ?expected_tag[..4], actual = ?candidate_advert[16..] }, "candidate advert HMAC tag does not match"); return None; } @@ -64,10 +63,13 @@ pub fn trial_decrypt_advert(eid_key: &[u8], candidate_advert: &[u8]) -> Option<[ mod tests { use super::derive; use super::KeyPurpose; - + #[test] fn derive_eidkey_nosalt() { - let input: [u8; 16] = hex::decode("00112233445566778899aabbccddeeff").unwrap().try_into().unwrap(); + let input: [u8; 16] = hex::decode("00112233445566778899aabbccddeeff") + .unwrap() + .try_into() + .unwrap(); let output = derive(&input, None, KeyPurpose::EIDKey).to_vec(); let expected = hex::decode("efafab5b2c84a11c80e3ad0770353138b414a859ccd3afcc99e3d3250dba65084ede8e38e75432617c0ccae1ffe5d8143df0db0cd6d296f489419cd6411ee505").unwrap(); assert_eq!(output, expected); @@ -75,12 +77,13 @@ mod tests { #[test] fn derive_eidkey_salt() { - let input: [u8; 16] = hex::decode("00112233445566778899aabbccddeeff").unwrap().try_into().unwrap(); + let input: [u8; 16] = hex::decode("00112233445566778899aabbccddeeff") + .unwrap() + .try_into() + .unwrap(); let salt = hex::decode("ffeeddccbbaa998877665544332211").unwrap(); let output = derive(&input, Some(&salt), KeyPurpose::EIDKey).to_vec(); let expected = hex::decode("168cf3dd220a7907f8bac30f559be92a3b6d937fe5594beeaf1e50e35976b7d654dd550e22ae4c801b9d1cdbf0d2b1472daa1328661eb889acae3023b7ffa509").unwrap(); assert_eq!(output, expected); } - - -} \ No newline at end of file +} diff --git a/libwebauthn/src/transport/cable/qr_code_device.rs b/libwebauthn/src/transport/cable/qr_code_device.rs index 3812d798..7729d8fe 100644 --- a/libwebauthn/src/transport/cable/qr_code_device.rs +++ b/libwebauthn/src/transport/cable/qr_code_device.rs @@ -16,7 +16,7 @@ use tracing::{debug, error}; use super::known_devices::CableKnownDeviceInfoStore; use super::tunnel::{self, KNOWN_TUNNEL_DOMAINS}; use super::{channel::CableChannel, Cable}; -use crate::proto::ctap2::cbor::CborSerialize; +use crate::proto::ctap2::cbor; use crate::transport::cable::advertisement::await_advertisement; use crate::transport::cable::crypto::{derive, KeyPurpose}; use crate::transport::cable::digit_encode; @@ -73,7 +73,7 @@ pub struct CableQrCode { impl ToString for CableQrCode { fn to_string(&self) -> String { - let serialized = self.to_vec().unwrap(); + let serialized = cbor::to_vec(&self).unwrap(); format!("FIDO:/{}", digit_encode(&serialized)) } } diff --git a/libwebauthn/src/transport/cable/tunnel.rs b/libwebauthn/src/transport/cable/tunnel.rs index 50924a30..89d5fc9c 100644 --- a/libwebauthn/src/transport/cable/tunnel.rs +++ b/libwebauthn/src/transport/cable/tunnel.rs @@ -24,8 +24,7 @@ use tungstenite::client::IntoClientRequest; use super::channel::CableChannel; use super::known_devices::ClientPayload; use super::known_devices::{CableKnownDeviceInfo, CableKnownDeviceInfoStore}; -use crate::proto::ctap2::cbor::CborSerialize; -use crate::proto::ctap2::cbor::{CborRequest, CborResponse}; +use crate::proto::ctap2::cbor::{self, CborRequest, CborResponse}; use crate::proto::ctap2::{Ctap2CommandCode, Ctap2GetInfoResponse}; use crate::transport::cable::known_devices::CableKnownDeviceId; use crate::transport::error::Error; @@ -198,7 +197,7 @@ pub(crate) async fn connect<'d>( ); if let CableTunnelConnectionType::KnownDevice { client_payload, .. } = connection_type { - let client_payload = serde_cbor::to_vec(client_payload) + let client_payload = cbor::to_vec(client_payload) .or(Err(Error::Transport(TransportError::InvalidEndpoint)))?; request.headers_mut().insert( "X-caBLE-Client-Payload", @@ -587,7 +586,7 @@ async fn connection_recv_initial( } }; - Ok(initial_message.info.to_vec()?) + Ok(cbor::to_vec(&initial_message.info)?) } async fn connection_recv_update(message: &[u8]) -> Result, Error> { @@ -685,7 +684,7 @@ async fn connection_recv( } CableTunnelMessageType::Ctap => { // Handle the CTAP message - let cbor_response: CborResponse = (&cable_message.payload.to_vec()?) + let cbor_response: CborResponse = (&cable_message.payload.to_vec()) .try_into() .or(Err(TransportError::InvalidFraming))?; From ebd23c6f8e8d1f6f80bb5849d3db74e436dd6237 Mon Sep 17 00:00:00 2001 From: Alfie Fresta Date: Sat, 14 Jun 2025 19:45:22 +0200 Subject: [PATCH 5/6] Add more tests --- libwebauthn/src/proto/ctap2/cbor/serde.rs | 67 +++++++++++++++++++++-- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/libwebauthn/src/proto/ctap2/cbor/serde.rs b/libwebauthn/src/proto/ctap2/cbor/serde.rs index b47d4412..5c370db0 100644 --- a/libwebauthn/src/proto/ctap2/cbor/serde.rs +++ b/libwebauthn/src/proto/ctap2/cbor/serde.rs @@ -43,8 +43,10 @@ where #[cfg(test)] mod tests { use super::*; + use serde::{Deserialize, Serialize}; use serde_cbor; use serde_indexed::{DeserializeIndexed, SerializeIndexed}; + use std::collections::BTreeMap; #[derive(Debug, PartialEq, SerializeIndexed, DeserializeIndexed)] struct IndexedStruct { @@ -57,12 +59,12 @@ mod tests { #[test] fn test_deserialize_indexed() { let expected = IndexedStruct { a: 10, b: 20 }; - let mut map = std::collections::BTreeMap::new(); + let mut map = BTreeMap::new(); map.insert(1, 10u8); map.insert(2, 20u8); let cbor = to_vec(&map).unwrap(); - let result = from_slice::(&cbor).unwrap(); + let result: IndexedStruct = from_slice(&cbor).unwrap(); assert_eq!(result, expected); } @@ -78,13 +80,70 @@ mod tests { fn test_deserialize_indexed_ignore_extra_field() { // Map: 1 => 10, 2 => 20, 3 => 99 (unexpected) let expected = IndexedStruct { a: 10, b: 20 }; - let mut map = std::collections::BTreeMap::new(); + let mut map = BTreeMap::new(); map.insert(1, 10u8); map.insert(2, 20u8); map.insert(3, 99u8); // unexpected field let cbor = to_vec(&map).unwrap(); - let result = from_slice::(&cbor).unwrap(); + let result: IndexedStruct = from_slice(&cbor).unwrap(); assert_eq!(result, expected); } + + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct NamedStruct { + #[serde(rename = "key")] + pub value: u8, + } + + #[test] + fn test_deserialize_named() { + let expected = NamedStruct { value: 10 }; + let mut map = BTreeMap::new(); + map.insert("key", 10u8); + + let cbor = to_vec(&map).unwrap(); + let result: NamedStruct = from_slice(&cbor).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn test_serialize_named() { + let named_struct = NamedStruct { value: 10 }; + let serialized = to_vec(&named_struct).unwrap(); + let expected = serde_cbor::to_vec(&named_struct).unwrap(); + assert_eq!(serialized, expected); + } + + #[derive(Debug, PartialEq, SerializeIndexed, DeserializeIndexed)] + struct NestedStruct { + #[serde(index = 0x00)] + pub inner: NamedStruct, + } + + #[test] + fn test_deserialize_nested() { + let expected = NestedStruct { + inner: NamedStruct { value: 10 }, + }; + + let mut inner_map = BTreeMap::new(); + inner_map.insert("key", 10u8); + let mut outer_map = BTreeMap::new(); + outer_map.insert(0, inner_map); + + let cbor = to_vec(&outer_map).unwrap(); + let result: NestedStruct = from_slice(&cbor).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn test_serialize_nested() { + let nested_struct = NestedStruct { + inner: NamedStruct { value: 10 }, + }; + let serialized = to_vec(&nested_struct).unwrap(); + let expected = serde_cbor::to_vec(&nested_struct).unwrap(); + assert_eq!(serialized, expected); + } } From c4ba62cca3b467dc2d70c6c818b96dbc878c8b4c Mon Sep 17 00:00:00 2001 From: Alfie Fresta Date: Sat, 14 Jun 2025 19:47:50 +0200 Subject: [PATCH 6/6] Remove use of serde_smol from cbor_types --- libwebauthn/src/transport/cable/tunnel.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/libwebauthn/src/transport/cable/tunnel.rs b/libwebauthn/src/transport/cable/tunnel.rs index 89d5fc9c..fc3eb82b 100644 --- a/libwebauthn/src/transport/cable/tunnel.rs +++ b/libwebauthn/src/transport/cable/tunnel.rs @@ -1,14 +1,12 @@ use std::collections::BTreeMap; use std::sync::Arc; -use ctap_types::serde::cbor_deserialize; use futures::{SinkExt, StreamExt}; use hmac::{Hmac, Mac}; use p256::{ecdh, NonZeroScalar}; use p256::{PublicKey, SecretKey}; use serde::Deserialize; use serde_bytes::ByteBuf; -use serde_cbor::Value; use serde_indexed::DeserializeIndexed; use sha2::{Digest, Sha256}; use snow::{Builder, TransportState}; @@ -24,7 +22,7 @@ use tungstenite::client::IntoClientRequest; use super::channel::CableChannel; use super::known_devices::ClientPayload; use super::known_devices::{CableKnownDeviceInfo, CableKnownDeviceInfoStore}; -use crate::proto::ctap2::cbor::{self, CborRequest, CborResponse}; +use crate::proto::ctap2::cbor::{self, CborRequest, CborResponse, Value}; use crate::proto::ctap2::{Ctap2CommandCode, Ctap2GetInfoResponse}; use crate::transport::cable::known_devices::CableKnownDeviceId; use crate::transport::error::Error; @@ -570,7 +568,7 @@ async fn connection_recv_initial( let decrypted_frame = decrypt_frame(encrypted_frame, noise_state).await?; - let initial_message: CableInitialMessage = match cbor_deserialize(&decrypted_frame) { + let initial_message: CableInitialMessage = match cbor::from_slice(&decrypted_frame) { Ok(initial_message) => initial_message, Err(e) => { error!(?e, "Failed to decode initial message"); @@ -578,7 +576,7 @@ async fn connection_recv_initial( } }; - let _: Ctap2GetInfoResponse = match cbor_deserialize(&initial_message.info) { + let _: Ctap2GetInfoResponse = match cbor::from_slice(&initial_message.info) { Ok(get_info_response) => get_info_response, Err(e) => { error!(?e, "Failed to decode GetInfo response");