webauthn_rp

WebAuthn Level 3 RP library.
git clone https://git.philomathiclife.com/repos/webauthn_rp
Log | Files | Refs | README

commit 9122de4849b2743665778898c615462791d53af4
parent af6d14ec68a4387bc09a25d138350b8323d4abb5
Author: Zack Newman <zack@philomathiclife.com>
Date:   Tue, 11 Feb 2025 20:32:21 -0700

add array-based UserHandle

Diffstat:
MCargo.toml | 12++++++------
Msrc/lib.rs | 11++++++-----
Msrc/request/register.rs | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/request/register/custom.rs | 6++++++
Msrc/request/register/ser.rs | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 147 insertions(+), 11 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -9,18 +9,18 @@ license = "MIT OR Apache-2.0" name = "webauthn_rp" readme = "README.md" repository = "https://git.philomathiclife.com/repos/webauthn_rp/" -rust-version = "1.83.0" -version = "0.2.3" +rust-version = "1.84.0" +version = "0.2.4" [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] -data-encoding = { version = "2.7.0", default-features = false } +data-encoding = { version = "2.8.0", default-features = false } ed25519-dalek = { version = "2.1.1", default-features = false, features = ["fast"] } p256 = { version = "0.13.2", default-features = false, features = ["ecdsa"] } -p384 = { version = "0.13.0", default-features = false, features = ["ecdsa"] } +p384 = { version = "0.13.1", default-features = false, features = ["ecdsa"] } precis-profiles = { version = "0.1.11", default-features = false } rand = { version = "0.9.0", default-features = false, features = ["thread_rng"] } rsa = { version = "0.9.7", default-features = false, features = ["sha2"] } @@ -29,10 +29,10 @@ serde_json = { version = "1.0.138", default-features = false, features = ["alloc url = { version = "2.5.4", default-features = false } [dev-dependencies] -data-encoding = { version = "2.6.0", default-features = false, features = ["alloc"] } +data-encoding = { version = "2.8.0", default-features = false, features = ["alloc"] } ed25519-dalek = { version = "2.1.1", default-features = false, features = ["alloc", "pkcs8"] } p256 = { version = "0.13.2", default-features = false, features = ["pem"] } -p384 = { version = "0.13.0", default-features = false, features = ["pkcs8"] } +p384 = { version = "0.13.1", default-features = false, features = ["pkcs8"] } serde_json = { version = "1.0.138", default-features = false, features = ["preserve_order"] } diff --git a/src/lib.rs b/src/lib.rs @@ -1163,12 +1163,13 @@ const fn base64url_nopad_len(n: usize) -> Option<usize> { // This won't overflow since this maxes at `isize::MAX` since // `n` <= ⌊3*isize::MAX/4⌋; thus `quot` <= ⌊isize::MAX/4⌋. // `n` can be partitioned into 4 possibilities: - // (1) n ≡ 0 (mod 4) = 4quot + 0 <= ⌊3*isize::MAX/4⌋ - // (2) n ≡ 1 (mod 4) = 4quot + 1 <= ⌊3*isize::MAX/4⌋ - // (3) n ≡ 2 (mod 4) = 4quot + 2 <= ⌊3*isize::MAX/4⌋ - // (4) n ≡ 3 (mod 4) = 4quot + 3 <= ⌊3*isize::MAX/4⌋ + // (1) n ≡ 0 (mod 4) = 4quot + 0 + // (2) n ≡ 1 (mod 4) = 4quot + 1 + // (3) n ≡ 2 (mod 4) = 4quot + 2 + // (4) n ≡ 3 (mod 4) = 4quot + 3 // For (1), rem is 0; thus 4quot + 0 = `n` which is fine. - // (2) is not possible per the proof in `webauthn_rp::base64url_nopad_decode_len`. + // For (2), rem is 1; thus 4quot + 2 = n - 1 + 2 = n + 1 <= ⌊3*isize::MAX/4⌋ + 1 <= isize::MAX for + // isize::MAX > 0. Clearly `isize::MAX > 0`; otherwise we couldn't allocate anything. // For (3), rem is 2; thus 4quot + 3 = n - 2 + 3 = n + 1 <= ⌊3*isize::MAX/4⌋ + 1 <= isize::MAX for // isize::MAX > 0. Clearly `isize::MAX > 0`; otherwise we couldn't allocate anything. // For (4), rem is 3; thus 4quot + 4 = n - 3 + 4 = n + 1 <= ⌊3*isize::MAX/4⌋ + 1 <= isize::MAX for diff --git a/src/request/register.rs b/src/request/register.rs @@ -748,6 +748,58 @@ impl UserHandle<Vec<u8>> { Self::rand(64).unwrap_or_else(|_e| unreachable!("there is a bug in UserHandle::rand")) } } +/// Implements [`Default`] for [`UserHandle`] of array of length of the passed `usize` literal. +/// +/// Only [`USER_HANDLE_MIN_LEN`]–[`USER_HANDLE_MAX_LEN`] inclusively are allowed to be passed. +macro_rules! user { + ( $( $x:literal),* ) => { + $( +impl Default for UserHandle<[u8; $x]> { + #[inline] + fn default() -> Self { + let mut data = [0; $x]; + rand::fill(data.as_mut_slice()); + Self(data) + } +} + )* + }; +} +// MUST only pass [`USER_HANDLE_MIN_LEN`]–[`USER_HANDLE_MAX_LEN`] inclusively. +user!( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, + 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, + 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64 +); +impl<const LEN: usize> UserHandle<[u8; LEN]> +where + Self: Default, +{ + /// Returns a new `UserHandle` based on `LEN` randomly-generated [`u8`]s. + /// + /// # Examples + /// + /// ``` + /// # use webauthn_rp::request::register::{UserHandle, USER_HANDLE_MIN_LEN, USER_HANDLE_MAX_LEN}; + /// assert_eq!( + /// UserHandle::<[u8; USER_HANDLE_MIN_LEN]>::new_rand() + /// .as_ref() + /// .len(), + /// 1 + /// ); + /// // The probability of an all-zero `UserHandle` being generated (assuming a good entropy + /// // source) is 2^-512 ≈ 7.5 x 10^-155. + /// assert_ne!( + /// UserHandle::<[u8; USER_HANDLE_MAX_LEN]>::new_rand().as_ref(), + /// [0; USER_HANDLE_MAX_LEN] + /// ); + /// ``` + #[inline] + #[must_use] + pub fn new_rand() -> Self { + Self::default() + } +} impl Default for UserHandle<Vec<u8>> { #[inline] fn default() -> Self { @@ -766,6 +818,24 @@ impl<T: Borrow<[u8]>> Borrow<[u8]> for UserHandle<T> { self.0.borrow() } } +impl<'a: 'b, 'b, const LEN: usize> From<&'a UserHandle<[u8; LEN]>> for UserHandle<&'b [u8; LEN]> { + #[inline] + fn from(value: &'a UserHandle<[u8; LEN]>) -> Self { + Self(&value.0) + } +} +impl<'a: 'b, 'b, const LEN: usize> From<UserHandle<&'a [u8; LEN]>> for UserHandle<&'b [u8]> { + #[inline] + fn from(value: UserHandle<&'a [u8; LEN]>) -> Self { + Self(value.0.as_slice()) + } +} +impl<'a: 'b, 'b, const LEN: usize> From<&'a UserHandle<[u8; LEN]>> for UserHandle<&'b [u8]> { + #[inline] + fn from(value: &'a UserHandle<[u8; LEN]>) -> Self { + Self(value.0.as_slice()) + } +} impl<'a: 'b, 'b> From<&'a UserHandle<Vec<u8>>> for UserHandle<&'b Vec<u8>> { #[inline] fn from(value: &'a UserHandle<Vec<u8>>) -> Self { diff --git a/src/request/register/custom.rs b/src/request/register/custom.rs @@ -1,4 +1,10 @@ use super::{UserHandle, UserHandleErr}; +impl<const LEN: usize> From<[u8; LEN]> for UserHandle<[u8; LEN]> { + #[inline] + fn from(value: [u8; LEN]) -> Self { + Self(value) + } +} impl<'a: 'b, 'b> TryFrom<&'a [u8]> for UserHandle<&'b [u8]> { type Error = UserHandleErr; #[inline] diff --git a/src/request/register/ser.rs b/src/request/register/ser.rs @@ -853,6 +853,65 @@ impl<'de> Deserialize<'de> for UserHandle<Vec<u8>> { deserializer.deserialize_str(UserHandleVisitor) } } +impl<'de, const LEN: usize> Deserialize<'de> for UserHandle<[u8; LEN]> +where + Self: Default, +{ + /// Deserializes [`prim@str`] based on + /// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponsejson-userhandle). + /// + /// # Examples + /// + /// ``` + /// # use webauthn_rp::request::register::{UserHandle, USER_HANDLE_MIN_LEN}; + /// # #[cfg(feature = "custom")] + /// assert_eq!( + /// serde_json::from_str::<UserHandle<[u8; USER_HANDLE_MIN_LEN]>>(r#""AA""#)?, + /// UserHandle::from([0; USER_HANDLE_MIN_LEN]) + /// ); + /// # Ok::<_, serde_json::Error>(()) + ///``` + #[inline] + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + /// `Visitor` for `UserHandle`. + struct UserHandleVisitor<const L: usize>; + impl<const L: usize> Visitor<'_> for UserHandleVisitor::<L> + where + UserHandle<[u8; L]>: Default, + { + type Value = UserHandle<[u8; L]>; + fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + formatter.write_str("UserHandle") + } + #[expect(clippy::panic_in_result_fn, clippy::unreachable, reason = "we want to crash when there is a bug")] + fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> + where + E: Error, + { + // Any value between `USER_HANDLE_MIN_LEN` and `USER_HANDLE_MAX_LEN` can be base64url encoded + // without fear since that range is just 1 to 64, and 4/3 of 64 is less than `usize::MAX`. + if crate::base64url_nopad_len(L).unwrap_or_else(|| unreachable!("there is a bug in webauthn_rp::base64url_nopad_len")) == v.len() { + let mut data = [0; L]; + BASE64URL_NOPAD + .decode_mut(v.as_bytes(), data.as_mut_slice()) + .map_err(|e| E::custom(e.error)) + .map(|len| { + assert_eq!(len, L, "there is a bug in data_encoding::BASE64URL_NOPAD::decode_mut"); + UserHandle(data) + }) + } else { + Err(E::invalid_value( + Unexpected::Str(v), &format!("{L} bytes encoded in base64url without padding").as_str() + )) + } + } + } + deserializer.deserialize_str(UserHandleVisitor) + } +} impl<'de: 'name + 'display_name, 'name, 'display_name> Deserialize<'de> for PublicKeyCredentialUserEntity<'name, 'display_name, Vec<u8>> {