webauthn_rp

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

commit 13aba926a476a81e2c94f0971bfecba3457ca51a
parent 5239f6a43dda53268908438de4449f56910c8405
Author: Zack Newman <zack@philomathiclife.com>
Date:   Wed, 29 Jan 2025 23:58:22 -0700

prevent panics from encoding::decode_mut

Diffstat:
MCargo.toml | 8++++----
Msrc/lib.rs | 73++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/request.rs | 3+--
Msrc/request/auth/ser.rs | 9+++++----
Msrc/request/register.rs | 5+----
Msrc/request/register/ser.rs | 7++++---
Msrc/request/ser.rs | 6+++---
Msrc/response.rs | 6+++---
Msrc/response/auth/ser.rs | 64+++++++++++++++++++++++++++++++++++++---------------------------
Msrc/response/auth/ser_relaxed.rs | 24+++++++++++-------------
Msrc/response/register/ser.rs | 72++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/response/register/ser_relaxed.rs | 59++++++++++++++++++++++++++++++-----------------------------
Msrc/response/ser.rs | 12+++++-------
13 files changed, 190 insertions(+), 158 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -10,7 +10,7 @@ name = "webauthn_rp" readme = "README.md" repository = "https://git.philomathiclife.com/repos/webauthn_rp/" rust-version = "1.83.0" -version = "0.2.2" +version = "0.2.3" [package.metadata.docs.rs] all-features = true @@ -22,10 +22,10 @@ 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"] } precis-profiles = { version = "0.1.11", default-features = false } -rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] } +rand = { version = "0.9.0", default-features = false, features = ["thread_rng"] } rsa = { version = "0.9.7", default-features = false, features = ["sha2"] } serde = { version = "1.0.217", default-features = false, features = ["alloc"], optional = true } -serde_json = { version = "1.0.137", default-features = false, features = ["alloc"], optional = true } +serde_json = { version = "1.0.138", default-features = false, features = ["alloc"], optional = true } url = { version = "2.5.4", default-features = false } [dev-dependencies] @@ -33,7 +33,7 @@ data-encoding = { version = "2.6.0", default-features = false, features = ["allo 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"] } -serde_json = { version = "1.0.137", default-features = false, features = ["preserve_order"] } +serde_json = { version = "1.0.138", default-features = false, features = ["preserve_order"] } ### FEATURES ################################################################# diff --git a/src/lib.rs b/src/lib.rs @@ -200,6 +200,7 @@ clippy::implicit_return, clippy::min_ident_chars, clippy::missing_trait_methods, + clippy::multiple_crate_versions, clippy::pub_with_shorthand, clippy::pub_use, clippy::ref_patterns, @@ -279,7 +280,6 @@ use core::{ error::Error, fmt::{self, Display, Formatter}, }; -use data_encoding::{Encoding, BASE64URL_NOPAD}; #[cfg(all(doc, feature = "serde_relaxed"))] use response::register::ser_relaxed::RegistrationRelaxed; #[cfg(all(doc, feature = "serde"))] @@ -1134,14 +1134,12 @@ impl Error for AggErr {} /// /// # Panics /// -/// `panic`s iff `n >= 0x4000`. +/// `panic`s iff `4 * n` overflows. +#[expect( + clippy::expect_used, + reason = "a precondition for calling this function ensures expect won't panic" +)] const fn base64url_nopad_len(n: usize) -> usize { - // `usize::MAX >= u16::MAX`; thus we conservatively cap `n` to the largest value that would not overflow - // from multiplying by 4 on all platforms. - assert!( - n < 0x4000, - "webauthn_rp::base64url_nopad_len must be passed an integer smaller than 0x4000" - ); // 256^n is the number of distinct values of the input. Let the base64 encoding in a URL safe // way without padding of the input be O. There are 64 possible values each byte in O can be; thus we must find // the minimum nonnegative integer m such that: @@ -1151,19 +1149,19 @@ const fn base64url_nopad_len(n: usize) -> usize { // <==> // m >= 8n/6 = 4n/3 // Clearly that corresponds to m = ⌈4n/3⌉; thus: - (n << 2).div_ceil(3) + + n.checked_mul(4).expect("webauthn_rp::base64url_nopad_len should not have been passed a usize that when multiplied by 4 causes overflow").div_ceil(3) } /// Calculates the number of bytes a base64url-encoded input of length `n` bytes will be decoded into. /// -/// `Some` is returned iff the value could be calculated without overflow; otherwise `None` is returned. -/// -/// Note this does not verify if `n` is a possible value. +/// `Some` is returned iff `n` is a valid input length. +#[cfg(feature = "serde")] #[expect( - clippy::unseparated_literal_suffix, - reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs" + clippy::arithmetic_side_effects, + clippy::integer_division_remainder_used, + reason = "proof and comment justifies its correctness" )] -#[cfg(feature = "serde")] -fn base64url_nopad_decode_len(n: usize) -> Option<usize> { +const fn base64url_nopad_decode_len(n: usize) -> Option<usize> { // 64^n is the number of distinct values of the input. Let the decoded output be O. // There are 256 possible values each byte in O can be; thus we must find // the maximum nonnegative integer m such that: @@ -1172,9 +1170,42 @@ fn base64url_nopad_decode_len(n: usize) -> Option<usize> { // lg(2^(8m)) = 8m <= lg(2^(6n)) = 6n lg is defined on all positive reals which 2^(8m) and 2^(6n) are // <==> // m <= 6n/8 = 3n/4 - // Clearly that corresponds to m = ⌊3n/4⌋; thus: - n.checked_mul(3).map(|len| len >> 2u8) + // Clearly that corresponds to m = ⌊3n/4⌋. + // + // There are three partitions for m: + // (1) m ≡ 0 (mod 3) = 3i + // (2) m ≡ 1 (mod 3) = 3i + 1 + // (3) m ≡ 2 (mod 3) = 3i + 2 + // + // From `crate::base64url_nopad_len`, we know that the encoded length, n, of an input of length m is m = ⌈4n/3⌉. + // The encoded length of (1) is thus n = ⌈4(3i)/3⌉ = ⌈4i⌉ = 4i ≡ 0 (mod 4). + // The encoded length of (2) is thus n = ⌈4(3i + 1)/3⌉ = ⌈4i + 4/3⌉ = 4i + 2 ≡ 2 (mod 4). + // The encoded length of (3) is thus n = ⌈4(3i + 2)/3⌉ = ⌈4i + 8/3⌉ = 4i + 3 ≡ 3 (mod 4). + // + // Thus if n ≡ 1 (mod 4), it is never a valid length. + // + // Let n be the length of a possible encoded output of an input of length m. + // We know from above that n ≢ 1 (mod 4), this leaves three possibilities: + // (1) n ≡ 0 (mod 4) = 4i + // (2) n ≡ 2 (mod 4) = 4i + 2 + // (3) n ≡ 3 (mod 4) = 4i + 3 + // + // For (1) an input of length 3i is the inverse since ⌈4(3i)/3⌉ = 4i. + // For (2) an input of length 3i + 1 is the inverse since ⌈4(3i + 1)/3⌉ = ⌈4i + 4/3⌉ = 4i + 2. + // For (3) an input of length 3i + 2 is the inverse since ⌈4(3i + 2)/3⌉ = ⌈4i + 8/3⌉ = 4i + 3. + // + // Consequently n is a valid length of an encoded output iff n ≢ 1 (mod 4). + + if n % 4 == 1 { + None + } else { + let (quot, rem) = (n >> 2u8, n % 4); + // 4quot + rem = n + // rem <= 3 + // 3rem <= 9 + // 3rem/4 <= 2 + // 3quot + 3rem/4 <= 4quot + rem + // Thus no operation causes overflow or underflow. + Some((3 * quot) + ((3 * rem) >> 2u8)) + } } -// We cache a `static` instance for performance reasons. -/// Encoder/decoder for base64url. -const BASE64URL_NOPAD_ENC: &Encoding = &BASE64URL_NOPAD; diff --git a/src/request.rs b/src/request.rs @@ -28,7 +28,6 @@ use core::{ num::NonZeroU32, str::FromStr, }; -use rand::Rng as _; use rsa::sha2::{Digest as _, Sha256}; #[cfg(feature = "serializable_server_state")] use std::time::SystemTime; @@ -210,7 +209,7 @@ impl Challenge { #[inline] #[must_use] pub fn new() -> Self { - Self(rand::thread_rng().r#gen()) + Self(rand::random()) } /// Returns the contained `u128` consuming `self`. #[inline] diff --git a/src/request/auth/ser.rs b/src/request/auth/ser.rs @@ -1,7 +1,8 @@ use super::{ - super::super::BASE64URL_NOPAD_ENC, AllowedCredential, AllowedCredentials, - AuthenticationClientState, Extension, PrfInput, PrfInputOwned, + AllowedCredential, AllowedCredentials, AuthenticationClientState, Extension, PrfInput, + PrfInputOwned, }; +use data_encoding::BASE64URL_NOPAD; use serde::ser::{Serialize, SerializeMap as _, SerializeStruct as _, Serializer}; impl Serialize for PrfInput<'_> { /// Serializes `self` to conform with @@ -34,14 +35,14 @@ impl Serialize for PrfInput<'_> { // The max value is 1 + 1 = 2, so overflow is not an issue. .serialize_struct("PrfInput", 1 + usize::from(self.second.is_some())) .and_then(|mut ser| { - ser.serialize_field("first", BASE64URL_NOPAD_ENC.encode(self.first).as_str()) + ser.serialize_field("first", BASE64URL_NOPAD.encode(self.first).as_str()) .and_then(|()| { self.second .as_ref() .map_or(Ok(()), |second| { ser.serialize_field( "second", - BASE64URL_NOPAD_ENC.encode(second).as_str(), + BASE64URL_NOPAD.encode(second).as_str(), ) }) .and_then(|()| ser.end()) diff --git a/src/request/register.rs b/src/request/register.rs @@ -32,7 +32,6 @@ use core::{ time::Duration, }; use precis_profiles::{precis_core::profile::Profile as _, UsernameCasePreserved}; -use rand::RngCore as _; #[cfg(any(doc, not(feature = "serializable_server_state")))] use std::time::Instant; #[cfg(any(doc, feature = "serializable_server_state"))] @@ -725,9 +724,7 @@ impl UserHandle<Vec<u8>> { pub fn rand(len: usize) -> Result<Self, UserHandleErr> { if (USER_HANDLE_MIN_LEN..=USER_HANDLE_MAX_LEN).contains(&len) { let mut data = vec![0; len]; - // [`ThreadRng` is infallible](https://docs.rs/rand_core/latest/src/rand_core/block.rs.html#237); - // thus there is no point in calling `try_fill_bytes`. - rand::thread_rng().fill_bytes(data.as_mut_slice()); + rand::fill(data.as_mut_slice()); Ok(Self(data)) } else { Err(UserHandleErr) diff --git a/src/request/register/ser.rs b/src/request/register/ser.rs @@ -5,7 +5,7 @@ )] extern crate alloc; use super::{ - super::super::BASE64URL_NOPAD_ENC, AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria, + AuthenticatorAttachmentReq, AuthenticatorSelectionCriteria, CoseAlgorithmIdentifier, CoseAlgorithmIdentifiers, CredProtect, CrossPlatformHint, Extension, ExtensionInfo, Hint, Nickname, PlatformHint, PublicKeyCredentialUserEntity, RegistrationClientState, ResidentKeyRequirement, RpId, UserHandle, Username, @@ -19,6 +19,7 @@ use core::{ marker::PhantomData, str, }; +use data_encoding::BASE64URL_NOPAD; use serde::{ de::{Deserialize, Deserializer, Error, MapAccess, Unexpected, Visitor}, ser::{Serialize, SerializeSeq as _, SerializeStruct, Serializer}, @@ -202,7 +203,7 @@ impl<T: AsRef<[u8]>> Serialize for UserHandle<T> { where S: Serializer, { - serializer.serialize_str(BASE64URL_NOPAD_ENC.encode(self.0.as_ref()).as_str()) + serializer.serialize_str(BASE64URL_NOPAD.encode(self.0.as_ref()).as_str()) } } /// `"displayName"`. @@ -840,7 +841,7 @@ impl<'de> Deserialize<'de> for UserHandle<Vec<u8>> { ..=crate::base64url_nopad_len(USER_HANDLE_MAX_LEN)) .contains(&v.len()) { - BASE64URL_NOPAD_ENC + BASE64URL_NOPAD .decode(v.as_bytes()) .map_err(E::custom) .map(UserHandle) diff --git a/src/request/ser.rs b/src/request/ser.rs @@ -1,8 +1,8 @@ use super::{ - super::BASE64URL_NOPAD_ENC, Challenge, CredentialId, Hint, PublicKeyCredentialDescriptor, RpId, - UserVerificationRequirement, + Challenge, CredentialId, Hint, PublicKeyCredentialDescriptor, RpId, UserVerificationRequirement, }; use core::str; +use data_encoding::BASE64URL_NOPAD; use serde::ser::{Serialize, SerializeSeq as _, SerializeStruct as _, Serializer}; impl Serialize for Challenge { /// Serializes `self` to conform with @@ -30,7 +30,7 @@ impl Serialize for Challenge { S: Serializer, { let mut data = [0; Self::BASE64_LEN]; - BASE64URL_NOPAD_ENC.encode_mut(self.0.to_le_bytes().as_slice(), data.as_mut_slice()); + BASE64URL_NOPAD.encode_mut(self.0.to_le_bytes().as_slice(), data.as_mut_slice()); serializer.serialize_str( str::from_utf8(data.as_slice()) // There is a bug, so crash and burn. diff --git a/src/response.rs b/src/response.rs @@ -9,7 +9,6 @@ use crate::{ error::{CollectedClientDataErr, CredentialIdErr}, register::error::{AttestationObjectErr, AttestedCredentialDataErr, AuthenticatorDataErr as RegAuthDataErr, AuthenticatorExtensionOutputErr as RegAuthExtErr, PubKeyErr, RegCeremonyErr}, }, - BASE64URL_NOPAD_ENC, }; use alloc::borrow::Cow; use core::{ @@ -20,6 +19,7 @@ use core::{ hash::{Hash, Hasher}, str, }; +use data_encoding::BASE64URL_NOPAD; use rsa::sha2::{digest::OutputSizeUser as _, Sha256}; #[cfg(feature = "serde_relaxed")] use ser_relaxed::SerdeJsonErr; @@ -1059,7 +1059,7 @@ impl<const R: bool> ClientDataJsonParser for LimitedVerificationParser<R> { if chall_key == AFTER_TYPE { chall_key_rem.split_at_checked(Challenge::BASE64_LEN).ok_or(CollectedClientDataErr::Len).and_then(|(base64_chall, base64_chall_rem)| { let mut chall = [0; 16]; - BASE64URL_NOPAD_ENC.decode_mut(base64_chall, chall.as_mut_slice()).map_err(|_e| CollectedClientDataErr::Challenge).and_then(|chall_len| { + BASE64URL_NOPAD.decode_mut(base64_chall, chall.as_mut_slice()).map_err(|_e| CollectedClientDataErr::Challenge).and_then(|chall_len| { assert_eq!(chall_len, 16, "there is a bug in BASE64URL_NOPAD::decode_mut"); base64_chall_rem.split_at_checked(AFTER_CHALLENGE.len()).ok_or(CollectedClientDataErr::Len).and_then(|(origin_key, origin_key_rem)| { if origin_key == AFTER_CHALLENGE { @@ -1149,7 +1149,7 @@ impl<const R: bool> ClientDataJsonParser for LimitedVerificationParser<R> { // This maxes at 39 + 22 = 61; thus overflow is not an issue. json.get(idx..idx + Challenge::BASE64_LEN).ok_or(CollectedClientDataErr::Len).and_then(|chall_slice| { let mut chall = [0; 16]; - BASE64URL_NOPAD_ENC.decode_mut(chall_slice, chall.as_mut_slice()).map_err(|_e| CollectedClientDataErr::Challenge).map(|len| { + BASE64URL_NOPAD.decode_mut(chall_slice, chall.as_mut_slice()).map_err(|_e| CollectedClientDataErr::Challenge).map(|len| { assert_eq!(len, 16, "there is a bug in BASE64URL_NOPAD::decode_mut"); SentChallenge(u128::from_le_bytes(chall)) }) diff --git a/src/response/auth/ser.rs b/src/response/auth/ser.rs @@ -9,7 +9,6 @@ use super::{ AuthenticationExtensionsPrfOutputsHelper, AuthenticationExtensionsPrfValues, ClientExtensions, }, - BASE64URL_NOPAD_ENC, }, error::UnknownCredentialOptions, Authentication, AuthenticatorAssertion, @@ -21,7 +20,6 @@ use core::{ marker::PhantomData, str, }; -#[cfg(doc)] use data_encoding::BASE64URL_NOPAD; use rsa::sha2::{digest::OutputSizeUser as _, Sha256}; use serde::{ @@ -109,25 +107,39 @@ impl<'d, const R: bool> Visitor<'d> for AuthenticatorAssertionVisitor<R> { } #[expect( clippy::panic_in_result_fn, - clippy::unreachable, reason = "we want to crash when there is a bug" )] + #[expect( + clippy::arithmetic_side_effects, + reason = "comment justifies its correctness" + )] fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> where E: Error, { - crate::base64url_nopad_decode_len(v.len()).ok_or_else(|| E::invalid_value(Unexpected::Str(v), &"a shorter base64url-encoded value")).and_then(|len| { - // The decoded length is 3/4 of the encoded length, so overflow could only occur - // if usize::MAX / 4 < 32 => usize::MAX < 128 < u8::MAX; thus overflow is not possible. - // We add 32 since the SHA-256 hash of `clientDataJSON` will be added to the - // raw authenticator data by `AuthenticatorDataAssertion::new`. - let mut auth_data = vec![0; len.checked_add(Sha256::output_size()).unwrap_or_else(|| unreachable!("there is a bug webauthn_rp::base64url_nopad_decode_len"))]; - auth_data.truncate(len); - BASE64URL_NOPAD_ENC.decode_mut(v.as_bytes(), auth_data.as_mut_slice()).map_err(|e| E::custom(e.error)).map(|dec_len| { - assert_eq!(len, dec_len, "there is a bug in BASE64URL_NOPAD::decode_mut"); - AuthData(auth_data) + crate::base64url_nopad_decode_len(v.len()) + .ok_or_else(|| { + E::invalid_value(Unexpected::Str(v), &"base64url-encoded value") + }) + .and_then(|len| { + // The decoded length is 3/4 of the encoded length, so overflow could only occur + // if usize::MAX / 4 < 32 => usize::MAX < 128 < u8::MAX; thus overflow is not + // possible. + // We add 32 since the SHA-256 hash of `clientDataJSON` will be added to the + // raw authenticator data by `AuthenticatorDataAssertion::new`. + let mut auth_data = vec![0; len + Sha256::output_size()]; + auth_data.truncate(len); + BASE64URL_NOPAD + .decode_mut(v.as_bytes(), auth_data.as_mut_slice()) + .map_err(|e| E::custom(e.error)) + .map(|dec_len| { + assert_eq!( + len, dec_len, + "there is a bug in BASE64URL_NOPAD::decode_mut" + ); + AuthData(auth_data) + }) }) - }) } } deserializer.deserialize_str(AuthDataVisitor) @@ -425,10 +437,8 @@ impl Serialize for UnknownCredentialOptions<'_, '_> { } #[cfg(test)] mod tests { - use super::{ - super::{Authentication, AuthenticatorAttachment}, - BASE64URL_NOPAD_ENC, - }; + use super::super::{Authentication, AuthenticatorAttachment}; + use data_encoding::BASE64URL_NOPAD; use rsa::sha2::{Digest as _, Sha256}; use serde::de::{Error as _, Unexpected}; use serde_json::Error; @@ -477,10 +487,10 @@ mod tests { 0, 0, ]; - let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes()); - let b64_adata = BASE64URL_NOPAD_ENC.encode(auth_data.as_slice()); - let b64_sig = BASE64URL_NOPAD_ENC.encode([].as_slice()); - let b64_user = BASE64URL_NOPAD_ENC.encode(b"\x00".as_slice()); + let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes()); + let b64_adata = BASE64URL_NOPAD.encode(auth_data.as_slice()); + let b64_sig = BASE64URL_NOPAD.encode([].as_slice()); + let b64_user = BASE64URL_NOPAD.encode(b"\x00".as_slice()); // Base case is valid. assert!(serde_json::from_str::<Authentication>( serde_json::json!({ @@ -511,7 +521,7 @@ mod tests { // `id` and `rawId` mismatch. let mut err = Error::invalid_value( Unexpected::Bytes( - BASE64URL_NOPAD_ENC + BASE64URL_NOPAD .decode("ABABABABABABABABABABAA".as_bytes()) .unwrap() .as_slice(), @@ -1265,10 +1275,10 @@ mod tests { 0, 0, ]; - let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes()); - let b64_adata = BASE64URL_NOPAD_ENC.encode(auth_data.as_slice()); - let b64_sig = BASE64URL_NOPAD_ENC.encode([].as_slice()); - let b64_user = BASE64URL_NOPAD_ENC.encode(b"\x00".as_slice()); + let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes()); + let b64_adata = BASE64URL_NOPAD.encode(auth_data.as_slice()); + let b64_sig = BASE64URL_NOPAD.encode([].as_slice()); + let b64_user = BASE64URL_NOPAD.encode(b"\x00".as_slice()); // Base case is valid. assert!(serde_json::from_str::<Authentication>( serde_json::json!({ diff --git a/src/response/auth/ser_relaxed.rs b/src/response/auth/ser_relaxed.rs @@ -119,10 +119,8 @@ impl<'de> Deserialize<'de> for AuthenticationRelaxed { } #[cfg(test)] mod tests { - use super::{ - super::{super::BASE64URL_NOPAD_ENC, AuthenticatorAttachment}, - AuthenticationRelaxed, - }; + use super::{super::AuthenticatorAttachment, AuthenticationRelaxed}; + use data_encoding::BASE64URL_NOPAD; use rsa::sha2::{Digest as _, Sha256}; use serde::de::{Error as _, Unexpected}; use serde_json::Error; @@ -171,10 +169,10 @@ mod tests { 0, 0, ]; - let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes()); - let b64_adata = BASE64URL_NOPAD_ENC.encode(auth_data.as_slice()); - let b64_sig = BASE64URL_NOPAD_ENC.encode([].as_slice()); - let b64_user = BASE64URL_NOPAD_ENC.encode(b"\x00".as_slice()); + let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes()); + let b64_adata = BASE64URL_NOPAD.encode(auth_data.as_slice()); + let b64_sig = BASE64URL_NOPAD.encode([].as_slice()); + let b64_user = BASE64URL_NOPAD.encode(b"\x00".as_slice()); // Base case is valid. assert!(serde_json::from_str::<AuthenticationRelaxed>( serde_json::json!({ @@ -205,7 +203,7 @@ mod tests { // `id` and `rawId` mismatch. let mut err = Error::invalid_value( Unexpected::Bytes( - BASE64URL_NOPAD_ENC + BASE64URL_NOPAD .decode("ABABABABABABABABABABAA".as_bytes()) .unwrap() .as_slice(), @@ -880,10 +878,10 @@ mod tests { 0, 0, ]; - let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes()); - let b64_adata = BASE64URL_NOPAD_ENC.encode(auth_data.as_slice()); - let b64_sig = BASE64URL_NOPAD_ENC.encode([].as_slice()); - let b64_user = BASE64URL_NOPAD_ENC.encode(b"\x00".as_slice()); + let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes()); + let b64_adata = BASE64URL_NOPAD.encode(auth_data.as_slice()); + let b64_sig = BASE64URL_NOPAD.encode([].as_slice()); + let b64_user = BASE64URL_NOPAD.encode(b"\x00".as_slice()); // Base case is valid. assert!(serde_json::from_str::<AuthenticationRelaxed>( serde_json::json!({ diff --git a/src/response/register/ser.rs b/src/response/register/ser.rs @@ -9,7 +9,6 @@ use super::{ AuthenticationExtensionsPrfOutputsHelper, AuthenticationExtensionsPrfValues, Base64DecodedVal, ClientExtensions, PublicKeyCredential, }, - BASE64URL_NOPAD_ENC, }, AttestationObject, AttestedCredentialData, AuthTransports, AuthenticationExtensionsPrfOutputs, AuthenticatorAttestation, ClientExtensionsOutputs, CredentialPropertiesOutput, FromCbor as _, @@ -22,6 +21,7 @@ use core::{ marker::PhantomData, str, }; +use data_encoding::BASE64URL_NOPAD; use rsa::sha2::{digest::OutputSizeUser as _, Sha256}; use serde::de::{Deserialize, Deserializer, Error, IgnoredAny, MapAccess, Unexpected, Visitor}; /// Functionality for deserializing DER-encoded `SubjectPublicKeyInfo` _without_ making copies of data or @@ -760,13 +760,8 @@ impl<'e> Deserialize<'e> for AttObj { where E: Error, { - // If the encoded length is greater than `usize::MAX / 3`, then it's quite close - // or more than `isize::MAX` which is the max capacity of a `str` and `Vec`. At which - // point, we just error instead of attempting to decode such a large payload. crate::base64url_nopad_decode_len(v.len()) - .ok_or_else(|| { - E::invalid_value(Unexpected::Str(v), &"a shorter base64url-encoded value") - }) + .ok_or_else(|| E::invalid_value(Unexpected::Str(v), &"base64url-encoded value")) .and_then(|len| { // The decoded length is 3/4 of the encoded length, so overflow could only occur // if usize::MAX / 4 < 32 => usize::MAX < 128 < u8::MAX; thus overflow is not @@ -774,7 +769,7 @@ impl<'e> Deserialize<'e> for AttObj { // the raw attestation object by `AuthenticatorAttestation::new`. let mut att_obj = vec![0; len + Sha256::output_size()]; att_obj.truncate(len); - BASE64URL_NOPAD_ENC + BASE64URL_NOPAD .decode_mut(v.as_bytes(), &mut att_obj) .map_err(|e| E::custom(e.error)) .map(|dec_len| { @@ -1387,13 +1382,14 @@ impl<'de> Deserialize<'de> for Registration { mod tests { use super::{ super::{ - super::BASE64URL_NOPAD_ENC, cbor, AuthenticatorAttachment, Ed25519PubKey, Registration, - RsaPubKey, UncompressedP256PubKey, UncompressedP384PubKey, ALG, EC2, EDDSA, ES256, - ES384, KTY, OKP, RSA, + cbor, AuthenticatorAttachment, Ed25519PubKey, Registration, RsaPubKey, + UncompressedP256PubKey, UncompressedP384PubKey, ALG, EC2, EDDSA, ES256, ES384, KTY, + OKP, RSA, }, spki::SubjectPublicKeyInfo, CoseAlgorithmIdentifier, }; + use data_encoding::BASE64URL_NOPAD; use ed25519_dalek::{pkcs8::EncodePublicKey, VerifyingKey}; use p256::{ elliptic_curve::sec1::{FromEncodedPoint as _, ToEncodedPoint as _}, @@ -1691,10 +1687,10 @@ mod tests { .unwrap() .to_public_key_der() .unwrap(); - let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes()); - let b64_adata = BASE64URL_NOPAD_ENC.encode(&att_obj[att_obj.len() - 113..]); - let b64_key = BASE64URL_NOPAD_ENC.encode(pub_key.as_bytes()); - let b64_aobj = BASE64URL_NOPAD_ENC.encode(att_obj.as_slice()); + let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes()); + let b64_adata = BASE64URL_NOPAD.encode(&att_obj[att_obj.len() - 113..]); + let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes()); + let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice()); // Base case is valid. assert!(serde_json::from_str::<Registration>( serde_json::json!({ @@ -1731,7 +1727,7 @@ mod tests { // `id` and `rawId` mismatch. let mut err = Error::invalid_value( Unexpected::Bytes( - BASE64URL_NOPAD_ENC + BASE64URL_NOPAD .decode("ABABABABABABABABABABAA".as_bytes()) .unwrap() .as_slice(), @@ -1873,7 +1869,7 @@ mod tests { // `id` and the credential id in authenticator data mismatch. err = Error::invalid_value( Unexpected::Bytes( - BASE64URL_NOPAD_ENC + BASE64URL_NOPAD .decode("ABABABABABABABABABABAA".as_bytes()) .unwrap() .as_slice(), @@ -1921,7 +1917,7 @@ mod tests { "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { "clientDataJSON": b64_cdata, - "authenticatorData": BASE64URL_NOPAD_ENC.encode(bad_auth.as_slice()), + "authenticatorData": BASE64URL_NOPAD.encode(bad_auth.as_slice()), "transports": [], "publicKey": b64_key, "publicKeyAlgorithm": -8, @@ -2096,7 +2092,7 @@ mod tests { "clientDataJSON": b64_cdata, "authenticatorData": b64_adata, "transports": [], - "publicKey": BASE64URL_NOPAD_ENC.encode(VerifyingKey::from_bytes(&[0; 32]).unwrap().to_public_key_der().unwrap().as_bytes()), + "publicKey": BASE64URL_NOPAD.encode(VerifyingKey::from_bytes(&[0; 32]).unwrap().to_public_key_der().unwrap().as_bytes()), "publicKeyAlgorithm": -8, "attestationObject": b64_aobj, }, @@ -2958,10 +2954,10 @@ mod tests { .unwrap() .to_public_key_der() .unwrap(); - let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes()); - let b64_adata = BASE64URL_NOPAD_ENC.encode(&att_obj[att_obj.len() - 113..]); - let b64_key = BASE64URL_NOPAD_ENC.encode(pub_key.as_bytes()); - let b64_aobj = BASE64URL_NOPAD_ENC.encode(att_obj.as_slice()); + let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes()); + let b64_adata = BASE64URL_NOPAD.encode(&att_obj[att_obj.len() - 113..]); + let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes()); + let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice()); // Base case is valid. assert!(serde_json::from_str::<Registration>( serde_json::json!({ @@ -4051,10 +4047,10 @@ mod tests { att_obj[att_obj_len - 67..att_obj_len - 35] .copy_from_slice(enc_key.x().unwrap().as_slice()); att_obj[att_obj_len - 32..].copy_from_slice(enc_key.y().unwrap().as_slice()); - let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes()); - let b64_adata = BASE64URL_NOPAD_ENC.encode(&att_obj[att_obj.len() - 148..]); - let b64_key = BASE64URL_NOPAD_ENC.encode(pub_key.as_bytes()); - let b64_aobj = BASE64URL_NOPAD_ENC.encode(att_obj.as_slice()); + let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes()); + let b64_adata = BASE64URL_NOPAD.encode(&att_obj[att_obj.len() - 148..]); + let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes()); + let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice()); // Base case is valid. assert!(serde_json::from_str::<Registration>( serde_json::json!({ @@ -4202,7 +4198,7 @@ mod tests { "clientDataJSON": b64_cdata, "authenticatorData": b64_adata, "transports": [], - "publicKey": BASE64URL_NOPAD_ENC.encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()), + "publicKey": BASE64URL_NOPAD.encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()), "publicKeyAlgorithm": -7, "attestationObject": b64_aobj, }, @@ -4514,10 +4510,10 @@ mod tests { att_obj[att_obj_len - 99..att_obj_len - 51] .copy_from_slice(enc_key.x().unwrap().as_slice()); att_obj[att_obj_len - 48..].copy_from_slice(enc_key.y().unwrap().as_slice()); - let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes()); - let b64_adata = BASE64URL_NOPAD_ENC.encode(&att_obj[att_obj.len() - 181..]); - let b64_key = BASE64URL_NOPAD_ENC.encode(pub_key.as_bytes()); - let b64_aobj = BASE64URL_NOPAD_ENC.encode(att_obj.as_slice()); + let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes()); + let b64_adata = BASE64URL_NOPAD.encode(&att_obj[att_obj.len() - 181..]); + let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes()); + let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice()); // Base case is valid. assert!(serde_json::from_str::<Registration>( serde_json::json!({ @@ -4667,7 +4663,7 @@ mod tests { "clientDataJSON": b64_cdata, "authenticatorData": b64_adata, "transports": [], - "publicKey": BASE64URL_NOPAD_ENC.encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()), + "publicKey": BASE64URL_NOPAD.encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()), "publicKeyAlgorithm": -35, "attestationObject": b64_aobj, }, @@ -5235,10 +5231,10 @@ mod tests { let att_obj_len = att_obj.len(); att_obj[att_obj_len - 261..att_obj_len - 5] .copy_from_slice(key.n().to_bytes_be().as_slice()); - let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes()); - let b64_adata = BASE64URL_NOPAD_ENC.encode(&att_obj[att_obj.len() - 343..]); - let b64_key = BASE64URL_NOPAD_ENC.encode(pub_key.as_bytes()); - let b64_aobj = BASE64URL_NOPAD_ENC.encode(att_obj.as_slice()); + let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes()); + let b64_adata = BASE64URL_NOPAD.encode(&att_obj[att_obj.len() - 343..]); + let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes()); + let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice()); // Base case is valid. assert!(serde_json::from_str::<Registration>( serde_json::json!({ @@ -5432,7 +5428,7 @@ mod tests { "clientDataJSON": b64_cdata, "authenticatorData": b64_adata, "transports": [], - "publicKey": BASE64URL_NOPAD_ENC.encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()), + "publicKey": BASE64URL_NOPAD.encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()), "publicKeyAlgorithm": -257, "attestationObject": b64_aobj, }, diff --git a/src/response/register/ser_relaxed.rs b/src/response/register/ser_relaxed.rs @@ -205,11 +205,12 @@ impl<'de> Deserialize<'de> for RegistrationRelaxed { mod tests { use super::{ super::{ - super::{super::request::register::CoseAlgorithmIdentifier, BASE64URL_NOPAD_ENC}, - cbor, AuthenticatorAttachment, ALG, EC2, EDDSA, ES256, ES384, KTY, OKP, RSA, + super::super::request::register::CoseAlgorithmIdentifier, cbor, + AuthenticatorAttachment, ALG, EC2, EDDSA, ES256, ES384, KTY, OKP, RSA, }, RegistrationRelaxed, }; + use data_encoding::BASE64URL_NOPAD; use ed25519_dalek::{pkcs8::EncodePublicKey, VerifyingKey}; use p256::{ elliptic_curve::sec1::{FromEncodedPoint as _, ToEncodedPoint as _}, @@ -386,10 +387,10 @@ mod tests { .unwrap() .to_public_key_der() .unwrap(); - let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes()); - let b64_adata = BASE64URL_NOPAD_ENC.encode(&att_obj[att_obj.len() - 113..]); - let b64_key = BASE64URL_NOPAD_ENC.encode(pub_key.as_bytes()); - let b64_aobj = BASE64URL_NOPAD_ENC.encode(att_obj.as_slice()); + let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes()); + let b64_adata = BASE64URL_NOPAD.encode(&att_obj[att_obj.len() - 113..]); + let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes()); + let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice()); // Base case is valid. assert!(serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ @@ -426,7 +427,7 @@ mod tests { // `id` and `rawId` mismatch. let mut err = Error::invalid_value( Unexpected::Bytes( - BASE64URL_NOPAD_ENC + BASE64URL_NOPAD .decode("ABABABABABABABABABABAA".as_bytes()) .unwrap() .as_slice(), @@ -540,7 +541,7 @@ mod tests { // `id` and the credential id in authenticator data mismatch. err = Error::invalid_value( Unexpected::Bytes( - BASE64URL_NOPAD_ENC + BASE64URL_NOPAD .decode("ABABABABABABABABABABAA".as_bytes()) .unwrap() .as_slice(), @@ -588,7 +589,7 @@ mod tests { "rawId": "AAAAAAAAAAAAAAAAAAAAAA", "response": { "clientDataJSON": b64_cdata, - "authenticatorData": BASE64URL_NOPAD_ENC.encode(bad_auth.as_slice()), + "authenticatorData": BASE64URL_NOPAD.encode(bad_auth.as_slice()), "transports": [], "publicKey": b64_key, "publicKeyAlgorithm": -8, @@ -731,7 +732,7 @@ mod tests { "clientDataJSON": b64_cdata, "authenticatorData": b64_adata, "transports": [], - "publicKey": BASE64URL_NOPAD_ENC.encode(VerifyingKey::from_bytes(&[0; 32]).unwrap().to_public_key_der().unwrap().as_bytes()), + "publicKey": BASE64URL_NOPAD.encode(VerifyingKey::from_bytes(&[0; 32]).unwrap().to_public_key_der().unwrap().as_bytes()), "publicKeyAlgorithm": -8, "attestationObject": b64_aobj, }, @@ -1496,10 +1497,10 @@ mod tests { .unwrap() .to_public_key_der() .unwrap(); - let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes()); - let b64_adata = BASE64URL_NOPAD_ENC.encode(&att_obj[att_obj.len() - 113..]); - let b64_key = BASE64URL_NOPAD_ENC.encode(pub_key.as_bytes()); - let b64_aobj = BASE64URL_NOPAD_ENC.encode(att_obj.as_slice()); + let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes()); + let b64_adata = BASE64URL_NOPAD.encode(&att_obj[att_obj.len() - 113..]); + let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes()); + let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice()); // Base case is valid. assert!(serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ @@ -2570,10 +2571,10 @@ mod tests { att_obj[att_obj_len - 67..att_obj_len - 35] .copy_from_slice(enc_key.x().unwrap().as_slice()); att_obj[att_obj_len - 32..].copy_from_slice(enc_key.y().unwrap().as_slice()); - let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes()); - let b64_adata = BASE64URL_NOPAD_ENC.encode(&att_obj[att_obj.len() - 148..]); - let b64_key = BASE64URL_NOPAD_ENC.encode(pub_key.as_bytes()); - let b64_aobj = BASE64URL_NOPAD_ENC.encode(att_obj.as_slice()); + let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes()); + let b64_adata = BASE64URL_NOPAD.encode(&att_obj[att_obj.len() - 148..]); + let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes()); + let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice()); // Base case is valid. assert!(serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ @@ -2708,7 +2709,7 @@ mod tests { "clientDataJSON": b64_cdata, "authenticatorData": b64_adata, "transports": [], - "publicKey": BASE64URL_NOPAD_ENC.encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()), + "publicKey": BASE64URL_NOPAD.encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()), "publicKeyAlgorithm": -7, "attestationObject": b64_aobj, }, @@ -3006,10 +3007,10 @@ mod tests { att_obj[att_obj_len - 99..att_obj_len - 51] .copy_from_slice(enc_key.x().unwrap().as_slice()); att_obj[att_obj_len - 48..].copy_from_slice(enc_key.y().unwrap().as_slice()); - let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes()); - let b64_adata = BASE64URL_NOPAD_ENC.encode(&att_obj[att_obj.len() - 181..]); - let b64_key = BASE64URL_NOPAD_ENC.encode(pub_key.as_bytes()); - let b64_aobj = BASE64URL_NOPAD_ENC.encode(att_obj.as_slice()); + let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes()); + let b64_adata = BASE64URL_NOPAD.encode(&att_obj[att_obj.len() - 181..]); + let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes()); + let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice()); // Base case is valid. assert!(serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ @@ -3146,7 +3147,7 @@ mod tests { "clientDataJSON": b64_cdata, "authenticatorData": b64_adata, "transports": [], - "publicKey": BASE64URL_NOPAD_ENC.encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()), + "publicKey": BASE64URL_NOPAD.encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()), "publicKeyAlgorithm": -35, "attestationObject": b64_aobj, }, @@ -3714,10 +3715,10 @@ mod tests { let att_obj_len = att_obj.len(); att_obj[att_obj_len - 261..att_obj_len - 5] .copy_from_slice(key.n().to_bytes_be().as_slice()); - let b64_cdata = BASE64URL_NOPAD_ENC.encode(c_data_json.as_bytes()); - let b64_adata = BASE64URL_NOPAD_ENC.encode(&att_obj[att_obj.len() - 343..]); - let b64_key = BASE64URL_NOPAD_ENC.encode(pub_key.as_bytes()); - let b64_aobj = BASE64URL_NOPAD_ENC.encode(att_obj.as_slice()); + let b64_cdata = BASE64URL_NOPAD.encode(c_data_json.as_bytes()); + let b64_adata = BASE64URL_NOPAD.encode(&att_obj[att_obj.len() - 343..]); + let b64_key = BASE64URL_NOPAD.encode(pub_key.as_bytes()); + let b64_aobj = BASE64URL_NOPAD.encode(att_obj.as_slice()); // Base case is valid. assert!(serde_json::from_str::<RegistrationRelaxed>( serde_json::json!({ @@ -3898,7 +3899,7 @@ mod tests { "clientDataJSON": b64_cdata, "authenticatorData": b64_adata, "transports": [], - "publicKey": BASE64URL_NOPAD_ENC.encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()), + "publicKey": BASE64URL_NOPAD.encode(bad_pub_key.to_public_key_der().unwrap().as_bytes()), "publicKeyAlgorithm": -257, "attestationObject": b64_aobj, }, diff --git a/src/response/ser.rs b/src/response/ser.rs @@ -5,14 +5,13 @@ extern crate alloc; use super::{ AllAcceptedCredentialsOptions, AuthTransports, AuthenticatorAttachment, AuthenticatorTransport, - Challenge, CredentialId, CurrentUserDetailsOptions, Origin, SentChallenge, BASE64URL_NOPAD_ENC, + Challenge, CredentialId, CurrentUserDetailsOptions, Origin, SentChallenge, }; use alloc::borrow::Cow; use core::{ fmt::{self, Formatter}, marker::PhantomData, }; -#[cfg(doc)] use data_encoding::BASE64URL_NOPAD; use serde::{ de::{Deserialize, Deserializer, Error, IgnoredAny, MapAccess, SeqAccess, Unexpected, Visitor}, @@ -245,7 +244,7 @@ impl<T: AsRef<[u8]>> Serialize for CredentialId<T> { where S: Serializer, { - serializer.serialize_str(BASE64URL_NOPAD_ENC.encode(self.0.as_ref()).as_str()) + serializer.serialize_str(BASE64URL_NOPAD.encode(self.0.as_ref()).as_str()) } } impl<'de> Deserialize<'de> for CredentialId<Vec<u8>> { @@ -285,7 +284,7 @@ impl<'de> Deserialize<'de> for CredentialId<Vec<u8>> { ..=crate::base64url_nopad_len(super::CRED_ID_MAX_LEN)) .contains(&v.len()) { - BASE64URL_NOPAD_ENC + BASE64URL_NOPAD .decode(v.as_bytes()) .map_err(E::custom) .map(CredentialId) @@ -376,7 +375,7 @@ impl<'de> Deserialize<'de> for Base64DecodedVal { where E: Error, { - BASE64URL_NOPAD_ENC + BASE64URL_NOPAD .decode(v.as_bytes()) .map_err(E::custom) .map(Base64DecodedVal) @@ -428,10 +427,9 @@ impl<'de> Deserialize<'de> for SentChallenge { where E: Error, { - // We try to avoid decoding when possible by at least ensuring the input length is correct. if v.len() == Challenge::BASE64_LEN { let mut data = [0; 16]; - BASE64URL_NOPAD_ENC + BASE64URL_NOPAD .decode_mut(v, data.as_mut_slice()) .map_err(|err| E::custom(err.error)) .map(|len| {