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
This commit is contained in:
Robin Krahl
2023-09-13 09:19:25 +02:00
committed by Nicolas Stalder
parent 290f90320e
commit cda15f89a6
2 changed files with 76 additions and 3 deletions
+2
View File
@@ -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
+74 -3
View File
@@ -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<String<64>>,
/// 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<String<128>>,
#[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<String<64>>,
#[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<String<64>>,
}
@@ -69,6 +81,46 @@ where
}
}
fn deserialize_from_str_and_truncate<'de, D, const L: usize>(
deserializer: D,
) -> Result<Option<String<L>>, D::Error>
where
D: serde::Deserializer<'de>,
{
let s: Option<&str> = serde::Deserialize::deserialize(deserializer)?;
Ok(s.map(truncate))
}
fn truncate<const L: usize>(s: &str) -> String<L> {
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);
}
}