ctap2.3: advertise smart-card transport when CCID is enabled

This commit is contained in:
Emanuele Cesena
2026-05-09 07:24:14 +02:00
committed by Robin Krahl
parent bcec723f1b
commit e91fb4779f
7 changed files with 104 additions and 1 deletions
+1
View File
@@ -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.
+1
View File
@@ -18,6 +18,7 @@ fuzz_target!(|requests: Vec<Request<'_>>| {
max_resident_credential_count: None,
large_blobs: None,
nfc_transport: false,
ccid_transport: false,
firmware_version: Some(0),
},
);
+3
View File
@@ -110,6 +110,9 @@ impl<UP: UserPresence, T: TrussedRequirements> 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();
+4
View File
@@ -126,6 +126,10 @@ pub struct Config {
pub large_blobs: Option<ctap2::large_blobs::Config>,
/// 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.
+88
View File
@@ -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
);
})
}
+4 -1
View File
@@ -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<dyn Fn(&dyn DynFilesystem)>;
pub struct Options {
pub files: Vec<(PathBuf, Vec<u8>)>,
pub max_resident_credential_count: Option<u32>,
pub nfc_transport: bool,
pub ccid_transport: bool,
pub inspect_ifs: Option<InspectFsFn>,
}
+3
View File
@@ -977,6 +977,7 @@ pub struct GetInfoReply {
pub aaguid: Value,
pub options: Option<BTreeMap<String, Value>>,
pub pin_protocols: Option<Vec<u8>>,
pub transports: Option<Vec<String>>,
pub force_pin_change: Option<bool>,
pub min_pin_length: Option<u32>,
pub attestation_formats: Option<Vec<String>>,
@@ -992,6 +993,8 @@ impl From<Value> 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)