webauthn_rp

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

commit 6a8646451f7de41194173cdcde74d7b6cbd0c20c
parent 27188a769b4ac4f2e32ea0a86a27923487469014
Author: Zack Newman <zack@philomathiclife.com>
Date:   Sun, 22 Jun 2025 18:30:16 -0600

prevent 'weak' ed25519 keys

Diffstat:
MCargo.toml | 4++--
Msrc/lib.rs | 4++--
Msrc/request/register.rs | 64++++++++++++++++++++++++++++++++--------------------------------
Msrc/response/auth.rs | 6++++++
Msrc/response/register.rs | 45+++++++++++++++++++++++++++++++++++++--------
Msrc/response/register/error.rs | 3+++
6 files changed, 82 insertions(+), 44 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -63,8 +63,8 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] data-encoding = { version = "2.9.0", default-features = false } -ed25519-dalek = { version = "2.1.1", default-features = false, features = ["fast"] } -hashbrown = { version = "0.15.3", default-features = false } +ed25519-dalek = { version = "2.1.1", default-features = false } +hashbrown = { version = "0.15.4", default-features = false } p256 = { version = "0.13.2", default-features = false, features = ["ecdsa"] } p384 = { version = "0.13.1", default-features = false, features = ["ecdsa"] } precis-profiles = { version = "0.1.12", default-features = false } diff --git a/src/lib.rs b/src/lib.rs @@ -515,8 +515,8 @@ reason = "RustCrypto hasn't updated rand yet" )] #![cfg_attr(docsrs, feature(doc_cfg))] -#[cfg(not(any(feature = "custom", all(feature = "bin", feature = "serde"))))] -compile_error!("'custom' must be enabled or both 'bin' and 'serde' must be enabled"); +//#[cfg(not(any(feature = "custom", all(feature = "bin", feature = "serde"))))] +//compile_error!("'custom' must be enabled or both 'bin' and 'serde' must be enabled"); #[cfg(feature = "serializable_server_state")] use crate::request::{ auth::ser_server_state::{ diff --git a/src/request/register.rs b/src/request/register.rs @@ -3135,38 +3135,38 @@ mod tests { // Length is 32. 32, // Compressed-y coordinate. - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, + 59, + 106, + 39, + 188, + 206, + 182, + 164, + 45, + 98, + 163, + 168, + 208, + 42, + 111, + 13, + 115, + 101, + 50, + 21, + 119, + 29, + 226, + 67, + 166, + 58, + 192, + 72, + 161, + 139, + 89, + 218, + 41, ] .as_slice(), ); diff --git a/src/response/auth.rs b/src/response/auth.rs @@ -451,6 +451,12 @@ impl<const USER_LEN: usize, const DISCOVERABLE: bool> AuthResponse .and_then(|ver_key| { Signature::from_slice(self.signature.as_slice()) .and_then(|sig| { + // We don't need to use `VerifyingKey::verify_strict` since + // `Ed25519PubKey::into_ver_key` verifies the public key is not + // in the small-order subgroup. `VerifyingKey::verify_strict` additionally + // ensures _R_ of the signature is not in the small-order subgroup, but this + // doesn't provide additional benefits and is still not enough to comply + // with standards like RFC 8032 or NIST SP 800-186. ver_key.verify( self.authenticator_data_and_c_data_hash.as_slice(), &sig, diff --git a/src/response/register.rs b/src/response/register.rs @@ -740,14 +740,8 @@ impl Ed25519PubKey<&[u8]> { self.into_ver_key().map(|_| ()) } /// Converts `self` into [`VerifyingKey`]. - #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")] pub(super) fn into_ver_key(self) -> Result<VerifyingKey, PubKeyErr> { - VerifyingKey::from_bytes( - self.0 - .try_into() - .unwrap_or_else(|_e| unreachable!("&Array::try_from has a bug")), - ) - .map_err(|_e| PubKeyErr::Ed25519) + self.into_owned().into_ver_key() } /// Transforms `self` into an "owned" version. #[inline] @@ -768,7 +762,36 @@ impl Ed25519PubKey<[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]> { } /// Converts `self` into [`VerifyingKey`]. fn into_ver_key(self) -> Result<VerifyingKey, PubKeyErr> { - VerifyingKey::from_bytes(&self.0).map_err(|_e| PubKeyErr::Ed25519) + // ["Taming the many EdDSAs"](https://eprint.iacr.org/2020/1244.pdf) goes over + // and proves varying levels of signature security. The only property that is + // important for WebAuthn is existential unforgeability under chosen message + // attacks (EUF-CMA). No matter how `ed25519-dalek` is used this is met. + // Additional properties that may be of importance are strong unforgeability + // under chosen message attacks (SUF-CMA), binding signature (BS), and + // strongly binding signature (SBS). + // No matter how `ed25519-dalek` is used, SUF-CMA is achieved. Because + // we always achieve SUF-CMA, we elect—despite no benefit in WebAuthn—to also + // achieve SBS. One can achieve SBS-secure by simply rejecting small-order + // keys which is precisely what `VerifyingKey::is_weak` does. + // Note this means we _don't_ conform to [RFC 8032](https://www.rfc-editor.org/rfc/rfc8032) + // nor [NIST SP 800-186](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-186.pdf). + // As stated, there is no additional security by conforming to the above specs though; + // furthermore, RFC 8032 does not require rejecting small-order points despite requiring + // canonical encoding of points; thus it does not achieve SBS-security. + // NIST SP 800-186 does achieve SUF-CMA and SBS-security but requires additional properties + // that have no cryptographic importance. Specifically it mandates canonicity of encodings + // for both public keys and signatures and requires not only that points not be small-order + // but more strictly that points are in the prime-order subgroup (excluding the identity). + // This is more work for no benefit, so we elect to stay within the confines of the exposed API. + VerifyingKey::from_bytes(&self.0) + .map_err(|_e| PubKeyErr::Ed25519) + .and_then(|key| { + if key.is_weak() { + Err(PubKeyErr::Ed25519) + } else { + Ok(key) + } + }) } } impl<'a: 'b, 'b> TryFrom<&'a [u8]> for Ed25519PubKey<&'b [u8]> { @@ -2945,6 +2968,12 @@ impl AuthResponse for AuthenticatorAttestation { match val.data.attestation { AttestationFormat::None => Ok(()), AttestationFormat::Packed(packed) => match packed.signature { + // We don't need to use `VerifyingKey::verify_strict` since + // `Ed25519PubKey::into_ver_key` verifies the public key is not + // in the small-order subgroup. `VerifyingKey::verify_strict` additionally + // ensures _R_ of the signature is not in the small-order subgroup, but this + // doesn't provide additional benefits and is still not enough to comply + // with standards like RFC 8032 or NIST SP 800-186. Sig::Ed25519(sig) => ver_key.verify(val.auth_data_and_32_trailing_bytes, &sig.into_sig()).map_err(|_e| AuthRespErr::Signature), Sig::P256(_) | Sig::P384(_) | Sig::Rs256(_) => unreachable!("there is a bug in AttestationObject::from_data"), } diff --git a/src/response/register/error.rs b/src/response/register/error.rs @@ -135,6 +135,9 @@ impl Error for RsaPubKeyErr {} #[derive(Clone, Copy, Debug)] pub enum PubKeyErr { /// Error when [`Ed25519PubKey`] is not valid. + /// + /// Note this means the underlying point is either not on the curve or is an element + /// of the small-order subgroup. Ed25519, /// Error when [`UncompressedP256PubKey`] or [`CompressedP256PubKey`] is not valid. P256,