diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b5d255..1f200ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Implement the `config` command with these subcommands: - `toggleAlwaysUv` - `setMinPINLength` +- Add `ccid_transport` to `Config` and set `transports` in `get_info` accordingly. - Load full credential from filesstem for getAssertion if an allow list is used with a discoverable credential. - Use UTF-8 code points instead of bytes when checking the minimum length for PINs. diff --git a/fuzz/fuzz_targets/ctap.rs b/fuzz/fuzz_targets/ctap.rs index c937193..2b9b045 100644 --- a/fuzz/fuzz_targets/ctap.rs +++ b/fuzz/fuzz_targets/ctap.rs @@ -18,6 +18,7 @@ fuzz_target!(|requests: Vec>| { max_resident_credential_count: None, large_blobs: None, nfc_transport: false, + ccid_transport: false, firmware_version: Some(0), }, ); diff --git a/src/ctap2.rs b/src/ctap2.rs index 018fd1d..28c9258 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -110,6 +110,9 @@ impl Authenticator for crate::Authenti if self.config.nfc_transport { transports.push(Transport::Nfc).unwrap(); } + if self.config.ccid_transport { + transports.push(Transport::SmartCard).unwrap(); + } transports.push(Transport::Usb).unwrap(); let mut attestation_formats = Vec::new(); diff --git a/src/lib.rs b/src/lib.rs index 86605af..03f400b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -126,6 +126,10 @@ pub struct Config { pub large_blobs: Option, /// Whether the authenticator supports the NFC transport. pub nfc_transport: bool, + /// Whether the authenticator exposes FIDO over a CCID smart-card interface + /// (CTAP 2.3 §3 FIDO Interfaces). When `true`, GetInfo advertises the + /// `"smart-card"` transport alongside `"usb"` / `"nfc"`. + pub ccid_transport: bool, /// Firmware version reported by `authenticatorGetInfo` (CTAP 2.1 §6.4 0x0E). /// /// The runner is expected to plumb its own version constant in here. diff --git a/tests/basic.rs b/tests/basic.rs index eb8eee8..34271ff 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -2669,3 +2669,91 @@ fn test_make_credential_hmac_secret_mc_invalid_salt_length_rejected() { assert_eq!(result.err(), Some(Ctap2Error(0x03))); }) } + +// ---------------------------------------------------------------------------- +// Transports (CTAP 2.1 §6.4 0x09 / CTAP 2.3 §3 smart-card) +// ---------------------------------------------------------------------------- + +/// Default config (USB only) advertises only `"usb"`. +#[test] +fn test_transports_usb_only_by_default() { + virt::run_ctap2(|device| { + let reply = device.exec(GetInfo).unwrap(); + let transports = reply.transports.expect("transports list missing"); + assert_eq!(transports, vec!["usb".to_owned()]); + }) +} + +/// With `nfc_transport=true`, `"nfc"` and `"usb"` are advertised. +#[test] +fn test_transports_nfc_added() { + let options = Options { + nfc_transport: true, + ..Default::default() + }; + virt::run_ctap2_with_options(options, |device| { + let reply = device.exec(GetInfo).unwrap(); + let transports = reply.transports.expect("transports list missing"); + assert!(transports.contains(&"nfc".to_owned())); + assert!(transports.contains(&"usb".to_owned())); + }) +} + +/// CTAP 2.3 §3: with `ccid_transport=true`, `"smart-card"` is advertised +/// alongside the other transports. +#[test] +fn test_transports_smart_card_advertised_when_ccid_enabled() { + let options = Options { + ccid_transport: true, + ..Default::default() + }; + virt::run_ctap2_with_options(options, |device| { + let reply = device.exec(GetInfo).unwrap(); + let transports = reply.transports.expect("transports list missing"); + assert!( + transports.contains(&"smart-card".to_owned()), + "smart-card missing from transports: {:?}", + transports + ); + }) +} + +/// `"smart-card"` is NOT advertised by default (no CCID). +#[test] +fn test_transports_smart_card_absent_when_ccid_disabled() { + virt::run_ctap2(|device| { + let reply = device.exec(GetInfo).unwrap(); + let transports = reply.transports.expect("transports list missing"); + assert!(!transports.contains(&"smart-card".to_owned())); + }) +} + +/// NFC + CCID together: all three transports advertised. Verifies the +/// flags are independent. +#[test] +fn test_transports_nfc_and_smart_card_combined() { + let options = Options { + nfc_transport: true, + ccid_transport: true, + ..Default::default() + }; + virt::run_ctap2_with_options(options, |device| { + let reply = device.exec(GetInfo).unwrap(); + let transports = reply.transports.expect("transports list missing"); + assert!( + transports.contains(&"nfc".to_owned()), + "nfc missing: {:?}", + transports + ); + assert!( + transports.contains(&"smart-card".to_owned()), + "smart-card missing: {:?}", + transports + ); + assert!( + transports.contains(&"usb".to_owned()), + "usb missing: {:?}", + transports + ); + }) +} diff --git a/tests/virt/mod.rs b/tests/virt/mod.rs index 586d7b6..71f54c5 100644 --- a/tests/virt/mod.rs +++ b/tests/virt/mod.rs @@ -73,7 +73,8 @@ where skip_up_timeout: None, max_resident_credential_count: options.max_resident_credential_count, large_blobs: None, - nfc_transport: false, + nfc_transport: options.nfc_transport, + ccid_transport: options.ccid_transport, firmware_version: Some(0), }, ); @@ -134,6 +135,8 @@ pub type InspectFsFn = Box; pub struct Options { pub files: Vec<(PathBuf, Vec)>, pub max_resident_credential_count: Option, + pub nfc_transport: bool, + pub ccid_transport: bool, pub inspect_ifs: Option, } diff --git a/tests/webauthn/mod.rs b/tests/webauthn/mod.rs index 635a12d..15fe884 100644 --- a/tests/webauthn/mod.rs +++ b/tests/webauthn/mod.rs @@ -977,6 +977,7 @@ pub struct GetInfoReply { pub aaguid: Value, pub options: Option>, pub pin_protocols: Option>, + pub transports: Option>, pub force_pin_change: Option, pub min_pin_length: Option, pub attestation_formats: Option>, @@ -992,6 +993,8 @@ impl From for GetInfoReply { aaguid: map.remove(&3).unwrap().deserialized().unwrap(), options: map.remove(&4).map(|value| value.deserialized().unwrap()), pin_protocols: map.remove(&6).map(|value| value.deserialized().unwrap()), + // 0x09: transports (CTAP 2.1) + transports: map.remove(&9).map(|value| value.deserialized().unwrap()), // 0x0C: forcePINChange (CTAP 2.1) force_pin_change: map.remove(&0x0C).map(|value| value.deserialized().unwrap()), // 0x0D: minPINLength (CTAP 2.1)