Skip to content

Commit 9099de3

Browse files
fix(nfc): recognise FIDO_2_1, FIDO_2_1_PRE, FIDO_2_2 in select_fido2 (#197)
Stricter CTAP 2.1+ NFC authenticators advertise the version string `FIDO_2_1`, `FIDO_2_1_PRE`, or `FIDO_2_2` from the FIDO AID SELECT response without shadowing as `U2F_V2`. The current code only matches `FIDO_2_0` and `U2F_V2`, so those devices are reported as supporting neither protocol. ## Changes - Match all spec-defined version strings in `select_fido2`. - Add a unit test for the version-string classifier. Other NFC items (CTAP2 keepalive, SELECT Le, SW preservation) are tracked separately in #195. Refs: CTAP 2.2 section 11.3.1.
1 parent 976bd8e commit 9099de3

1 file changed

Lines changed: 41 additions & 1 deletion

File tree

libwebauthn/src/transport/nfc/channel.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ const SELECT_P2: u8 = 0x00;
2626
const FIDO2_AID: &[u8; 8] = b"\xa0\x00\x00\x06\x47\x2f\x00\x01";
2727
const SW1_MORE_DATA: u8 = 0x61;
2828

29+
/// Returns true if `version` is a known FIDO2 version string returned by an
30+
/// authenticator's FIDO AID SELECT response. See CTAP 2.2 section 11.3.1.
31+
fn is_fido2_version(version: &[u8]) -> bool {
32+
matches!(
33+
version,
34+
b"FIDO_2_0" | b"FIDO_2_1_PRE" | b"FIDO_2_1" | b"FIDO_2_2"
35+
)
36+
}
37+
2938
pub type CancelNfcOperation = ();
3039

3140
#[derive(thiserror::Error)]
@@ -148,8 +157,9 @@ where
148157
let response = self.handle(self.ctx, command)?;
149158
let mut u2f = false;
150159
let mut fido2 = false;
151-
if response == b"FIDO_2_0" {
160+
if is_fido2_version(&response) {
152161
// If the authenticator ONLY implements CTAP2, the device SHALL respond with "FIDO_2_0", or 0x4649444f5f325f30.
162+
// Stricter CTAP 2.1+ authenticators may instead return "FIDO_2_1_PRE", "FIDO_2_1", or "FIDO_2_2".
153163
fido2 = true;
154164
// NOTE: Yubikeys seem to ignore this part of the specification and always return U2F_V2, even if U2F-NFC is disabled.
155165
} else if response == b"U2F_V2" {
@@ -331,3 +341,33 @@ where
331341
self.auth_token_data = None;
332342
}
333343
}
344+
345+
#[cfg(test)]
346+
mod tests {
347+
use super::is_fido2_version;
348+
349+
#[test]
350+
fn fido2_versions_are_recognised() {
351+
assert!(is_fido2_version(b"FIDO_2_0"));
352+
assert!(is_fido2_version(b"FIDO_2_1_PRE"));
353+
assert!(is_fido2_version(b"FIDO_2_1"));
354+
assert!(is_fido2_version(b"FIDO_2_2"));
355+
}
356+
357+
#[test]
358+
fn u2f_v2_is_not_classified_as_fido2() {
359+
// U2F_V2 is handled by a separate fallback path that probes via
360+
// ctap2_get_info, so the version-string classifier must report false.
361+
assert!(!is_fido2_version(b"U2F_V2"));
362+
}
363+
364+
#[test]
365+
fn unknown_versions_are_rejected() {
366+
assert!(!is_fido2_version(b""));
367+
assert!(!is_fido2_version(b"FIDO_2"));
368+
assert!(!is_fido2_version(b"FIDO_2_3"));
369+
assert!(!is_fido2_version(b"FIDO_3_0"));
370+
assert!(!is_fido2_version(b"fido_2_0"));
371+
assert!(!is_fido2_version(b"FIDO_2_0\0"));
372+
}
373+
}

0 commit comments

Comments
 (0)