From cda15f89a67c2940d19ec2067db6eb9cd11cdb3e Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Fri, 7 Jul 2023 17:02:08 +0200 Subject: [PATCH] Truncate overlong name and displayName values Previously, we just returned an error if a name or displayName value for a PublicKeyCredentialEntity was longer than the supported 64 bytes. With this patch, we instead truncate the value at a UTF-8 character boundary. The logic for determining the truncation point is borrowed from a nightly function from the standard library. Fixes: https://github.com/solokeys/fido-authenticator/issues/30 --- CHANGELOG.md | 2 ++ src/webauthn.rs | 77 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de64e3a..1877372 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - Rename `url` to `icon` in `PublicKeyCredentialRpEntity` and ignore its content ([#9][]) +- Truncate overlong `name` and `displayName` values for `PublicKeyCredentialEntity` instances ([#30][]) [#9]: https://github.com/solokeys/ctap-types/issues/9 +[#30]: https://github.com/solokeys/fido-authenticator/issues/30 ## [0.1.2] - 2022-03-07 diff --git a/src/webauthn.rs b/src/webauthn.rs index 56fa272..65f09b2 100644 --- a/src/webauthn.rs +++ b/src/webauthn.rs @@ -7,7 +7,11 @@ use serde::{de::Deserializer, Deserialize, Serialize}; #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct PublicKeyCredentialRpEntity { pub id: String<256>, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde( + default, + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_from_str_and_truncate" + )] pub name: Option>, /// This field has been removed in Webauthn 2 but CTAP 2.2 requires implementors to accept it. /// @@ -47,9 +51,17 @@ pub struct PublicKeyCredentialUserEntity { )] #[serde(skip_serializing_if = "Option::is_none")] pub icon: Option>, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde( + default, + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_from_str_and_truncate" + )] pub name: Option>, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde( + default, + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_from_str_and_truncate" + )] pub display_name: Option>, } @@ -69,6 +81,46 @@ where } } +fn deserialize_from_str_and_truncate<'de, D, const L: usize>( + deserializer: D, +) -> Result>, D::Error> +where + D: serde::Deserializer<'de>, +{ + let s: Option<&str> = serde::Deserialize::deserialize(deserializer)?; + Ok(s.map(truncate)) +} + +fn truncate(s: &str) -> String { + let split = floor_char_boundary(s, L); + let mut truncated = String::new(); + // floor_char_boundary(s, L) <= L, so this cannot fail + truncated.push_str(&s[..split]).unwrap(); + truncated +} + +// Copy of the nightly str::floor_char_boundary function +fn floor_char_boundary(s: &str, index: usize) -> usize { + if index >= s.len() { + s.len() + } else { + let lower_bound = index.saturating_sub(3); + let new_index = s.as_bytes()[lower_bound..=index] + .iter() + .rposition(|b| is_utf8_char_boundary(*b)); + + // SAFETY: we know that the character boundary will be within four bytes + unsafe { lower_bound + new_index.unwrap_unchecked() } + } +} + +// Copy of the private u8::is_utf8_char_boundary function +#[inline] +const fn is_utf8_char_boundary(b: u8) -> bool { + // This is bit magic equivalent to: b < 128 || b >= 192 + (b as i8) >= -0x40 +} + impl PublicKeyCredentialUserEntity { pub fn from(id: Bytes<64>) -> Self { Self { @@ -107,3 +159,22 @@ pub struct PublicKeyCredentialDescriptor { // https://w3c.github.io/webauthn/#enumdef-authenticatortransport // transports: ... } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_truncate() { + // Example from ยง 6.4.1 String Truncation in the Webauthn spec + let v = vec![0x61, 0x67, 0xcc, 0x88]; + let s = std::str::from_utf8(&v).unwrap(); + + assert_eq!(truncate::<1>(s), "a"); + assert_eq!(truncate::<2>(s), "ag"); + assert_eq!(truncate::<3>(s), "ag"); + assert_eq!(truncate::<4>(s), s); + assert_eq!(truncate::<5>(s), s); + assert_eq!(truncate::<64>(s), s); + } +}