From c7a17d5b21b028236f62ca2c5c333da483372ee8 Mon Sep 17 00:00:00 2001 From: Alfie Fresta Date: Sat, 30 May 2026 16:47:53 +0100 Subject: [PATCH 1/4] fix(webauthn): bounds-check slicing flagged by clippy::indexing_slicing CI builds with --all-features, which disables the indexing_slicing deny, so two ops-layer sites slipped through. get_assertion: the SHA-256 digest is already 32 bytes, so convert it into [u8; 32] instead of slicing finalize()[..32]. origin: use as_bytes().get(boundary) for the label-boundary check so an out-of-range index cannot panic. --- libwebauthn/src/ops/webauthn/get_assertion.rs | 6 ++---- libwebauthn/src/ops/webauthn/idl/origin.rs | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/libwebauthn/src/ops/webauthn/get_assertion.rs b/libwebauthn/src/ops/webauthn/get_assertion.rs index 7e45b51d..1fcbf7d0 100644 --- a/libwebauthn/src/ops/webauthn/get_assertion.rs +++ b/libwebauthn/src/ops/webauthn/get_assertion.rs @@ -61,13 +61,11 @@ impl PrfInputValue { /// WebAuthn L3 PRF: salt = SHA-256("WebAuthn PRF" || 0x00 || ev.{first,second}). pub fn to_hmac_secret_input(&self) -> HMACGetSecretInput { const PREFIX: &[u8] = b"WebAuthn PRF\x00"; - let hash = |slice: &[u8]| { + let hash = |slice: &[u8]| -> [u8; 32] { let mut hasher = Sha256::default(); hasher.update(PREFIX); hasher.update(slice); - let mut out = [0u8; 32]; - out.copy_from_slice(&hasher.finalize()[..32]); - out + hasher.finalize().into() }; HMACGetSecretInput { salt1: hash(&self.first), diff --git a/libwebauthn/src/ops/webauthn/idl/origin.rs b/libwebauthn/src/ops/webauthn/idl/origin.rs index 7be9cce5..b788edcb 100644 --- a/libwebauthn/src/ops/webauthn/idl/origin.rs +++ b/libwebauthn/src/ops/webauthn/idl/origin.rs @@ -315,7 +315,7 @@ pub(crate) fn is_registrable_domain_suffix_or_equal( return false; } let boundary = effective_domain.len() - rp_id.len() - 1; - if effective_domain.as_bytes()[boundary] != b'.' { + if effective_domain.as_bytes().get(boundary) != Some(&b'.') { return false; } if &effective_domain[boundary + 1..] != rp_id { From 8b02aafefd7858ddf3eec24b1a4cd44a93151c62 Mon Sep 17 00:00:00 2001 From: Alfie Fresta Date: Sat, 30 May 2026 16:47:53 +0100 Subject: [PATCH 2/4] chore(webauthn): allow clippy::expect_used on infallible clientDataJSON to_json serializes a fixed-shape struct of strings and a bool, which serde_json cannot fail to encode. The API is intentionally infallible, so allow expect_used on the function rather than threading a Result through hash() and every caller. --- libwebauthn/src/ops/webauthn/client_data.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/libwebauthn/src/ops/webauthn/client_data.rs b/libwebauthn/src/ops/webauthn/client_data.rs index 27a0d6b2..484d0983 100644 --- a/libwebauthn/src/ops/webauthn/client_data.rs +++ b/libwebauthn/src/ops/webauthn/client_data.rs @@ -41,6 +41,7 @@ impl ClientData { /// Strings are escaped per WebAuthn L3 §5.8.1.2 (CCDToString), via /// `serde_json`'s RFC 8259 string encoder. Field order matches the spec: /// `type`, `challenge`, `origin`, `topOrigin?`, `crossOrigin`. + #[allow(clippy::expect_used)] // serialization of this fixed-shape struct cannot fail pub fn to_json(&self) -> String { let operation = match self.operation { Operation::MakeCredential => "webauthn.create", From 7fd290f954ea2fad241849927ff327fa6b91f9a2 Mon Sep 17 00:00:00 2001 From: Alfie Fresta Date: Sat, 30 May 2026 17:08:56 +0100 Subject: [PATCH 3/4] fix(transport): bounds-check continuation-packet parsing in HID framing HidMessageParser::update validated continuation packets by indexing packet[4] and slicing packet[..4]. The length guard at the top of the function keeps those in bounds, but the raw indexing trips clippy::indexing_slicing. Use .get() and is_some_and so the accesses are bounded at the call site, with no change in behaviour. --- libwebauthn/src/transport/hid/framing.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/libwebauthn/src/transport/hid/framing.rs b/libwebauthn/src/transport/hid/framing.rs index da086a70..3ebbcd0b 100644 --- a/libwebauthn/src/transport/hid/framing.rs +++ b/libwebauthn/src/transport/hid/framing.rs @@ -141,7 +141,10 @@ impl HidMessageParser { if self.packets.is_empty() { // First packet must be an initialization packet: high bit of // byte 4 set (CTAP 2.2 §11.2.4). - if packet[4] & PACKET_INITIAL_CMD_MASK == 0 { + let is_initialization = packet + .get(4) + .is_some_and(|&byte| byte & PACKET_INITIAL_CMD_MASK != 0); + if !is_initialization { error!("First packet is not an initialization packet"); return Err(IOError::new( IOErrorKind::InvalidData, @@ -151,15 +154,20 @@ impl HidMessageParser { } else { // Continuation packets: same CID as the initial packet, SEQ has // high bit cleared, SEQ starts at 0 and increments monotonically. - let initial = &self.packets[0]; - if packet[..4] != initial[..4] { + let initial_cid = self.packets.first().and_then(|initial| initial.get(..4)); + if packet.get(..4) != initial_cid { error!("Continuation packet CID does not match initial packet"); return Err(IOError::new( IOErrorKind::InvalidData, "Continuation packet CID mismatch", )); } - let seq = packet[4]; + let Some(&seq) = packet.get(4) else { + return Err(IOError::new( + IOErrorKind::InvalidData, + "Packet length is invalid", + )); + }; if seq & PACKET_INITIAL_CMD_MASK != 0 { error!(seq, "Unexpected init packet during continuation"); return Err(IOError::new( From 9e9be1244dafa4ce8bb0cf459bfdd55cc18363b5 Mon Sep 17 00:00:00 2001 From: Alfie Fresta Date: Sat, 30 May 2026 17:08:56 +0100 Subject: [PATCH 4/4] chore(clippy): enforce production denies under the virt feature The denies (panic, unwrap, expect, indexing_slicing, and others) were gated on not(any(test, feature = "virt")). Every CI job builds with --all-features, so the virt feature was always on and the denies never ran in CI. Violations could then land unnoticed. Gate the denies on not(test) instead, so they cover production code even when virt is enabled. The few virt test-utility match arms that panic on a poisoned lock or mark an unreachable path keep a local allow. The now-redundant module-level deny in psl is dropped. --- libwebauthn/src/lib.rs | 17 +++++++++-------- libwebauthn/src/ops/webauthn/psl/mod.rs | 3 --- libwebauthn/src/transport/hid/channel.rs | 6 ++++++ 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/libwebauthn/src/lib.rs b/libwebauthn/src/lib.rs index 7972f919..d7f4abd2 100644 --- a/libwebauthn/src/lib.rs +++ b/libwebauthn/src/lib.rs @@ -1,11 +1,12 @@ -// Tests and the virt test-utility feature are allowed to use unwrap/expect/panic for convenience. -#![cfg_attr(not(any(test, feature = "virt")), deny(clippy::unwrap_used))] -#![cfg_attr(not(any(test, feature = "virt")), deny(clippy::expect_used))] -#![cfg_attr(not(any(test, feature = "virt")), deny(clippy::panic))] -#![cfg_attr(not(any(test, feature = "virt")), deny(clippy::todo))] -#![cfg_attr(not(any(test, feature = "virt")), deny(clippy::unreachable))] -#![cfg_attr(not(any(test, feature = "virt")), deny(clippy::indexing_slicing))] -#![cfg_attr(not(any(test, feature = "virt")), deny(clippy::unwrap_in_result))] +// Production code must not panic. Tests keep unwrap/expect/panic latitude +// through `not(test)`, and the virt test-utility code through local allows. +#![cfg_attr(not(test), deny(clippy::unwrap_used))] +#![cfg_attr(not(test), deny(clippy::expect_used))] +#![cfg_attr(not(test), deny(clippy::panic))] +#![cfg_attr(not(test), deny(clippy::todo))] +#![cfg_attr(not(test), deny(clippy::unreachable))] +#![cfg_attr(not(test), deny(clippy::indexing_slicing))] +#![cfg_attr(not(test), deny(clippy::unwrap_in_result))] #[cfg(all( feature = "nfc", diff --git a/libwebauthn/src/ops/webauthn/psl/mod.rs b/libwebauthn/src/ops/webauthn/psl/mod.rs index 10ed069d..bd2c2da5 100644 --- a/libwebauthn/src/ops/webauthn/psl/mod.rs +++ b/libwebauthn/src/ops/webauthn/psl/mod.rs @@ -20,9 +20,6 @@ //! Most callers should use [`SystemPublicSuffixList::auto`], which probes //! the standard system paths for whichever format is available. -// Module-scoped until the crate-wide indexing_slicing deny lands. -#![cfg_attr(not(any(test, feature = "virt")), deny(clippy::indexing_slicing))] - pub mod dafsa; pub mod dat; mod system; diff --git a/libwebauthn/src/transport/hid/channel.rs b/libwebauthn/src/transport/hid/channel.rs index 6afe084b..1d9328d8 100644 --- a/libwebauthn/src/transport/hid/channel.rs +++ b/libwebauthn/src/transport/hid/channel.rs @@ -244,6 +244,8 @@ impl<'d> HidChannel<'d> { .open_device(&hidapi) .or(Err(Error::Transport(TransportError::ConnectionFailed)))?), #[cfg(feature = "virt")] + #[allow(clippy::unreachable)] + // virtual devices never go through hid_open HidBackendDevice::VirtualDevice(_) => unreachable!(), } } @@ -317,6 +319,8 @@ impl<'d> HidChannel<'d> { response } #[cfg(feature = "virt")] + #[allow(clippy::panic)] + // virt test-utility: a poisoned lock is unrecoverable OpenHidDevice::VirtualDevice(backend) => { let Ok(mut guard) = backend.lock() else { panic!("Poisoned lock on Virtual HID device"); @@ -378,6 +382,8 @@ impl<'d> HidChannel<'d> { })? } #[cfg(feature = "virt")] + #[allow(clippy::panic)] + // virt test-utility: a poisoned lock is unrecoverable OpenHidDevice::VirtualDevice(backend) => { let Ok(mut guard) = backend.lock() else { panic!("Poisoned lock on Virtual HID device");